A Complete Guide to Module Federation with Rspack: Building Modular and Scalable Web Applications
Introduction
The modern web is constantly evolving, demanding faster loading times, improved performance, and enhanced user experiences. Traditional monolithic frontend architectures are often unable to keep pace with these demands. Enter Module Federation, a groundbreaking feature introduced in Webpack 5, that enables building modular and scalable web applications by allowing separate applications to share code and resources.
However, the traditional Webpack often struggles with performance issues when dealing with large projects. Rspack emerges as a powerful alternative, leveraging the Rust programming language for speed and efficiency, offering a compelling solution for developers seeking a faster and more robust build system.
This article delves into the world of Module Federation, exploring its capabilities and demonstrating how to leverage it effectively with Rspack for building truly modular and scalable web applications.
Understanding Module Federation
Module Federation is a mechanism that allows breaking down a large application into smaller, independent modules or micro-frontends. These modules can be developed, deployed, and updated independently, resulting in a highly flexible and maintainable architecture.
Key Concepts:
-
Remote Containers: These are modules exposed by a separate application or build. They can be consumed by other applications through the
import()
function. - Shared Containers: These containers hold code that needs to be shared between multiple applications. They provide a single source of truth for shared dependencies, reducing redundancy and ensuring consistency across the application landscape.
-
Exposed Modules: These are modules or components specifically designed to be accessed from other applications. They are defined within a
remoteEntry.js
file and can be imported by other applications using the@scope/name
convention. - Consumed Modules: These modules are imported from other applications. They leverage the exposed modules of remote containers, enabling code sharing and collaboration.
The Power of Rspack
Rspack, built with Rust, excels at handling large, complex projects with its unparalleled performance and efficiency. Here's why it's a perfect match for Module Federation:
- Speed: Rspack offers significantly faster build times compared to Webpack, especially for large applications and projects with extensive dependencies.
- Parallelism: Rspack leverages the multi-core capabilities of modern CPUs, enabling parallel processing and further accelerating the build process.
- Stability: The Rust programming language ensures a high level of stability and reliability, making Rspack a robust choice for critical production environments.
Setting up the Development Environment
Before diving into code examples, let's set up the necessary environment. This example will utilize npm
as the package manager.
1. Install Node.js and Rspack:
Ensure you have Node.js installed on your system. You can download the latest version from the official website: https://nodejs.org/
Install Rspack globally:
npm install -g rspack
2. Project Structure:
We will create a simple project structure with two applications, app1
and app2
. Each application will have its own package.json
file and a src
directory containing the source code.
my-app
├── app1
│ └── src
│ └── index.js
└── app2
└── src
└── index.js
Building a Simple Module Federation Example
1. app1
- The Remote Application:
-
package.json
:
{
"name": "app1",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"start": "rspack serve --config rspack.config.js",
"build": "rspack build --config rspack.config.js"
},
"author": "",
"license": "MIT",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
-
rspack.config.js
:
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
},
devServer: {
port: 3000,
},
plugins: [
{
apply(compiler) {
compiler.hooks.afterResolvers.tap("ModuleFederationPlugin", (compiler) => {
compiler.options.output.publicPath = "http://localhost:3000/";
const ModuleFederationPlugin = require("rspack/lib/plugins/ModuleFederationPlugin");
compiler.options.plugins.push(
new ModuleFederationPlugin({
name: "app1",
filename: "remoteEntry.js",
exposes: {
"./Counter": "./src/Counter",
},
shared: {
react: {
singleton: true,
requiredVersion: false,
},
"react-dom": {
singleton: true,
requiredVersion: false,
},
},
}),
);
});
},
},
],
};
-
src/Counter.js
:
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<h1>
Counter: {count}
</h1>
<button =="" onclick="{()">
setCount(count + 1)}>Increment
</button>
</div>
);
};
export default Counter;
-
src/index.js
:
import React from 'react';
import ReactDOM from 'react-dom/client';
import Counter from './Counter';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<counter>
</counter>
);
2. app2
- The Consuming Application:
-
package.json
:
{
"name": "app2",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"start": "rspack serve --config rspack.config.js",
"build": "rspack build --config rspack.config.js"
},
"author": "",
"license": "MIT",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
-
rspack.config.js
:
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
},
devServer: {
port: 3001,
},
plugins: [
{
apply(compiler) {
compiler.hooks.afterResolvers.tap("ModuleFederationPlugin", (compiler) => {
compiler.options.output.publicPath = "http://localhost:3001/";
const ModuleFederationPlugin = require("rspack/lib/plugins/ModuleFederationPlugin");
compiler.options.plugins.push(
new ModuleFederationPlugin({
name: "app2",
filename: "remoteEntry.js",
remotes: {
app1: "app1@http://localhost:3000/remoteEntry.js",
},
shared: {
react: {
singleton: true,
requiredVersion: false,
},
"react-dom": {
singleton: true,
requiredVersion: false,
},
},
}),
);
});
},
},
],
};
-
src/index.js
:
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
const App = () => {
const [Counter, setCounter] = React.useState(null);
React.useEffect(() => {
const loadCounter = async () => {
const { default: CounterComponent } = await import("app1/Counter");
setCounter(CounterComponent);
};
loadCounter();
}, []);
return (
<div>
<h1>
App 2
</h1>
{Counter &&
<counter>
</counter>
}
</div>
);
};
root.render(
<app>
</app>
);
Explanation of the Code
-
app1
(Remote Application):-
rspack.config.js
: This configures the Module Federation plugin forapp1
.-
name
: Defines the name of the remote application, which will be used by consuming applications to reference it. -
filename
: Specifies the name of theremoteEntry.js
file containing the exposed modules. -
exposes
: Lists the modules exposed by the remote application. In this case, we exposeCounter
from./src/Counter.js
. -
shared
: Defines dependencies that need to be shared betweenapp1
and consuming applications. This ensures consistency and avoids duplicate installations.
-
-
-
app2
(Consuming Application):-
rspack.config.js
: Configures Module Federation forapp2
.-
remotes
: Specifies the remote applications to be consumed. The key is the name of the remote application (app1
), and the value is the URL of itsremoteEntry.js
file. -
shared
: Defines shared dependencies to ensure consistency withapp1
.
-
-
-
src/index.js
(Consuming Application):-
Import the remote module dynamically: The
import()
function dynamically loads theCounter
component fromapp1
at runtime. -
Display the imported component: Once the remote module is loaded, the
Counter
component is rendered withinapp2
.
-
Import the remote module dynamically: The
Running the Example
-
Start
app1
:
cd my-app/app1
npm run start
-
Start
app2
:
cd my-app/app2
npm run start
Now, you should have two applications running on different ports. Navigate to http://localhost:3001/
in your browser to see app2
, which will display the counter component from app1
.
Building and Deploying Applications
Once the development is complete, you can build the applications for deployment using the npm run build
command within each application directory. This will generate the optimized bundles for production.
Advanced Concepts and Best Practices
- Micro-Frontends: Module Federation excels at building complex applications composed of multiple independent micro-frontends, each with its own development team and deployment schedule.
- Code Sharing and Collaboration: Shared dependencies and the ability to import modules from other applications promote code sharing and collaboration between development teams.
- Lazy Loading and Performance: By dynamically loading remote modules only when needed, Module Federation can significantly improve the initial loading time and performance of your application.
-
Dynamic Remotes: The
remotes
property in the Module Federation plugin can be dynamic, allowing you to load different remote applications based on user roles, preferences, or other conditions. - Webpack Compatibility: Although primarily associated with Rspack, Module Federation is also fully supported by Webpack 5, providing developers with flexibility in their build tools.
Conclusion
Module Federation with Rspack is a powerful combination that unlocks a new level of modularity, scalability, and performance for building modern web applications. By leveraging its capabilities, you can create applications composed of independent modules, promote code sharing, and achieve optimal performance through lazy loading.
This guide provides a solid foundation for exploring Module Federation and Rspack. As you delve deeper, consider the advanced concepts and best practices outlined above to maximize the benefits of this powerful technology.
Remember that this example is a simple illustration of Module Federation's capabilities. With a bit of creativity and a deep understanding of the concepts, you can unlock its true potential and build truly exceptional web applications!