If you have any questions or feedback, pleasefill out this form
This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.
In a recent project, we utilized Vue for development, and when it comes to managing more complex data flow and states, Vuex is typically employed as the Single Source of Truth store. When creating a store in Vue, all modules are generally written first and then collectively added to the root of Vue.
export default new Vuex.Store({
modules: {
profile,
users,
menus,
list,
food,
product,
todo,
...
},
});
This approach works well for typical small to medium-sized projects. However, as the architecture grows larger, the structure of the store can easily become bulky and increasingly complex. Additionally, having too many actions and mutations within modules can unnecessarily increase the bundle size, and not all modules need to be used immediately after app initialization. To address this, Vue leverages Webpack's dynamic import mechanism for dynamically loading components.
In Vuex, you can use store.registerModule
to add modules to the store only when needed. With this API, we can also combine it with Webpack's dynamic import mechanism to reduce bundle size and simplify operations, allowing us to include only the necessary modules during app initialization.
Today, I’m going to introduce how to dynamically load modules using Webpack's dynamic import mechanism.
Webpack Dynamic Import
Before we dive in, let's discuss how Webpack's dynamic import mechanism works. Generally, the methods to achieve code splitting in Webpack include:
- Setting multiple entries
- Using the SplitChunks plugin to create different chunks
- Using the dynamic import mechanism to bring in code
If we want to dynamically import the corresponding module in a component, the most convenient method is to use the dynamic import mechanism. It allows import()
to become a function that returns a Promise, which Webpack will automatically split into separate chunks during the build process. For example:
import(/* webpackChunkName: "CreateMenu" */ './pages/NewMenu.js'),
During the Webpack build, CreateMenu.js
will be extracted into a separate chunk.
Version: webpack 4.16.3
Time: 112ms
Built at: 2018-08-20 14:49:40
Asset Size Chunks Chunk Names
Profile.c943bf21.js 32.7 KiB Profile [emitted] Profile
The official documentation provides a way to specify chunk names in comments, which helps clarify which chunk is being imported during debugging.
This mechanism requires an additional Babel plugin configuration: Syntax Dynamic Import Babel Plugin.
Now that we understand how to use Webpack's dynamic import, let’s integrate it with Vuex.
Timing of Import
When dynamically importing modules, there are a few questions to consider:
- When should the module be imported?
- How should we handle errors if the import fails?
When to Import the Module?
You should load the module when a component might need the store's state (obviously). Thus, we can write:
// component.vue
export default {
mounted() {
import("./modules/menus").then(menus =>
this.$store.registerModule("menus", menus.default)
)
},
render() {
// your template
},
}
This looks straightforward, but soon we encounter a few issues:
- If the import isn’t finished by the time we render, accessing
menus
data in the template will result in an error due to it being undefined. - If the module has already been imported in another component, it will throw an error:
duplicate getter key: menus
.
To fix these issues, we can slightly modify our approach:
// component.vue
export default {
data: () => ({
loaded: false,
}),
mounted() {
if (this.$store.state.menus) {
import("./modules/menus").then(menus => {
this.$store.registerModule("menus", menus.default)
this.loaded = true
})
} else {
this.loaded = true
}
},
render() {
return this.loaded ? h() : null
},
}
This indeed looks much better, but repeating this logic across every component that needs store data can become cumbersome. So, let’s extract it into a reusable higher-order component (HOC).
export default function createMenuModule(Component, moduleName, dynamicModule) {
return Vue.component(`dynamicModule-${Component.name || 'Component'}`, {
data: () => ({
isLoaded: false,
}),
mounted() {
if (this.$store.state[moduleName]) {
dynamicModule
.then(module => this.$store.registerModule(moduleName, module.default)) // register module into store
}
},
render(h) {
return this.isLoaded ? (
<Component {...this.$props} />
) : null;
},
});
}
// MenuList.js
export default createModule(MenuList, import(/* webpackChunkName: "Menus" */ './modules/menus')); // return a higher order vue component
This way, we can confidently import the corresponding module when needed, without having to deal with the annoying loading logic in every component. You can also modify the parameters to allow this function to accept multiple modules. Of course, this introduces more considerations (like module name mapping and handling multiple promises), but the concept remains similar.
Load Only When Necessary
In the previous example, we resolved the issues mentioned earlier. However, we can observe that if we simply write import()
in the parameters, it seems like a request will be sent regardless! What if we checked the store first to see if the corresponding data exists before deciding to load?
So, let’s modify the function to accept the store as a parameter.
export default function createMenuModule(Component, moduleName, loader = () => Promise.resolve()) {
return Vue.component(`dynamicModule-${Component.name || 'Component'}`, {
data: () => ({
isLoaded: false,
}),
mounted() {
if (this.$store.state[moduleName]) {
loader()
.then(module => this.$store.registerModule(moduleName, module.default)) // register module into store
}
},
render(h) {
return this.isLoaded ? (
<Component {...this.$props} />
) : null;
},
});
}
// MenuList.js
export default createModule(MenuList, 'menus', {
loader: () => import('./modules/menus'),
})
By passing the last parameter as a function, we allow for more handling in the loader (error handling, error logging, GA, etc.), making the entire component more robust. If you want to be more thorough, the third parameter could also accept mechanisms like timeout or LoadingComponent, enhancing the usability of this higher-order function. (However, in most cases, you’d want the module to load as quickly as possible.)
Error Handling
While we hope that promises load smoothly, many factors can impact module loading, such as unstable internet connections or going offline. Therefore, we need an error handling mechanism.
In the mounted lifecycle hook, we can utilize catch to handle errors and set a new data property to record error information.
export default function createMenuModule(Component, moduleName, loader = () => Promise.resolve()) {
return Vue.component(`DynamicModule-${Component.name || 'Component'}`, {
data: () => ({
isLoaded: false,
error: null,
}),
mounted() {
if (this.$store.state[moduleName]) {
loader()
.then(module => this.$store.registerModule(moduleName, module.default)) // register module into store
.catch(err => {
this.error = err
sendToLoggingService(err);
})
}
},
render(h) {
if (this.error) {
return h('pre', this.error);
}
return this.isLoaded ? (
<Component {...this.$props} />
) : null;
},
});
}
// MenuList.js
export default createModule(MenuList, 'menus', {
loader: () => import('./modules/menus'),
})
You can also take advantage of Vue’s errorCaptured or implement an ErrorBoundary
component to log this information.
When to Unregister
When should we unregister? In the first half, we dealt with loading, but for unloading, we can’t directly implement it in the component unmount, as we don’t know if other components are accessing the store data. So unless you are confident that only a single component will use it, avoid unregistering the module casually.
Other Considerations
While it’s not as straightforward in React, you can use react-loadable. However, Redux does not have a similar API like registerReducer, so you would need to implement that yourself. If you’re using libraries like redux-observable or redux-saga (for dynamically loading epics or sagas), you could implement a similar approach.
Conclusion
Compared to React and Redux, the Vue ecosystem offers better support for asynchronous loading (making it easier to implement). Certainly, introducing such mechanisms can increase debugging complexity, although it effectively reduces bundle size. However, various mechanisms need to work together to ensure the app runs smoothly.
This article attempts to address potential issues and solutions encountered during dynamic loading, hoping to assist developers grappling with bundle size challenges.
If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨
☕Buy me a coffee