Webpack 5 cross application code sharing – module Federation

Time:2021-7-26

Webpack 5 cross application code sharing - module Federation

Although the news of webpack 5 has been out for a long time, the official version has not been released yet. In the changelog of webpack 5, in addition to the conventional performance optimization and compilation speed-up, there is an expected functionModule FederationModule FederationIt can be forcibly translated into “module Federation”, but it sounds strange. I also threw this question in a front-end group. Unexpectedly, everyone’s replies are also diverse. Therefore, this paper directly usesModule FederationIt sounds more comfortable without translation.

Webpack 5 cross application code sharing - module Federation

What is module Federation?

Module FederationIt is mainly used to solve the problem of code sharing among multiple applications, which can make our more elegant implementation of cross application code sharing. Suppose we now have two projects a and B. project a has a rotation chart component and project B has a news list component.

Webpack 5 cross application code sharing - module Federation

Webpack 5 cross application code sharing - module Federation

Now there is a need to migrate the news list of project B to project a, and ensure that the news list styles on both sides are consistent in the subsequent iteration process. At this time, you have two ways:

  1. Using the CV method, copy a complete copy of the code of project B to project a;
  2. Independently publish the news component to the internal NPM, and load the component through NPM;

CV method is certainly faster than independent components. After all, there is no need to separate component code from project B and release NPM. However, the defect of the CV method is that the code cannot be synchronized in time. If another colleague modifies the news component of project B after you copy the code, the news components of project a and project B will be inconsistent.

At this time, if your two projects happen to use webpack 5, it should be a very happy thing, because you can directly use the news component of project B in project a without any cost and only need a few lines of configuration. Not only that, you can also use the carousel component of project a in project B. That is, throughModule FederationThe implementation of code sharing is two-way. It sounds like you really want people to say, “I can’t learn!”.

Module Federation practice

Let’s look at the code of project a / b.

The directory structure of project a is as follows:

├── public
│   └── index.html
├── src
│   ├── index.js
│   ├── bootstrap.js
│   ├── App.js
│   └── Slides.js
├── package.json
└── webpack.config.js

The directory structure of project B is as follows:

├── public
│   └── index.html
├── src
│   ├── index.js
│   ├── bootstrap.js
│   ├── App.js
│   └── NewsList.js
├── package.json
└── webpack.config.js

The difference between items a and B mainly lies in the import components in app.js. Both index.js and bootstrap.js 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"));

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;

App.js for 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;

Now let’s look at the connectionModule FederationPrevious webpack configuration:

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

module.exports = {
  mode: "development",
  //Entry file
  entry: "./src/index",
  //Development service configuration
  devServer: {
    //Port a of the project is 3001 and port B of the project is 3002
    port: 3001,
    contentBase: path.join(__dirname, "dist"),
  },
  output: {
    //Port a of the project is 3001 and port B of the project is 3002
    publicPath: "http://localhost:3001/",
  },
  module: {
    //Escape using Babel loader
    rules: [
      {
        test: /\.jsx?$/,
        loader: "babel-loader",
        exclude: /node_modules/,
        options: {
          presets: ["@babel/preset-react"],
        },
      },
    ],
  },
  plugins: [
    //Processing HTML
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
};

Configuration: exposures / remotes

Now, we modify the webpack configuration to introduceModule Federation, let project a introduce the news component of project B.

//Webpack configuration for project B
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      //Files provided for other services to load
      filename: "remoteEntry.js",
      //Unique ID to mark the current service
      name: "app2",
      //Modules that need to be exposed are introduced through ` ${name} / ${expose} '
      exposes: {
        "./NewsList": "./src/NewsList",
      }
    })
  ]
};

//Webpack configuration for project a
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app1",
      //Service referencing app2
      remotes: {
        app2: "[email protected]://localhost:3002/remoteEntry.js",
      }
    })
  ]
};

We focus onexposes/remotes

  • ProvidedexposesOption indicates that the current application is aRemoteexposesModules within can be used by othersHostReference byimport(${name}/${expose})
  • ProvidedremotesOption indicates that the current application is aHost, can be referencedremoteinexposeModule.

Then modify the app.js of project a:

import React from "react";
import Slides from './Slides';
//Introduce the news component of 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;

Webpack 5 cross application code sharing - module Federation

At this time, project a has successfully accessed the news component of project B. Let’s look at the network request of item A. item a is configuredapp2: "[email protected]://localhost:3002/remoteEntry.js"After the remote of project B, the remote of project B will be requested firstremoteEntry.jsFile as entry. When we import the news component of project B, we will get the information of project Bsrc_NewsList_js.jsFile.

Webpack 5 cross application code sharing - module Federation

Configuration: shared

In addition to the configuration related to module introduction and module exposure mentioned earlier, there is anothersharedConfiguration is mainly used to avoid multiple public dependencies.

For example, our current project a has introduced areact/react-domThe news list component exposed by project B also depends onreact/react-dom。 If this problem is not solved, project a will load tworeactLibrary. This reminds me that when I first started my career, a project of the company introduced three different versions of jQuery in its own templates due to the splicing of PHP templates, which particularly affected the page performance.

Therefore, when using module Federation, we must remember to configure public dependencies tosharedYes. In addition, two items must be configured at the same timesharedOtherwise, an error will be reported.

Next, open project a in the browser. In the network panel of chrome, you can see that project a directly uses project Breact/react-dom

Webpack 5 cross application code sharing - module Federation

Two way sharing

As mentioned earlier, the sharing of module federation can be bidirectional. Next, we will also configure project a as aRemote, expose the carousel component of item a to item B for use.

//Webpack configuration for project B
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app2",
      filename: "remoteEntry.js",
      //Expose news list component
      exposes: {
        "./NewsList": "./src/NewsList",
      },
      //Service referencing app1
      remotes: {
        app1: "[email protected]://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 carousel assembly
      exposes: {
        "./Slides": "./src/Slides",
      },
      //Service referencing app2
      remotes: {
        app2: "[email protected]://localhost:3002/remoteEntry.js",
      },
      shared: {
        react: { singleton: true },
        "react-dom": { singleton: true }
      },
    })
  ]
};

Use the carousel component in project B:

// App.js
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>
+   <h2 style={{ textAlign: 'center' }}>App 2, Remote Slides, Local NewsList</h2>
+   <React.Suspense fallback="Loading Slides">
+     <RemoteSlides />
+   </React.Suspense>
    <NewsList />
  </div>
);

export default App;

Webpack 5 cross application code sharing - module Federation

Introducing multiple dependencies at the same time

Module Federation also supports remote multiple projects at once. We can create a new project C and introduce the rotation chart component of project a and the news list component of project B at the same time.

//Webpack configuration for project C
//Other configurations are basically the same as those in the previous project, except that the port needs to be modified to 3003
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app3",
      //Relying on projects a and B at the same time
      remotes: {
        app1: "[email protected]://localhost:3001/remoteEntry.js",
        app2: "[email protected]://localhost:3002/remoteEntry.js",
      },
      shared: {
        react: { singleton: true },
        "react-dom": { singleton: true }
      }
    })
  ]
};

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;

Webpack 5 cross application code sharing - module Federation

Load logic

One thing to pay special attention to here is the entry fileindex.jsThere is no logic in itself, but put logic inbootstrap.jsIn,index.jsDe dynamic loadingbootstrap.js

// 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"));

If deletedbootstrap.js, put logic directly intoindex.jsIs it feasible? After testing, it is really not feasible.

Webpack 5 cross application code sharing - module Federation

The main reason is that the JS files exposed by remote need to be loaded first. Ifbootstrap.jsNot an asynchronous logic, inimport NewsListWill rely on app2 whenremote.js, if directly inmain.jsExecution, app2remote.jsThere is no load at all, so there will be a problem.

Webpack 5 cross application code sharing - module Federation

Webpack 5 cross application code sharing - module Federation

It can also be seen from the network panel,remote.jsBeforebootstrap.jsLoaded, so ourbootstrap.jsMust be an asynchronous logic.

Webpack 5 cross application code sharing - module Federation

The loading logic of item a is as follows:

Load main.js

main.jsIt mainly includes some runtime logic of webpack, remote request and bootstrap request.

Webpack 5 cross application code sharing - module Federation

Webpack 5 cross application code sharing - module Federation

Load remote.js

main.jsProject B will be loaded firstremote.js, the file will be exposedexposesThe internal components configured in are for external use.

Webpack 5 cross application code sharing - module Federation

Load bootstrap.js

main.jsLoad your own main logicbootstrap.jsbootstrap.jsWill use the news list component of app2.

Webpack 5 cross application code sharing - module Federation

Internal use__webpack_require__.eTo load the news component,__webpack_require__.estaymain.jsDefined in.

Webpack 5 cross application code sharing - module Federation

/* webpack/runtime/ensure chunk */
(() => {
  __webpack_require__.f = {};
  __webpack_require__.e = (chunkId) => {
    // __ webpack_ require__. E will pass in the chunkid__ webpack_ require__. Find in F
    return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
      __webpack_require__.f[key](chunkId, promises);
      return promises;
    }, []));
  };
})();

__webpack_require__.fThere are 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

For the time being, we only look at the logic of remotes, because our news component is loaded 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;
            });
        }
    })();

As you can see, the final call method will becomerequire("webpack/container/reference/app2")["./NewsList"], and this module is based on the previously loaded app2remote.jsAlready defined.

Webpack 5 cross application code sharing - module Federation

src_NewsList_js.jsLoading byremote.jslaunch.

Webpack 5 cross application code sharing - module Federation

summary

Provided by webpack 5Module FederationIt is still very powerful, especially for code sharing in multiple projects, which provides great convenience, but it has a fatal disadvantage. All your projects need to be based on webpack and have been upgraded to webpack 5. CompareModule Federation, I prefer the solution provided by vite, which uses the browser’s native modularity to share code.

You can visit my GitHub for the complete code. If you want to see more aboutModule FederationYou can access the official warehouse.

Webpack 5 cross application code sharing - module Federation