Microfrontend with Webpack Module Federation

Senura Vihan Jayadeva
9 min readMar 8, 2023

--

Hello everyone, in this write-up, I’ll provide a brief overview of Microfrontends and subsequently dive into Module Federation, explaining its functionality and the steps involved in its implementation.

Microfrontend Overview

Microfrontends is an architectural style for building web applications where the frontend is break down into smaller, more manageable and independent pieces that can be developed and deployed by different teams separately. Each microfrontend represents a distinct feature or functionality within the application, and it can be developed, tested, and deployed independently, using its own technology stack, programming language, and development tools.

Advantages in Microfrontends:

By breaking down the frontend into smaller, more manageable pieces, microfrontends can help organizations to scale their development efforts, reduce code complexity, and speed up time-to-market. They can also enable better collaboration between teams, as each team can work on their own microfrontend without having to worry about the impact on the rest of the application.

Overall, microfrontends can be a powerful way to improve the scalability, flexibility, and maintainability of large web applications, especially in complex enterprise environments.

Strategies:

Several strategies have been tried in frontend applications to break out from the monolithic architecture.

  1. The most popular method of sharing code between applications has been utilizing Node packages, and this practice has been around for a while. Keeping up with the changes in the most recent versions of each published package is a major drawback. That leads to an increase in the amount of time needed for testing, deployment, and upgrading to the modifications. As more and more packages are added, the application’s size also grows.
  2. Deploying JavaScript runtime packages to a CDN for consumption is another possible approach for micro-frontends, but it has some drawbacks. First, it requires custom logic to be written and handled by the framework, which means that the micro-frontends must be designed and developed with this approach in mind. This also means that the framework needs to be flexible enough to enable this approach. Second, this approach puts a lot of dependency on the framework, which means that as and when the framework pushes updates, refactoring might be required in the application. This can lead to additional maintenance overheads and can make it harder to keep up with the latest framework updates. Finally, this approach increases the complexity of the system, which can impact the performance of the application and can make it harder to debug issues. Overall, while deploying JavaScript runtime packages to a CDN for consumption is a possible approach for micro-frontends, it is important to carefully consider its trade-offs and drawbacks before adopting this approach.
  3. Third approach is Module Federation. In the below section I will talk about what is Module Federation, How it works, How to Implement.

Module Federation

Module Federation is a feature in Webpack 5 that allows you to share code between separate applications, or micro-frontends, without the need for a monolithic architecture.

In simpler terms, imagine you have two separate applications, App A and App B, and you want to share some code between them, such as a component or a utility function. With Module Federation, you can define which parts of each application can be shared and then use them in the other application.

This means as mentioned in Microfrontend overview earlier you can create smaller, more focused applications that can be developed independently, but still share common functionality. It can also help with code reusability and make it easier to maintain a consistent design across multiple applications.

In Module federation applications there are mainly 4 entities:

  1. Host or Container => The currently running application that is hosting federated remote modules.
  2. Remote => A reference to an external federated module.
  3. Expose => A module that is exported from an application is “exposed” for consumption.
  4. Shared => A module that is shared across different remotes.

Overall, Module Federation is a powerful feature that helps you break down large monolithic applications into smaller, more manageable pieces, without sacrificing the benefits of code sharing and reuse. From below figures I guess you can get an good idea.

Implementation

Now let’s go to our implementation of Module Federation example.

Go to this github link and there you can find a full working repository which I used Module Federation for this article.

https://github.com/senuravihanjayadeva/ModuleFederation

Here we have 3 react applications called shell app, app1 and app2. In app1 we have some components like Navbar, CardList. In app2 we have Footer component. Then shell app it consumes those components in app1 and app2. As you can see below we have the working shell app which consist of Navbar and CardList from app1 and Footer form app2.

Here I will explain important facts you should aware when configuring module federation in your webpack configuration.

Below you can see the Navbar and CardList components in app1.

Okay now I will come to the important part which is our webpack configuration.

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
entry: './src/index',
mode: 'development',
devServer: {
static: path.join(__dirname, 'dist'),
port: 3001,
},
output: {
publicPath: 'auto',
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
// To learn more about the usage of this plugin, please visit https://webpack.js.org/plugins/module-federation-plugin/
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Navbar': './src/Components/Navbar',
'./CardList': './src/Components/CardList',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};

This code snippet is configuring a ModuleFederationPlugin instance for webpack, which specifies the settings for a remote module, which is intended to be loaded by another application.

Let’s break down the configuration options:

  1. name: This option sets the name of the remote application. In this case, it's set to 'app1', meaning that this application will be named 'app1'.
  2. filename: This option specifies the name of the remoteEntry file that will be generated. This file is a JSON file that lists all of the modules that the remote application is exposing. In this case, it's set to 'remoteEntry.js', which means that the generated file will be named remoteEntry.js.
  3. exposes: This option is an object that lists all of the modules that the remote application is exposing to other applications. The keys in this object represent the module names that other applications can use to access the modules, and the values are the paths to the modules within the remote application. In this case, the remote application is exposing 2 modules: In this case './Navbar', and './CardList' which are the Navbar and CardList components from the files located at ./src/Components/Navbar and ./src/Components/CardListwill be exposed.
  4. shared: This option is an object that lists all of the modules that the remote application will share with other applications. The keys in this object are the names of the shared modules, and the values are objects that can contain various configuration options. In this case, the shared modules are 'react' and 'react-dom'. The configuration option 'singleton' is set to true for both of these modules, which means that only one instance of each module will be shared among all consuming applications.

Now let’s go to our app2. In app2 we have Footer component.

And Here is the webpack configuration for app2.

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
entry: './src/index',
mode: 'development',
devServer: {
static: path.join(__dirname, 'dist'),
port: 3002,
},
output: {
publicPath: 'auto',
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
// To learn more about the usage of this plugin, please visit https://webpack.js.org/plugins/module-federation-plugin/
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js',
exposes: {
'./Footer': './src/Components/Footer',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};

As in app1 here also it specifies the settings for a remote module, which is intended to be loaded by another application.

The plugin’s name property specifies the unique name of the remote module, which in this case is app2. The filename property specifies the name of the output file that will contain the manifest and code for this remote module. The exposes property defines what parts of the remote module should be exposed to other applications. In this case, the Footer component from the file located at ./src/Components/Footer will be exposed.

In here also the shared property is used to indicate which dependencies should be shared between the remote module and the host application. The react and react-dom libraries are marked as shared and will be loaded only once by the host application and shared by all remote modules. The singleton flag is set to true, which means that these libraries will be loaded only once and shared across all modules that depend on them.

Overall, this configuration object sets up a remote module named app2, which exposes its Footer component and shares the react and react-dom libraries with the host application.

Right, now let’s come to the Shell app in our microfrontend project which is the main application that loads and used components from app1 and app2.

const HtmlWebpackPlugin = require("html-webpack-plugin");
const {ModuleFederationPlugin} = require("webpack").container;
const ExternalTemplateRemotesPlugin = require("external-remotes-plugin");
const path = require("path");

module.exports = {
entry: "./src/index",
mode: "development",
devServer: {
static: path.join(__dirname, "dist"),
port: 3000,
},
output: {
publicPath: "auto",
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/,
options: {
presets: ["@babel/preset-react"],
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: "shell",
remotes: {
app1: "app1@http://localhost:3001/remoteEntry.js",
app2: "app2@http://localhost:3002/remoteEntry.js",
},
shared: {react: {singleton: true}, "react-dom": {singleton: true}},
}),
new ExternalTemplateRemotesPlugin(),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
};

In the configuration you provided, remotes is an object that maps the names of remote applications to their URLs. The names (app1 and app2 in this case) are arbitrary and can be any string. The URLs point to the remoteEntry.js files of the remote applications. These files contain information about the modules that the remote application wants to expose to other applications.

When the ModuleFederationPlugin is used in an application, it creates a "container" that is responsible for loading the remote applications and making their modules available to the local application. The container works by fetching the remoteEntry.js files from the specified URLs and parsing them to determine which modules are available.

Once the remote modules are loaded, they can be used in the local application just like any other module. For example, if the app1 remote application exposes a module called MyComponent, it can be imported in the local application like this:

import MyComponent from 'app1/MyComponent';

Below you can see how I imported and used components from app1 and app2 in our shell app.

Facts:

You do not need to have webpack configured to use components exposed from module federation. Module Federation allows you to share components between different webpack projects without requiring a specific configuration of webpack for every project. Instead, it relies on a standardized way of loading remote components at runtime using dynamic imports and the import() function. Consumer application can be either Single Page Application or not.

import React, { useState, useEffect } from 'react';

function App() {
const [Navbar, setNavbar] = useState(null);

useEffect(() => {
async function loadNavbar() {
const { Navbar } = await import('http://remote-app.com/navbar');
setNavbar(Navbar);
}
loadNavbar();
}, []);

return (
<div>
{Navbar && <Navbar />}
</div>
);
}

export default App;

When a remote component is requested, the host application will download the remote component’s code and run it in its own context. This allows you to use components from different projects without needing to configure your webpack setup for every project.

I guess now you may have an some idea about module federation and how to use in real applications. If you want to learn more about module federation and Microfrontend Architecture you can refer the following documentations. Thank you.

https://micro-frontends.org

https://webpack.js.org/concepts/module-federation/

--

--

Senura Vihan Jayadeva
Senura Vihan Jayadeva

Written by Senura Vihan Jayadeva

Software Engineering undergraduate of Sri Lanka Institute of Information Technology | Physical Science Undergraduate of University of Sri Jayewardenepura

Responses (1)