Good frameworks exist out there that help us create micro-frontends such as single-spa and OpenComponents. But what if we don’t want to integrate a different framework in our architecture? Let’s take a look at how to implement micro-frontends in our Vue apps.
In this article, we will discuss how to dramatically simplify our app architecture by using webpack 5’s Module Federation to consume and share micro-frontend components with example code.
Module federation is a JavaScript architecture invented by Zack Jackson. This architecture allows the sharing of code and dependencies between two different application codebases.
The code is loaded dynamically, and if a dependency is missing, the dependency will be downloaded by the host application, which allows for less code duplication in the application.
The concept of micro-frontends has been gaining traction in recent times. The push for microservices has also brought about the same implementation to the modern web in the form of micro-frontends. As the monolith app scales, it becomes difficult to maintain, especially across several teams working on the same app.
We can look at micro-frontends as feature-based, where there are different teams and each team handles a particular feature component while another team handles something else. In the end, all teams merge the different components they have built to form one application.
Developers made use of frameworks like single-spa and OpenComponents to achieve this, but with the new release of webpack 5 and the module federation, we can easily achieve the same goal, but way easier.
Adopting a micro-frontend approach to building your web applications is probably the best strategy. This is especially true if you are building a large-scale web application with many moving parts or applications that are branched out into sub-applications where you want some consistency in the overall look.
Let me highlight a few reasons you might want to switch to the micro-frontend approach:
These are some ways developers split large apps:
We have explained some concepts about micro-frontends and module federation. Now it’s time for a proof of concept.
Here, we will demonstrate how we can use the module federation to create micro-frontends in Vue. To test this out, we will be spinning up two different apps, so we can create a micro-frontend in one of them and share it with the other.
First, we create a folder to host the two Vue applications:
mkdir vue-mf
It is in the vue-mf
folder we will run our Vue application. We won’t be using the Vue CLI here. Instead, we will be using the new release of webpack, which is webpack 5, to set up the Vue application.
We will name the two applications we want to share components as Company
and Shop
respectively. We create a folder for each of them in the vue-mf
folder and then grab a webpack starter file of Vue created by Jherr from GitHub into each folder:
git clone https://github.com/jherr/wp5-starter-vue-3.git
Let’s take a look at the file structure now that we have set up the app:
+-- vue-mf/ | +-- Company/ | +-- Shops/
When we open up one of the app folders, this is the structure:
+-- vue-mf/ | +-- Company/ | +-- src/ | +-- App.vue | +-- bootloader.js | +-- index.css | +-- index.html | +-- index.js | +-- package.json | +-- webpack.config.js | +-- Shops/
So we have two apps, Company
and Shop
, which are exactly the same for now. When we survey the file structure, we take a look at the package.json
. We have our webpack loader, CSS loader, and all the basic loaders and webpack stuff we need:
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); const { VueLoaderPlugin } = require("vue-loader"); module.exports = { output: { publicPath: "http://localhost:8080/", }, resolve: { extensions: [".vue", ".jsx", ".js", ".json"], }, devServer: { port: 8080, }, module: { rules: [ { test: /.vue$/, loader: "vue-loader", }, { test: /.css$/i, use: ["style-loader", "css-loader"], }, { test: /.(js|jsx)$/, exclude: /node_modules/, use: { loader: "babel-loader", }, }, ], }, plugins: [ new VueLoaderPlugin(), new ModuleFederationPlugin({ name: "starter", filename: "remoteEntry.js", remotes: {}, exposes: {}, shared: require("./package.json").dependencies, }), new HtmlWebPackPlugin({ template: "./src/index.html", }), ], };
If we take a look at the webpack.config.js
file, we can see that our public path is set to port 8080
. We can also see webpack checking for our file extensions and using the appropriate loaders.
The important thing to take note of here is our Vue loader plugin used in parsing our files and the Module federation plugin from webpack 5 we have imported and used, which will allow us to perform sharing functionality. We will get back to the configuration of the ModuleFederationPlugin
later in this tutorial.
N.B., make sure to set the public path and dev server port in the other application (Shop) to port 8081
, so we can be able to run both apps simultaneously.
In our application, the App.vue
file will serve as the homepage, so let’s add some markup:
<template> <div> <h1>Our Application Homepage</h1> </div> </template>
The header component is one part of an application we would like to share between applications. Let’s say one of the teams of developers decides to build the header, so we create a header
component that we can share in the two applications.
In the src
folder in our Company app, we will create a header
component. To do this, we create a Header.vue
file, and in it, we create the header
component:
<template> <div> <header> App Header </header> </div> </template>
After creating the header, navigate to App.vue
and import the header
component:
<template> <div> <Header /> <h1>Our Application Homepage</h1> </div> </template> <script> import Header from './Header.vue'; export default { components: { Header, }, } </script>
We can now start our development server by navigating to each folder and running:
yarn && yarn start
Right now our app looks like this in the Company app.
header
component through the Module Federation pluginWe now have our header in the Company app, we would like to use it in the Shop app. So we head over to the webpack configuration in the Company app:
plugins: [ new VueLoaderPlugin(), new ModuleFederationPlugin({ name: "Company", filename: "remoteEntry.js", remotes: {}, exposes: { "./Header": "./src/Header", }, shared: require("./package.json").dependencies, }), new HtmlWebPackPlugin({ template: "./src/index.html", }), ],
In the webpack Module Federation configuration, we set the name
to the app name, which is Company
, and remoteEntry.js
to be our filename. When we navigate to the remoteEntry.js
file name, we see the code related to the components and dependencies we want to share. We also exposed the header
component with its location.
Now, if we restart our server and navigate to http://localhost:8080/remoteEntry.js
, we will see this:
Now grab the remote entry URL and switch to the webpack configuration file in our Shop app:
plugins: [ new VueLoaderPlugin(), new ModuleFederationPlugin({ name: "Shop", filename: "remoteEntry.js", remotes: { Company: "Company@http://localhost:8080/remoteEntry.js" }, exposes: {}, shared: require("./package.json").dependencies, }), new HtmlWebPackPlugin({ template: "./src/index.html", }), ],
Here, we give the plugin a name of Shop
and set the remote to remoteEntry
URL. Then in our App.vue
file in the Shop app, we import and use the header
component from our Company app:
<template> <div> <Header /> <h2>Our Shop Page</h2> </div> </template> <script> import Header from 'Company/Header'; export default { components: { Header, }, } </script>
If we restart our server, we can see that the shop page now has the header component, meaning we have successfully shared the component between the two apps. Yay!
N.B., if the team working on the header decides to push a new update for the header
component, the Shop app team will immediately see the update once the Shop app is refreshed.
Let’s say you are using a state manager in your Vue application like Vuex. You might be asking yourself how you might have state, share it between the two components, and also have it update. So, let’s install Vuex for both apps:
yarn install vuex@next
Once we have installed Vuex, navigate to the bootloader.js
file in our Company app, where we initialize our Vue app.
Here, we import our store and create a state:
import { createApp } from "vue"; import { createStore } from 'vuex' import "./index.css"; import App from "./App.vue"; const app = createApp(App) const store = createStore({ state () { return { cartItems: 0 } } }) app.use(store) app.mount("#app");
If, for instance, this is an ecommerce store where we want to display the number of cart items we have in our cart, we create a cartItems
state and display it in our company header. Then go to our header
component, access the state, and display it:
<template> <div> <header> <h2> App Header</h2> <p>items: {{cartCount}}</p> </header> </div> </template> <script> export default { computed: { cartCount() { return this.$store.state.cartItems } }, } </script>f
We have successfully set up our state, but the problem with this is if we start the server for both apps and check the Company app, we can see the header display with the state:
But if we navigate to the Shop app, we can no longer see the shared header
component anymore, much less the state we added. Instead, we get an error message that says we can’t read the state of undefined, because in our Shop app, we haven’t set up any store.
To rectify this problem, we copy all the code we have in the bootloader of the Company app and paste it into the bootloader.js
file of the Shop app. This time, we changed the cartCount
state to 12
. If we restart the server, we now have our header in the Shop app, with cart items of 12
.
Let’s say we want to mimic the addition of more shop items to the cart, so in the Shop app, we add a button that increments the cartCount
state:
<template> <div> <Header /> <h2>Our Shop Page</h2> <div> <button @click="addItem">Add item</button> </div> </div> </template> <script> import Header from 'Company/Header'; export default { components: { Header, }, methods: { addItem() { this.$store.state.cartItems += 1 } }, } </script>
If we restart the Shop application, we can see that the items in the header now update. Yay!
We have come to the end of this tutorial.
Here, we discussed how to dramatically simplify our app architecture by using webpack 5’s Module Federation to consume and share micro-frontend components with example code.
Whether you should or should not adopt micro-frontends depends on the kind of project you are building, because this approach will not be the best for small applications or businesses. A micro-frontend architectural approach is your best bet when working on a large project with distributed teams.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowPlaywright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
Matcha, a famous green tea, is known for its stress-reducing benefits. I wouldn’t claim that this tea necessarily inspired the […]
Backdrop and background have similar meanings, as they both refer to the area behind something. The main difference is that […]
AI tools like IBM API Connect and Postbot can streamline writing and executing API tests and guard against AI hallucinations or other complications.
3 Replies to "Building micro-frontends with webpack’s Module Federation"
Thanks for writing this. I think you should check out the articles and work of Florian Rappl like https://dev.to/florianrappl/11-popular-misconceptions-about-micro-frontends-463p. We recently migrated away from a larger monolith and first tried module federation. Did not work out well, as this made everything quite tight and spaghetti.
Micro frontends when done right require a lot more than just some tool like module federation.
Nice Article 👍
const HtmlWebPackPlugin = require(“html-webpack-plugin”); is missing webpack.config.js