Intro
Github repository for the project: https://github.com/IvanGadjo/OneVendorsBundle_ModFedPlugin_SplitChunksPlugin
Webpack’s module federation is a technique which gives us an insight in how the future of micro-frontend architecture may look like. With the ability to share and dynamically run code between applications, the ModuleFederationPlugin boasts powerful features which have perspective future (You can read more about it here).
The idea for this blog post came to me while working on a project on my internship. I had used Webpack’s ModuleFederationPlugin to share both component and vendor library modules between two web apps. The problem was that I had 14 different vendor modules to share, but I needed to have them all bundled into one common vendors chunk, in order to reduce the network load of having 14 different requests at the same time. Therefore, the idea was to have all different vendor bundles bundled into one, so to have only one request from the host app to the remote app when the vendor library is needed.
In this post I will try to demonstrate the power of using Webpack’s ModuleFederationPlugin to share modules between two simple web applications, one acting as a host (app1) and the other one as a remote (app2). Moreover, to make it simpler, both apps will be written in plain JavaScript. The idea is that the host will load the bundles of a function, which uses one Lodash method, as well as a button component, which uses the D3 library, directly from the remote app using Webpack’s ModuleFederationPlugin. Finally, I will show you how to achieve bundling these two vendor libraries’ bundles into one bundle using Webpack’s SplitChunksPlugin, so that they can be shared between the remote and host applications as one chunk and improve performance.
Project Structure
The project is consisted of the host app – app1, which loads a shared function, a shared component and a vendors bundle from the remote app – app2. This is just a simple demo showing the work of Webpack’s ModuleFederationPlugin and SplitChunksPlugin. The final project structure should look like this:
Setup
After creating two folders, one for the host and one for the remote app, cd into the Remote_App directory
▶ Remote_App
We will need to initialize a npm project and install webpack so we can produce bundles of our code, therefore run these 2 commands form your terminal:
- npm init
- npm i webpack webpack-cli --save-dev The next step is to create the src folder which will hold our shared modules
▶ Remote_App/src
Create a new file called bootstrap.js and another folder – sharedModules. In the sharedModules folder create our first shared function – mySharedFunction.js. Leave this file empty for now.
▶ Remote_App/src/bootstrap.js
Populate this file with the next line:
import('./sharedModules/mySharedFunction');
In order for the Webpack module federation to work, the best way to implement the sharing between code is through dynamic imports like this, although sharing through eager consumption of modules is also possible and static imports of shared modules are supported as well. This is because the shared components/vendors are loaded at runtime and it’s best to have them asynchronously imported. You can reference this section of Webpack’s documentation regarding this.
▶ Remote_App/webpack.config.js
Now cd back out of the source folder and create a webpack.config.js file which is the configuration file for using Webpack with our remote app:
const path = require('path');
module.exports = {
entry: './src/bootstrap.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
mode: 'development'
};
The entry point would be our bootstrap.js file. This file would act as an entry point for the dynamic imports of all the shared modules you could have. Every bundle will be outputted to the dist folder.
▶ Host_App
Just as before, we need to initialize a npm project an install webpack:
- npm init
- npm i webpack webpack-cli --save-dev
▶ Host_App/src
For the same reasons as in the remote, create a bootstrap.js file. Also create an empty mainLogic.js file. This file will later on contain dynamic imports of the shared modules.
▶ Host_App/src/bootstrap.js
import('./mainLogic');
▶ Host_App/webpack.config.js
You can copy-paste the config file for Webpack in this host app from the remote app. It contains almost the same config, except for the filename prop, it will be only called bundle.js as we will have only that one app-related bundle.
filename: 'bundle.js'
Hosting the apps
To achieve hosting the apps we use webpack-dev-server (it is a CLI-based tool for starting a static server for your assets). Besides installing webpack-dev-server, we also need the HtmlWebpackPlugin so we can render html files. Therefore, you need to cd in both host and remote app directories and run the following commands:
- npm i webpack-dev-server --save-dev
- npm i html-webpack-plugin --save-dev
Next we need to add extend both webpack config files, of the host app as well as the remote:
▶ Host_App/webpack.config.js
devServer: {
static: path.join(__dirname,'dist'),
port: 3001
},
After including this option in our webpack config file of the host, the content from the dist folder will be rendered on port 3001. Lets create one html page now:
▶ Host_App/src/template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= htmlWebpackPlugin.options.title %> </title>
</head>
<body>
HOST APP
</body>
</html>
The htmlWebpackPlugin.options.title comes from the title property of the HtmlWebpackPlugin we define in the next step.
▶ Host_App/webpack.config.js
At the top we need an import for the plugin:
const HtmlWebpackPlugin = require('html-webpack-plugin');
We also create a plugins prop in the webpack config file containing our HtmlWebpackPlugin setup like this:
plugins: [
new HtmlWebpackPlugin({
title: 'Host app',
template: path.resolve(__dirname, './src/template.html')
})
]
Now you can add this command to your npm scripts which will start the server. In the package.json, under scripts add "start": "webpack serve --open"
. Now if you execute npm start
in the terminal, the server should be started at port localhost:3001. Only a white background will be shown with the text “HOST APP” written on the screen.
▶ Remote_App
The same steps are replicated in the remote app. Firstly install the required npm packages, then create a template.html and add the npm script for starting the server in the package.json
▶ Remote_App/webpack.config.js
Update the webpack.config.js file of the remote app to look like the following:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = {
entry: './src/bootstrap.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
mode: 'development',
devServer: {
static: path.join(__dirname,'dist'),
port: 3000
},
plugins: [
new HtmlWebpackPlugin({
title: 'Remote app',
template: path.resolve(__dirname, './src/template.html')
})
]
};
Using Module Federation & adding vendor libraries
Until this point we only set up the starting code for both apps and hosted them on different ports. Now we need to truly utilize Webpack’s module federation plugin, and the next thing which we would do is share two modules - ordinary JS function which uses a feature from our first shared vendor library – Lodash and a button styled with the D3 library (D3 is a JS library for manipulating documents based on data, but in our case, for the sake of simplicity we will use it to style the button only).
▶ Remote_App
Let’s start with the remote. Firstly npm install the Lodash and D3 libraries
- npm install lodash d3
▶ Remote_App/src/sharedModules/mySharedFunction.js
The function which will be shared is called myFunction(). It will use the sortedUniq() method from Lodash to remove duplicates from an array of numbers:
import _ from 'lodash';
export const myFunction = () => {
let sampleArray = [1,1,2,2,2,3,4,5,5,6];
let sortedArray = _.sortedUniq(sampleArray);
console.log('My resulting array: ' + sortedArray);
}
▶ Remote_App/src/sharedModules/mySharedButton.js
import * as d3 from 'd3';
// create button & fill with text and id param
let d3Btn = document.createElement('button');
d3Btn.setAttribute('id','btn-d3');
d3Btn.appendChild(document.createTextNode('D3 Button'));
// append to the body
let container = document.getElementsByTagName('body');
container[0].appendChild(d3Btn);
// use d3
// change color of text to orange
d3.select('#btn-d3').style('color','orange');
We just create a button and use D3 to change the internal text color of it.
▶ Remote_App/src/bootstrap.js
Next step is to import the modules dynamically, so the bootstrap file would look like this now:
import('./sharedModules/mySharedFunction');
import('./sharedModules/mySharedButton');
▶ Remote_App/webpack.config.js
To enable the usage of the ModuleFederationPlugin we need to register it in the config file. Import at the top of the file:
const { ModuleFederationPlugin } = require('webpack').container;
In the plugins section of the config we register the plugin:
new ModuleFederationPlugin({
name: 'remoteApp_oneVendorsBundle',
library: {
type: 'var',
name: 'remoteApp_oneVendorsBundle'
},
filename: 'remoteEntry.js',
exposes: {
'./mySharedFunction':'./src/sharedModules/mySharedFunction.js',
'./mySharedButton':'./src/sharedModules/mySharedButton.js'
},
shared: [
'lodash', 'd3'
]
})
We register a name for our application – it would be used by the host app to connect with the remote. We also register a script by the name of remoteEntry.js. This will be the “magic” script which enables the sharing of modules between our two apps, and will be automatically generated when building our app. To put it shortly, through the use of multiple Webpack plugins under the hood of ModuleFederationPlugin, Webpack’s dependency graph can also map dependencies remotely and require those JS bundles during runtime.
We also need to have a shared section where we put the vendor libraries which we like to be shared with the host app.
▶ Host_App/webpack.config.js
The only thing we need to do in the host application is to add some code to configure the ModuleFederationPlugin to work with the remote app. First we require the plugin:
const { ModuleFederationPlugin } = require('webpack').container;
And in the plugins section we should have the following code:
new ModuleFederationPlugin({
name: 'hostApp_oneVendorsBundle',
library: {
type: 'var',
name: 'hostApp_oneVendorsBundle'
},
remotes: {
remoteApp: 'remoteApp_oneVendorsBundle'
},
shared: [
'lodash', 'd3'
]
})
Here we need to register the remote app in order to share modules. In our host app we would reference the remote by the name “remoteApp”, as we register it like that in the remotes section of the ModuleFederationPlugin. We also need the Lodash and D3 to be shared. The vendor bundles will be loaded together with the bundle for the shared function and button.
▶ Host_App/src/template.html
We only need to add a <script>
tag in the <head>
of template.html to make everything work:
<script src='http://localhost:3000/remoteEntry.js'></script>
The shared myFunction() will be loaded with a click of a button, and we need a <div>
which will act as a container for rendering the button, that’s why we need this code in the <body>
:
<button id="btn-shared-modules-loader"
style="display: block; margin-top: 10px;">Load shared modules</button>
<div id='shared-btn-container' style="margin-top: 10px;"></div>
▶ Host_App/src/mainLogic.js
By document.getElementById() we get the button from the template.html and we add an onClick event listener which dynamically loads the shared function and button bundle:
let loadSharedModulesBtn = document.getElementById('btn-shared-modules-loader');
loadSharedModulesBtn.addEventListener('click', async () => {
let sharedFunctionModule = await import('remoteApp/mySharedFunction');
sharedFunctionModule.myFunction();
let sharedButtonModule = await import('remoteApp/mySharedButton');
let sharedButton = document.createElement(sharedButtonModule.name);
let sharedButtonContainer = document.getElementById('shared-btn-container');
sharedButtonContainer.appendChild(sharedButton);
})
Now it is a good idea to bundle our code. Add the following npm script to the package.json of both apps: "build": "webpack --config webpack.config.js"
. After executing npm run build
in both apps you will see the resulting dist folders containing all the bundles produced by Webpack.
Moreover, if you now start both apps and in the host you click on the Load shared modules button, the D3 button would show, the console log from the shared function will display the filtered array and both vendor bundles will be loaded from the remote. It is important to start the remote app first, or just reload the host if you started the apps in the different order.
If you open the network tab of developer tools in the browser, we can see that the Lodash, D3 and shared modules bundles are not loaded without a click on the button. After the click all bundles are loaded and in the console we get the message from myFunction() from the remote, but we also see the shared button. If you hover over the name of the bundles you can see they are actually coming from the remote, from localhost:3000.
Achieving one vendors bundle
The initial use of Webpack’s SplitChunksPlugin is to achieve code splitting – splitting code into smaller bundles and control resource load . Nonetheless, in my case I reversed this process - I came up with a crafty way of using it to bundle all vendors code into one bundle. In this example we just have a small number of vendor bundles, but this can be quite beneficial and performance optimizing when working on a bigger scale with many smaller vendor modules, assuming we need to load all vendor bundles at the same time.
▶ Remote_App/webpack.config.js
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/](lodash|d3|delaunator|internmap|robust-predicates)/,
name: 'Vendors_Lodash_D3',
chunks: 'all'
}
}
}
}
In case you were wondering about delaunator, internmap … Those are modules which are added when installing D3, if you do not include them in the regex they will produce separate vendor modules for themselves in the dist directory, which is not what we wanted to achieve. This can also be avoided if D3 is imported more selectively (not have import * as d3 from d3
).
Now running npm run build
in the remote app will result with a common vendor bundle in the dist folder called Vendors_Lodash_D3.bundle.js.
Finally, if you start both apps, the remote will load the whole Vendors_Lodash_D3 bundle by itself and not load any other vendor modules:
After clicking the load shared modules button in the host app, it will load both bundles for the shared function and shared D3 button, but also it will load only one vendor bundle - Vendors_Lodash_D3:
Conclusion
In this post I demonstrated the power and potential of using Webpack’s ModuleFederationPlugin to share code between two web applications. Moreover, by using a clever combination of Webpack’s ModuleFederationPlugin and SplitChunksPlugin, we can bundle more vendor modules into one, therefore relief network load and improve bundle loading performance between the apps.
I hope this post has been helpful to many of you from the community and you will use this implementation in your projects. Big thanks to Zack Jackson @scriptedalchemy for convincing me to write a blog post on this topic.
Top comments (1)
Very informative article, thanks for sharing.