In recent projects, I have been using Vue for development. When it comes to managing complex data flow or state, I usually use Vuex as a store for Single Truth of Source. In Vue, when creating a store, all modules are written first and then placed in the root of Vue.
export default new Vuex.Store({
modules: {
profile,
users,
menus,
list,
food,
product,
todo,
...
},
});
This approach works fine for small to medium-sized projects. However, as the project structure becomes larger, it can easily make the data structure of the store more complex and larger. Additionally, having a large number of actions and mutations in modules can increase the bundle size unnecessarily. Not all modules are used immediately after app initialization. To address this, Vue provides dynamic import mechanism through webpack for async components.
In Vuex, we can use store.registerModule
to add modules to the store only when needed. With this API, we can reduce the bundle size by leveraging webpack's dynamic import mechanism and simplify all operations. During app initialization, we only need to include the necessary modules.
Today, I will introduce how to dynamically load modules using webpack's dynamic import mechanism.
Webpack Dynamic Import
Before we begin, let's talk about webpack's dynamic import mechanism. Generally, there are three methods to achieve code splitting in webpack:
- Setting up multiple entry points
- Using the SplitChunks plugin to split chunks
- Using the dynamic import mechanism to import code
When 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. During webpack build, these files will be automatically split into separate chunks. For example:
import(/* webpackChunkName: "CreateMenu" */ './pages/NewMenu.js'),
During webpack build, CreateMenu.js
will be split 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 the chunk name using comments, which helps in debugging to identify the imported chunk.
To use this mechanism, you need to configure the Babel plugin Syntax Dynamic Import Babel Plugin.
Now that we know how to use webpack's dynamic import, let's integrate it with Vuex.
Timing of Import
To dynamically import modules, we need to consider a few questions:
- When should we import the module?
- How should we handle errors if the import fails?
When to import the module?
We should import the module when the component may require the store's state (obviously). So we can write it like this:
// component.vue
export default {
mounted() {
import("./modules/menus").then(menus =>
this.$store.registerModule("menus", menus.default)
)
},
render() {
// your template
},
}
This looks simple, but we quickly encounter a few issues:
- If the import is not completed before rendering, accessing the
menus
data in the template will cause an explosion due toundefined
. - If the module has already been imported in another component, there will be an error:
duplicate getter key: menus
.
To address these issues, let's make a slight modification:
// 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 loaded ? h() : null
},
}
This looks much better. However, it becomes cumbersome to repeat this logic in every component that requires store data. 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
Now we can safely import the corresponding module when needed, without having to handle the annoying loading logic in each component. Of course, you can modify the parameters to allow the function to accept multiple modules. However, this introduces more considerations (such as module name mapping, handling multiple promises, etc.), but the concept remains the same.
Lazy Loading
In the previous example, we solved the issues mentioned earlier. However, if we directly write import()
in the parameter, it seems like a request will always be sent, regardless of the circumstances! What if we can check if the corresponding data exists in the store before deciding whether to load it or not?
So let's modify the function further to allow passing 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'),
})
Now we can pass a function as the last parameter. This function can handle additional processing such as error handling, error logging, GA, etc., to make the component more robust. If we want to be more detailed, we can also pass additional parameters like timeouts and a LoadingComponent mechanism to make this higher-order function even more useful (although in most cases, we want the modules to load as quickly as possible).
Error Handling
Although we hope that promises will load smoothly and everything will be fine, there are many factors that can affect the loading of modules. Most commonly, it could be due to unstable network connections or being offline. Therefore, we need an error handling mechanism.
In the mounted
lifecycle hook, we can use catch
to handle errors and set a new data property to record the 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'),
})
Of course, we can also make use of Vue's errorCaptured
or implement an ErrorBoundary
component to record this information.
When to Unregister
When should we unregister? In the earlier part, we handled the loading, but unmounting cannot be directly implemented in the component because we don't know if there are other components accessing the store data. So unless you are certain that only a single component will use it, do not unregister the module indiscriminately.
Others
In React, it's not as convenient, but you can use react-loadable. However, Redux does not have an API similar to registerModule
, so you would need to implement it yourself. If you are using libraries like redux-observable or redux-saga (for dynamically loading epics or sagas), you can also implement similar mechanisms.
Conclusion
Compared to React and Redux, Vue's ecosystem has better support for asynchronous loading (easier implementation). However, introducing this mechanism inevitably increases the complexity of debugging, although it effectively reduces the bundle size. It is important to combine various mechanisms to make the entire app more robust.
This article attempts to address the issues and solutions that may arise when dynamically loading modules, and I hope it can help developers who are struggling with bundle size.