Separating a Vue project into modules
Have you ever wanted to properly separate your Vue project into plugin-type modules? In this example I’ll show you the basics of making self-contained modules which can be dynamically loaded (link to end result at the bottom).
Disclaimer — This is not a beginner article, and while I’ll add a few links, I’m assuming you are already comfortable with Vue, Vuex, and Vue-Router.
Setup
Beginning with a basic PWA template you can clear everything except App.vue and main.js. For the sake of simplicity, just strip App.vue back to just a <router-view> element.
Add a “services” folder containing router.js and store.js, and populate them with your Router and Store declarations. Finally, import and include them into your main.js. Here’s my code.
System Module
We’ll store all our modules together in a “modules” folder, so create that in the root. We’re also going to work on the System Module first so create a subfolder called “system” with the folders and files shown.
Next, let’s give the routes.js an empty array to export:
export default []
Similarly, give the store.js a skeleton store to export:
export default {
namespaced: true,
state: {},
getters: {},
mutations: {},
actions: {}
}
For anyone not familiar with the namespaced property, it’s for Vuex namespacing, which allows us to have identical state, getter, mutation, and action names in each module’s store (such as “initialize”).
Now let’s package this module’s routes.js and store.js into the module.js file, as well as give it a name property:
import routes from './module/routes'
import store from './module/store'export default {
name: 'system',
routes: routes,
store: store
}
Now to add some actions to the System Module store.js. We’re going to make two actions; first a generic initialize() action (which for now just outputs text to the console), and then the initializeModule(); which accepts module.js files as its payload and injects them into the app. The System Module’s store.js should updated to:
import Store from '../../../services/store'
import Router from '../../../services/router'
export default {
namespaced: true,
state: {},
getters: {},
mutations: {},
actions: {
initialize ({ dispatch }) {
console.info('System initializing...')
console.info('System initialized.')
},
initializeModule ({ dispatch }, module) {
Store.registerModule(module.name, module.store)
Router.addRoutes(module.routes)
dispatch(module.name + '/initialize', null, { root: true })
}
}
}
A bit of an explanation: The initializeModule() function makes use of Vuex’s registerModule() to inject the module’s store, and Vue-Router’s addRoutes() to inject the module’s routes. Note that the module name given to registerModule() will become it’s namespace path. To finish off initializeModule() we want to dispatch the given module’s intitialize() action by using it’s name as the namespace path. Note the third parameter ({root:true}) needed in the dispatch call to break out of the current namespace (and return to the root).
And that completes the System Module; but before we move onto the next module, this special loader module needs to be manually loaded. Going back to your main.js add the following after your imports and before your new Vue instance:
import system from './modules/system/module'Store.registerModule('system', system.store)
Router.addRoutes(system.router)
Store.dispatch('system/initialize', null, { root: true })
That should do it. When you run the application, you won’t see anything (as we haven’t yet added routes to any pages), but you should see the console outputs indicating that the system module has initialized (and if there are no errors, it has also injected its store and routes).
System initializing...
System initialized.
Site Module
Alright, let’s make a new module now called “site”. It will be the exact same files and structure as the System Module, but will have an extra pages folder of .vue files (shown left). If you’re copying and pasting your System Module, remember to update the initialize() action to log “site initializing…” (rather than“system initializing…”); update the name property of module.js to be “site”; and remove the initializeModule() action from store.js.
The purpose of this module is to display all those general website pages which are mostly static, so let’s start with that. Just so we can see something load, update those newly created .vue files with some text. For example, add the following to Home.vue:
<template>
<div>
<h1>Home Page</h1>
<p>Welcome home!</p>
<ul>
<li><router-link to="/contact">Contact</router-link></li>
<li><router-link to="/faq">FAQ</router-link></li>
</ul>
</div>
</template>
Then add those pages as routes in our Site Module’s routes.js:
import Home from '../pages/Home.vue'
import Contact from '../pages/Contact.vue'
import FAQ from '../pages/FAQ.vue'
export default [
{ path: '', component: Home },
{ path: '/contact', component: Contact },
{ path: '/faq', component: FAQ }
]
Because the System Module is pretty useless on its own, and we will always need the Site Module, let’s use the System Module’s initialize() action to always load the Site Module. To do that, all we need to do is update the Site Module’s module.js to dispatch an initialization call. Note that because this particular initialize() action is in the same namespace as the initializeModule() action, we don’t need the namespace path, nor the 3rd parameter to force a root call.
import Store from '../../../services/store'
import Router from '../../../services/router'
import siteModule from '../../site/module'export default {
namespaced: true,
state: {},
getters: {},
mutations: {},
actions: {
initialize ({ dispatch }) {
console.info('System initializing...')
console.info('System initialized.')
dispatch('initializeModule', siteModule)
},
initializeModule ({ dispatch }, module) {
Store.registerModule(module.name, module.store)
Router.addRoutes(module.routes)
dispatch(module.name + '/initialize', null, { root: true })
}
}
}
Now when you run the app, you will see your home page. We will be looking into automatically loading module navigation buttons in Part 2, but for now you can just test out the url links from Home.vue or by visiting the pages directly: http://localhost:8080/contact (for example).
Another Module
So we now have everything in place to add our first functional module. Duplicate the Site Module folder and call it moduleA. Replace the pages with something different (I used Buildings.vue, Colors.vue, Trees.vue), and update their contents to reflect that. We then need to update the new routes being added in the routes.js and update the initialize() action in the store.js to log “ModuleA initializing…” instead of “Site initializing…”. Finally, update module.js name property to “moduleA”.
And now we just need somewhere to load it. So let’s update Home.vue in the Site Module to include a button and a dispatch call:
<template>
<div>
<h1>Home Page</h1>
<p>Welcome home!</p>
<ul>
<li><router-link to="/contact">Contact</router-link></li>
<li><router-link to="/faq">FAQ</router-link></li>
<li><router-link to="/trees">Trees</router-link></li>
</ul>
<button @click="loadModuleA">Load Module A</button>
</div>
</template><script>
import moduleA from '../../moduleA/module'
export default {
methods: {
loadModuleA () {
this.$store.dispatch('system/initializeModule', moduleA)
}
}
}
</script>
And that’s it. Let’s run the app and you should see this:
If you click on the “Trees” link, your page should go blank because it’s a link to a route that doesn’t yet exist. But if you first press that button to load Module A, you should notice the moduleA initialization log in the console; and now when you click on the “Trees” link it should display ModuleA’s Tree.vue.
Conclusion
In the end, we now have a way to isolate pages, routes, and stores into completely separate modules; enabling better code management, and separation of work if you’re working in a team. Another area this separation helps is the ability to avoid loading certain routes or actions altogether; thereby adding a little more access control. I haven’t yet tested any performance differences; but my gut tells me that for most cases, even if there are any, they won’t be significant.
In Part 2 we’ll add automatic page navigation links for loaded modules. Please give me some love on this one so I can muster the effort for the next one!
As this was an example to show the core concept, I cut out a lot of features for simplicity which are worth adding yourself:
- Tracking which modules are currently loaded
- Avoid re-loading already loaded modules
- Unload module function
- A module dependency list for each module and dependency check before loading a module.
- …