Microfrontend with Module Federation

Introduction

In a previous article, I provided guidance on using Vite to set up a React project integrated with Micro Frontend. However, you may have noticed that during development, you need to rebuild the static files for the exposed components in the Remote App before they can be used in the Shell App. To address this issue and improve development performance, in this article, I will guide you on how to use Module Federation to implement Micro Frontend. First, let’s explore some concepts about Module Federation.

What is Module Federation?

Module Federation is an architectural pattern for decentralizing JavaScript applications (similar to microservices on the server-side). It allows you to share code and resources among multiple JavaScript applications (or micro-frontends). This can help you:

  • Reduce code duplication
  • Improve code maintainability
  • Lower the overall size of your applications
  • Enhance the performance of your applications

What is Module Federation 2.0?

Module Federation 2.0 differs from the original Module Federation built into Webpack 5 by not only providing the core features of module export, loading, and dependency sharing but also additional features like dynamic type hinting, Manifest, Federation Runtime, and Runtime Plugin System. These features make Module Federation more suitable for use as a micro-frontend architecture in large-scale web applications.

Prerequisites


Implementation Steps

Similar to the previous article, to set up a Micro Frontend, you will need a Host app and a Remote app. Use the following command to create two corresponding projects: `shell-app` and `remote-app`.

yarn create rsbuild

Rspack

  • Rspack is a high-performance web construction tool based on Rust, with interoperability with the webpack ecosystem. It can be integrated into webpack projects at a low cost and offers better build performance.
  • Compared to webpack, Rspack has significantly improved build performance, thanks to the language advantages brought by Rust, as well as its parallel architecture and incremental compilation features. Benchmark tests have shown that Rspack can bring a 5 to 10 times increase in compilation performance.

Rsbuild

  • Rsbuild is a web construction tool based on Rspack, with the following features:Rsbuild is an enhanced version of the Rspack CLI, more user-friendly and ready out of the box.
  • Rsbuild represents the Rspack team's exploration and implementation of best practices for web construction.
  • Rsbuild is the best solution for migrating Webpack applications to Rspack, reducing configuration by 90% and speeding up builds by 10 times.

Then add the following package to each project

yarn add -D @module-federation/enhanced

This is the core package of Module Federation, serving as a Webpack build plugin, Rspack build plugin, and Runtime entry dependency.


The next step is to create a file named bootstrap.tsx and copy the content from App.tsx into it. After that, modify the content of the index.tsx file as follows (perform this for both projects):

import('./bootstrap');


The next step in your remote-app project is to create a file named Button.tsx with the following content:

export default function Button() {
return (
<button
style={{
padding: 10,
width: 100,
borderRadius: 5,
backgroundColor: 'green',
color: '#fff',
}}
>
Button name
</button>
);
}


Update the App.tsx file to use the Button

import './App.css';
import Button from './Button';

const App = () => {
return (
<div className="content">
<h1>Remote app</h1>
<div style={{ textAlign: 'center' }}>
<Button />
</div>
</div>
);
};

export default App;


Update the config of the file `rsbuild.config.ts`

import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack";

export default defineConfig({
plugins: [pluginReact()],
server: {
port: 3000,
},
dev: {
assetPrefix: true,
},
tools: {
rspack: {
output: {
uniqueName: "federation_provider",
},
plugins: [
new ModuleFederationPlugin({
name: "federation_provider",
exposes: {
"./button": "./src/Button.tsx",
},
shared: ["react", "react-dom"],
}),
],
},
},
});

You can see that the configuration is similar to when using Vite. It also requires a unique name, exposes the Button component for use in the Shell app, and defines shared modules as react and react-dom.


Let's start the remote-app and take a look

yarn dev


Next, in the shell-app project, modify the `tsconfig.json` file as follows to create a module alias for the remote modules:

{
"compilerOptions": {
"paths":{
"*": ["./@mf-types/*"]
}
}
}

This way, Module Federation will auto-generate the corresponding type file (.d.ts) for the remote module to use in the TypeScript project.


Update the file `rsbuild.config.ts` as follows

import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack";

export default defineConfig({
plugins: [pluginReact()],
server: {
port: 2000,
},
tools: {
rspack: {
plugins: [
new ModuleFederationPlugin({
name: "federation_consumer",
remotes: {
federation_provider: "federation_provider@http://localhost:3000/mf-manifest.json",
},
shared: ["react", "react-dom"],
}),
],
},
},
});

The configuration is quite similar to `remote-app`, with the addition of the `remotes` field used to define the remote module. You should replace it with the correct name of the remote module and the port defined in `remote-app`.


Modify `App.tsx` to use the remote module Button as follows:

import "./App.css";
import Button from "federation_provider/button";

const App = () => {
return (
<div className="content">
<h1>Shell app</h1>
<div style={{ textAlign: "center" }}>
<Button />
</div>
</div>
);
};

export default App;



The remote module will be loaded when used in the shell-app


The result is that when you change the content of the Button.tsx file in remote-app, you only need to reload to see the corresponding update in shell-app.

Dynamic import

Dynamic import is an essential feature in any large web application, and when using Micro Frontend with Module Federation, there are two ways to implement it as follows:

1. Using `import`

First, create the file `function1.ts` in the remote app as follows:

export const fn1 = () => {
console.log('fn1');
};


Next, update the `rsbuild.config.ts` file to expose this module. 

export default defineConfig({
...
plugins: [
new ModuleFederationPlugin({
name: 'federation_provider',
exposes: {
'./button': './src/Button.tsx',
'./function1': './src/function1.ts', // add here
},
shared: ['react', 'react-dom'],
}),
],
})


Then, to use it in the shell-app within the App.tsx file, you can implement it as follows:

const handle1 = () => {
import("federation_provider/function1").then((module) => {
module.fn1();
});
};


2. Federation runtime

Using this method, there's no need to define which remote modules to use in the `rsbuild.config.ts`. Instead, the initialization and loading of remote modules can be handled flexibly according to custom logic.

First, create an additional project named `function` (similarly to how the `remote-app` project was created, as I explained earlier, including creating the `bootstrap.tsx` file, etc.).

Next, create the `function2.ts` file.

export const fn2 = () => {
console.log('fn2');
};


Then update the `rsbuild.config.ts` file to expose the module as follows: 

import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';

export default defineConfig({
plugins: [pluginReact()],
server: {
port: 4002,
},
dev: {
assetPrefix: true,
},
tools: {
rspack: {
output: {
uniqueName: 'fn',
},
plugins: [
new ModuleFederationPlugin({
name: 'fn',
exposes: {
'./function2': './src/function2.ts',
},
}),
],
},
},
});


Finally, update the App.tsx file in the shell-app to use remote modules.

import { init, loadRemote } from "@module-federation/enhanced/runtime";
import "./App.css";
import Button from "federation_provider/button";

const App = () => {
const handle1 = () => {
import("federation_provider/function1").then((module) => {
module.fn1();
});
};

const handle2 = () => {
init({
name: "fn",
remotes: [
{
name: "fn",
entry: "http://localhost:4002/mf-manifest.json",
},
],
});
loadRemote<{ fn2: () => void }>("fn/function2").then((md) => {
md?.fn2();
});
};

return (
<div className="content">
<h1>Shell app</h1>
<div style={{ textAlign: "center" }}>
<Button />
<div>
<button onClick={handle1}>function 1</button>
</div>
<div>
<button onClick={handle2}>function 2</button>
</div>
</div>
</div>
);
};

export default App;

You can see that method 2 will use the `init` function to register the remote module (this way, you no longer need to define remotes in the `rsbuild.config.ts` file), and then use the `loadRemote` function to load the required module.


As a result, the corresponding module will only be loaded to execute the function when you click on a button.




If you have any suggestions or questions regarding the content of the article, please don't hesitate to leave a comment below!

Comments

Popular posts from this blog

Kubernetes Practice Series

NodeJS Practice Series

Deploying a NodeJS Server on Google Kubernetes Engine

Docker Practice Series

Setting up Kubernetes Dashboard with Kind

React Practice Series

Sitemap

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Using Terraform to Create VM Instances and Connect via SSH