In the previous article I introduced the concept of Domain-Driven-Design and showed how it can be utilized within Nuxt with their modules. In this part I want to share with you how we applied the same concept in Vue Storefront 1 which is a regular, Vue 2 application.
In short words - we want to build something similar to Nuxt modules in a regular Vue.js application. Of course, we will cut down a lot of functionalities and focus only on the ones that we need for this particular use case.
Such module should meet the following requirements:
This is more or less how we used the Nuxt module in the previous article. Now let's see how we can create this architecture, starting with the module API itself.
In Vue Storefront we decided to define each module as a function. The module function has access to all vital parts of the application through a context object:
function categoryModule({ app, router, store }) {
// we can also use app.$router, app.$store but this approah is more readable to me
}
TIP: Putting all function parameters in a single object makes it easier to maintain. It’s easier to remove or add new properties into such functions in the future because their order doesn’t matter.
Let’s quickly review all the parameters and for what we would need them:
The app
is our root Vue instance. We will need it for:
installing Vue plugins
Vue.use(plugin)
CAUTION: For the plugins that are adding properties to the Vue.prototype
, it’s important that we register the module before the Vue instance initialization!
registering global components/directives
Vue.component('ComponentName', ComponentInstance)
Vue.directive('directiveName', directiveInstance)
CAUTION! Avoid directly adding properties to global configuration like this:
Vue.myGlobalProperty
You can accidentally create dependencies between modules (which is something we try to avoid) if you use these properties in other modules.
The router
instance will be used for
router.addRoutes(routes)
router.beforeEach()
router.afterEach()
We will use store
to register Vuex modules
store.addModule('ModuleName', moduleInstance)
That’s all we need for our module system to integrate with the app.!
Now let’s take a look at the Nuxt module from the previous article:
// index.js
module.exports = function ProductCatalogModule (moduleOptions) {
this.extendRoutes((routes) => {
routes.unshift({
name: 'product',
path: '/p/:slug/',
component: path.resolve(themeDir, 'pages/Product.vue')
});
);
// we can't register stores through Nuxt modules so we have to make a plugin
this.addPlugin(path.resolve(__dirname, 'plugins/add-stores.js'))
}
// plugins/add-stores.js
import CategoryStore from '../stores/category.js'
import ProductStore from '../stores/product.js'
export default async ({ store }) => {
store.registerModule(CategoryStore)
store.registerModule(ProductStore)
};
This simple Nuxt module adds a single route and then registers two Vuex modules. It looks like we have everything needed to recreate it with our new API. Let’s try!
import ProductPage from './pages/Product.vue'
import CategoryStore from '../stores/category.js'
import ProductStore from '../stores/product.js'
export function categoryModule({ app, router, store }) {
router.addRoutes([{
path: '/p/:slug/',
component: ProductPage
}]);
store.registerModule('category', CategoryStore)
store.registerModule('product', ProductStore)
}
And thats it, even less code than with Nuxt!
Now that we know how our API will look like let’s start implementing the underlying business logic. For now the module is just a function and we have to create the bindings between this function and the rest of the app.
Let’s see how easy it is to create a registerModule
function!
// modules.js
// router instance
import router from './router.js'
// store instance
import store from './store.js'
// vue instance
import Vue from 'vue'
export function registerModule(module) {
module({ app: Vue, store, router })
}
We’re just passing the app, store and router object into the module function.
Now when we want to register our catalog module we’re doing it like this in the entry point of our application (or somewhere else if the module is not including routes):
// main.js
import { CatalogModule } from './modules/catalog'
import { registerModule } from './modules.js'
import Vue from 'vue'
// instantiate router, store and plugins
const app = new Vue({
// options
))
registerModule(CatalogModule)
And that’s all! now we have a working simple module system in the regular Vue application! This concept is really powerful and can be easily extended if needed.
Once your app will become a little bit more complex you’ll quickly realize that some level of communication between modules is inevitable. In Vue Storefront we wanted modules to be notified about certain events in the app like user log in/log out actions.
There are many ways and approaches to this topic and perhaps I’ll write a separate article about this in the future but today I will share the simplest solution to that problem - Event Bus.
We will declare a set of global events that could happen in our app that other modules can listen to and emit. By not putting these events declaration inside any module we’re making sure that they are always accessible and not tied to the implementation details of our auth module.
Let’s start with the Event Bus implementation. If that's the first time you hear this term don’t get scared. In Vue world Event Bus is just a Vue instance that is used to emit and listen to events.
// eventBus.js
import Vue from 'vue'
const eventBus = new Vue()
export default eventBus
That’s the whole declaration! Now we can use this Event Bus anywhere in our app:
// emit an event
bus.$emit('init')
// we can perform some actions after the event is being emitted by listening to it
bus.$on('init', () => console.log('Hello world!')
The problem I have with Vue events system is that its using strings as identifiers. It’s super hard to debug them if you accidentally make a typo somewhere.
Since we already know what events we would like to be globally available in our app we can explicitly declare them:
// eventBus.js
import Vue from 'vue'
const eventBus = new Vue()
const EVENT_TYPES = { logIn: 'logIn', logOut: 'logOut' }
export default {
$emit: {
logIn: (payload) => eventBus.$emit(EVENT_TYPES.logIn, payload)
logOut: (payload) => eventBus.$emit(EVENT_TYPES.logOut, payload)
},
$on: {
logIn: (callback) => eventBus.$on(EVENT_TYPES.logIn, callback)
logOut: (callback) => eventBus.$on(EVENT_TYPES.logOut, callback)
}
}
We used EVENT_TYPES
enum to map string identifiers into a data objects similarly like we do with mutation types in Vuex.
With this approach we’re not only eliminating the risk of making a typo but also improving Developer Experience with autosuggestions.
import eventBus from './eventBus.js'
eventBus.$emit.logIn()
The only thing that's left is injecting the event bus instance into our module system so it can be accessed in every module:
// modules.js
// router instance
import router from './router.js'
// store instance
import store from './store.js'
// vue instance
import Vue from 'vue'
// event bus instance
import eventBus from './eventBus.js'
export function registerModule(module) {
module({ app: Vue, store, router, eventBus })
}
As you can see extending our module system with additional properties is a straightforward process because we used a single object as a parameter we can easily add new properties without introducing breaking changes to already existing modules.
Now, let’s see this Event Bus in action to understand it a little bit better.
In our app we will probably have some kind of authorization module with a `logIOut action in its Vuex store:
export function authModule({ app, router, store, eventBus }) {
const authStore = {
// state, mutations, getters...
actions: {
// other actions
logOut({ commit }) {
// clear token, end session
eventBus.$emit.logOut()
}
}
}
store.registerModule('auth', authStore)
}
After all the steps required for the log out are performed we are notifying the rest of our app about the event so it can react to it.
Now the cart module could listen to this event and clear the cart once the user is logged out
export function cartModule({ app, router, store, eventBus }) {
// register vuex store, routes etc
eventBus.$on.logIn(store.dispatch('cart/load'))
eventBus.$on.logOut(store.dispatch('cart/clear'))
}
This way we’re avoiding tight coupling of modules while still allowing them to communicate with each other. If you follow this approach you’ll cut down the complexity of your app by boiling it down into a set of smaller and scoped “sub-applications”.
Vue is a very flexible framework. Even though it’s not promoting a DDD concepts with the common best practices it’s straightforward to adjust our application to follow this approach.
Let me know in the comments if you like this series and if you’d like me to explore this concept more in the next articles!
Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.