Although Webpack 5 has been announced for a long time, the official version has not been released yet. In addition to the general performance optimization and compilation speed, one of the most anticipated features of Webpack 5’s ChangeLog is Module Federation. Module Federation can be forcibly translated as “Module Federation”, but it sounds weird. I also asked this question in one of the front groups and received a variety of responses. Therefore, this article will use Module Federation directly, which sounds more comfortable without translation.

What is Module Federation?

Module Federation is mainly used to solve the problem of code sharing between multiple applications. It allows us to implement code sharing across applications more elegantly. Suppose we now have two projects A and B. Project A has A wheel map component inside and Project B has A news list component inside.

Now there is A requirement to migrate the news list from project B to Project A, and to ensure that the style of the news list on both sides remains the same during subsequent iterations. There are two things you can do:

  1. Make A complete copy of the code of project B to project A by using CV method;
  2. The news component is independent, published to the internal NPM, through the NPM load components;

The CV approach is definitely faster than standalone components, because you don’t need to separate the component code from project B and release the NPM. However, the disadvantage of the CV method is that the code cannot be synchronized in time. If another of your colleagues modifies the news component of project B after you copy the code, the news component of project A and project B will be inconsistent.

At this point, if both of your projects happen to be using Webpack 5, you should be happy because you can use the news component of Project B directly in Project A for nothing, with just A few lines of configuration. Not only that, you can also use project A’s caroute diagram component in Project B. In other words, code sharing via Module Federation is a two-way process, which sounds like “No more learning!” .

The Module Federation practice

Let’s look at the code for project A/B.

The directory structure for project A is as follows:

├ ─ ─ public │ └ ─ ─ index. The HTML ├ ─ ─ the SRC │ ├ ─ ─ index. The js │ ├ ─ ─ the bootstrap, js │ ├ ─ ─ App. Js │ └ ─ ─ Slides TAB. Js ├ ─ ─ package. The json └ ─ ─ webpack.config.jsCopy the code

The directory structure for project B is as follows:

├ ─ ─ public │ └ ─ ─ index. The HTML ├ ─ ─ the SRC │ ├ ─ ─ index. The js │ ├ ─ ─ the bootstrap, js │ ├ ─ ─ App. Js │ └ ─ ─ NewsList. Js ├ ─ ─ package. The json └ ─ ─ webpack. Config. JsCopy the code

The difference between project A and project B mainly lies in the difference in the components imported from app.js. The index.js and bootstrap.js of the two items are the same.

// index.js
import("./bootstrap");

// bootstrap.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />.document.getElementById("root"));
Copy the code

App.js of project A:

import React from "react";
import Slides from './Slides';

const App = () = > (
  <div>
    <h2 style={{ textAlign: 'center' }}>App1, Local Slides</h2>
    <Slides />
  </div>
);

export default App;
Copy the code

App.js of project B:

import React from "react";
import NewsList from './NewsList';
const RemoteSlides = React.lazy(() = > import("app1/Slides"));

const App = () = > (
  <div>
    <h2 style={{ textAlign: 'center' }}>App 2, Local NewsList</h2>
    <NewsList />
  </div>
);

export default App;
Copy the code

Now let’s look at the WebPack configuration before adding Module Federation:

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development".// Import file
  entry: "./src/index".// Develop the service configuration
  devServer: {
    // Port A is 3001, and port B is 3002
    port: 3001.contentBase: path.join(__dirname, "dist"),},output: {
    // Port A is 3001, and port B is 3002
    publicPath: "http://localhost:3001/",},module: {
    // Use babel-loader to escape
    rules: [{test: /\.jsx? $/,
        loader: "babel-loader".exclude: /node_modules/,
        options: {
          presets: ["@babel/preset-react"],},},],},plugins: [
    / / deal with HTML
    new HtmlWebpackPlugin({
      template: "./public/index.html",})]};Copy the code

Configuration: exposes/remotes

Now let’s modify the WebPack configuration to introduce Module Federation so that project A introduces the news component of project B.

// WebPack configuration for project B
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // Provide files for other services to load
      filename: "remoteEntry.js".// A unique ID that identifies the current service
      name: "app2".// The module that needs to be exposed is introduced with '${name}/${expose}' when used
      exposes: {
        "./NewsList": "./src/NewsList",}})]};// WebPack configuration for project A
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app1".// Reference app2's services
      remotes: {
        app2: "app2@http://localhost:3002/remoteEntry.js",}})]};Copy the code

We focus on reception /remotes:

  • providesexposesThe option indicates that the current application is aRemote.exposesThe inside module can be replaced by the otherHostThe reference mode isimport(${name}/${expose}).
  • providesremotesThe option indicates that the current application is aHost, can be quotedremoteexposeThe module.

Then modify the app.js of project A:

import React from "react";
import Slides from './Slides';
// Import the news component for project B
const RemoteNewsList = React.lazy(() = > import("app2/NewsList"));

const App = () = > (
  <div>
    <h2 style={{ textAlign: 'center' }}>App1, Local Slides, Remote NewsList</h2>
    <Slides />
    <React.Suspense fallback="Loading Slides">
      <RemoteNewsList />
    </React.Suspense>
  </div>
);

export default App;
Copy the code

At this point, project A has successfully connected to project B’s news component. We’ll look at A web request, projects A configuration app2: “app2 @ http://localhost:3002/remoteEntry.js” remote, will first request project B remoteEntry. Js files as an entrance. When we import the news component for project B, we get the src_newslist_js.js file for project B.

Configuration: Shared

In addition to the configuration related to module introduction and module exposure mentioned earlier, there is also a shared configuration, which is used to avoid multiple common dependencies in a project.

For example, our current project, PROJECT A, has introduced A react/react-dom, and project B exposes A news-list component that also relies on react/react-dom. If this problem is not resolved, project A will load two React libraries. This reminds me of a project of the company when I just started my career. Due to the way of PHP template stitching, different departments introduced a jQuery in their templates, resulting in the introduction of three different versions of jQuery in the project, which especially affected the page performance.

Therefore, when using Module Federation, we should remember to configure common dependencies into shared. In addition, you must configure shared for both projects, otherwise an error will be reported.

Project A directly uses the react/react-dom of project B.

Two-way sharing

As mentioned earlier, Module Federation sharing can be two-way. Next, we will also configure project A as A Remote to expose project B’s multicast map components.

// WebPack configuration for project B
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app2".filename: "remoteEntry.js".// Expose the news list component
      exposes: {
        "./NewsList": "./src/NewsList",},// Reference app1's services
      remotes: {
        app1: "app1@http://localhost:3001/remoteEntry.js",},shared: {
        react: { singleton: true },
        "react-dom": { singleton: true}}})]};// WebPack configuration for project A
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app1".filename: "remoteEntry.js".// Expose the wheel map component
      exposes: {
        "./Slides": "./src/Slides",},// Reference app2's services
      remotes: {
        app2: "app2@http://localhost:3002/remoteEntry.js",},shared: {
        react: { singleton: true },
        "react-dom": { singleton: true}},})]};Copy the code

Using the caroute diagram component in Project B:

// App.js
import React from "react";
import NewsList from './NewsList';
+const RemoteSlides = React.lazy(() => import("app1/Slides"));

const App = () => (
  <div>
- 

App 2, Local NewsList

+

App 2, Remote Slides, Local NewsList

+ + + <NewsList /> </div> ); export default App; Copy the code

Introduce multiple dependencies at the same time

Module Federation also supports multiple Remote items at once. We can create A new project C and introduce both the cast map component of Project A and the news list component of project B.

// WebPack configuration for project C
// Other configurations are basically the same as the previous project, except that port 3003 needs to be changed
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app3".// Depend on both project A and project B
      remotes: {
        app1: "app1@http://localhost:3001/remoteEntry.js".app2: "app2@http://localhost:3002/remoteEntry.js",},shared: {
        react: { singleton: true },
        "react-dom": { singleton: true}}})]};Copy the code

Access components:

import React from "react";
const RemoteSlides = React.lazy(() = > import("app1/Slides"));
const RemoteNewsList = React.lazy(() = > import("app2/NewsList"));

const App = () = > (
  <div>
    <h2 style={{ textAlign: 'center' }}>App 3, Remote Slides, Remote Remote</h2>
    <React.Suspense fallback="Loading Slides">
      <RemoteSlides />
      <RemoteNewsList />
    </React.Suspense>
  </div>
);

export default App;
Copy the code

Load the logic

One thing to note here is that the entry file index.js does not have any logic. Instead, it puts the logic in bootstrap.js, and index.js loads bootstrap.js dynamically.

// index.js
import("./bootstrap");

// bootstrap.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />.document.getElementById("root"));
Copy the code

Is it possible to delete bootstrap.js and put the logic directly into index.js? When tested, it did not work.

The main reason is that the JS files exposed by remote need to be loaded first. If bootstrap.js is not an asynchronous logic, when importing NewsList, it will rely on remote. App2’s remote.js is not loaded at all, so there will be problems.

As can be seen from the Network panel, remote.js is loaded before bootstrap.js, so our bootstrap.js must be asynchronous logic.

The loading logic of project A is as follows:

The main loading. Js

Main.js contains some runtime logic for Webpack, as well as remote request and bootstrap request.

Loading remote. Js

Main.js preferentially loads the remote.js of project B, which exposes the internal components configured in the exposes for external use.

The bootstrap loading. Js

Main.js loads its own main logic, bootstrap.js, which uses app2’s news list component.

The news component is loaded internally using __webpack_require__.e, which is defined in main.js.

/* webpack/runtime/ensure chunk */
(() = > {
  __webpack_require__.f = {};
  __webpack_require__.e = (chunkId) = > {
    // __webpack_require__.e is looked up in __webpack_require__.f via the passed chunkId
    return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) = > {
      __webpack_require__.f[key](chunkId, promises);
      return promises;
    }, []));
  };
})();
Copy the code

__webpack_require__.f has three parts:

__webpack_require__.f.remotes = (chunkId, promises) = > {}  // webpack/runtime/remotes
__webpack_require__.f.consumes = (chunkId, promises) = > {} // webpack/runtime/consumes
__webpack_require__.f.j = (chunkId, promises) = > {}        // webpack/runtime/jsonp
Copy the code

We’ll just look at the logic of Remotes for now, because our news component is loaded in as remote.

	/* webpack/runtime/remotes loading */
	(() = > {
		var installedModules = {};
		var chunkMapping = {
			"webpack_container_remote_app2_NewsList": [
				"webpack/container/remote/app2/NewsList"]};var idToExternalAndNameMapping = {
			"webpack/container/remote/app2/NewsList": [
				"default"."./NewsList"."webpack/container/reference/app2"]}; __webpack_require__.f.remotes =(chunkId, promises) = > {
			// chunkId: webpack_container_remote_app2_NewsList
			chunkMapping[chunkId].forEach((id) = > {
				// id: webpack/container/remote/app2/NewsList
				var data = idToExternalAndNameMapping[id];
        // require("webpack/container/reference/app2")["./NewsList"]
				var promise = __webpack_require__(data[2])[data[1]].return promise;
			});
		}
	})();
Copy the code

As you can see, the last call way becomes the require (” webpack/container/reference/app2 “) (“. / NewsList “), and this module is loaded before the app2 remote. Js has been defined.

Src_newslist_js.js is loaded by remote.js.

conclusion

Module Federation, which comes with Webpack 5, is still powerful, especially when it comes to sharing code across multiple projects, but it has the fatal drawback of requiring all of your projects to be based on Webpack and upgraded to Webpack 5. I prefer vite to Module Federation, which takes advantage of the browser’s native modularity capabilities for code sharing.

The full code is available on Github, and if you want to see more examples of Module Federation, you can visit the official repository.