微前端open in new window 将微服务的开发理念带到了前端开发中。微前端将原来的单体应用拆分成多个子应用,每个子应用可以使用不同的技术栈开发,最终再拼合成一个整体的应用对外提供服务。

从 Webpack 5 开始,Webpack 通过 Module Federation 内置了对构建微前端应用的支持。

Module Federation 的例子

我们通过下面这个例子来演示,页面的结构如下:

<body>
  <h1>Demo</h1>
  <aside>
    <ul>
      <li><button>Hello world</button></li>
      <li><button>Hello federation</button></li>
      <li><button>Hello webpack</button></li>
    </ul>
  </aside>
  <main>内容随着点击按钮的不同而变化。</main>
</body>
1
2
3
4
5
6
7
8
9
10
11

添加 Webpack 配置

webpack.mf.js

const path = require('path');
const { mode } = require('webpack-nano/argv');
const { merge } = require('webpack-merge');
const parts = require('./webpack.parts');

const commonConfig = merge([
  {
    entry: [path.join(__dirname, 'src', 'mf.js')],
    output: { publicPath: '/' },
  },
  parts.loadJavaScript(),
  parts.loadImages(),
  parts.page(),
  parts.extractCSS({ loaders: [parts.tailwind()] }),
]);

const configs = {
  development: merge(
    { entry: ['webpack-plugin-serve/client'] },
    parts.devServer()
  ),
  production: {},
};

module.exports = merge(commonConfig, configs[mode], { mode });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

这个配置文件使用了一部分我们在前面章节中定义的配置。.babelrc 如下:

.balbelrc

{
  "presets": [
    "@babel/preset-react",
    ["@babel/preset-env", { "modules": false }]
  ]
}
1
2
3
4
5
6

package.json

{
  "scripts": {
    "build:mf": "wp --config webpack.mf.js --mode production",
    "start:mf": "wp --config webpack.mf.js --mode development"
  }
}
1
2
3
4
5
6

通过 React 来实现这个应用

为了避免直接操作 DOM,我们通过 React 来实现这个应用。

src/mf.js

import ReactDOM from 'react-dom';
import React from 'react';
import './main.css';

function App() {
  const options = ['Hello world', 'Hello fed', 'Hello webpack'];
  const [content, setContent] = React.useState('Changes on click.');

  return (
    <main className="max-w-md mx-auto space-y-8">
      <h1 className="text-xl">Demo</h1>
      <aside>
        <ul className="flex space-x-8">
          {options.map((option) => (
            <li key={option}>
              <button
                className="rounded bg-blue-500 text-white p-2"
                onClick={() => setContent(option)}
              >
                {option}
              </button>
            </li>
          ))}
        </ul>
      </aside>
      <article>{content}</article>
    </main>
  );
}

const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(<App />, container);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

我们通过前面删除多余样式 一章介绍的方法,使用 Tailwind 来美化一下页面。

此时执行 npm run start:mf,我们点击按钮,可以发现内容会随着按钮点击发生变化。

分离出引导程序(bootstrap)

我们现在将这个单体应用分割成多个模块。在实际实践中,这些模块可以被不同的团队使用不同的技术栈开发。

我们使用 Webpack 的 ModuleFederationPlugin 插件,同时异步加载各个模块。同时,因为我们需要动态加载不同的模块,因此需要使用一个运行时。

src/bootstrap.js

import('./mf');
1

我们调整一下 webpack 配置。


const { ModuleFederationPlugin } = require("webpack").container;

...

const commonConfig = merge([
  {
    // entry: [path.join(__dirname, "src", "mf.js")],
    entry: [path.join(__dirname, "src", "bootstrap.js")],
    output: { publicPath: "/" },
  },
  ...
  {
    plugins: [
      new ModuleFederationPlugin({
        name: "app",
        remotes: {},
        shared: {
          react: { singleton: true },
          "react-dom": { singleton: true },
        },
      }),
    ],
  },
]);

...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

此时执行 npm run start:mf,可以看到页面与之前的一样,没有任何变化。

如果我们不使用运行时,而是直接将 entry 指向原始模块,我们会在浏览器中看到 Uncaught Error: Shared module is not available for eager consumption 这样一条错误信息。

我们现在将 header 部分分离成单独的模块,然后通过使用 module federation 来加载这个模块。

注意上面配置文件中,singleton 的配置,这么做可以保证分离出来各个模块可以使用相同的 react 库。

分离出 Header 模块

src/header.js

import React from 'react';

const Header = () => <h1 className="text-xl">Demo</h1>;

export default Header;
1
2
3
4
5

我们现在修改应用代码,使用这个新的模块。同时,我们新加一个 mf 的命名空间,我们将会在 Module Federation 里来管理这个命名空间。

src/mf.js

...
import Header from "mf/header";

function App() {
  ...
  return (
    <main className="max-w-md mx-auto space-y-8">
      {/* <h1 className="text-xl">Demo</h1> */}
      <Header />
      ...
    </main>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13

现在添加 federation 配置,我们定义在 webpack.parts.js 中。

webpack.parts.js

const { ModuleFederationPlugin } = require('webpack').container;

exports.federateModule = ({ name, filename, exposes, remotes, shared }) => ({
  plugins: [
    new ModuleFederationPlugin({
      name,
      filename,
      exposes,
      remotes,
      shared,
    }),
  ],
});
1
2
3
4
5
6
7
8
9
10
11
12
13

因为我们要构建多个模块,为了代码复用,我们通过 --component 来区分不同的模块构建。

webpack.mf.js

// const { mode } = require("webpack-nano/argv");
// const { ModuleFederationPlugin } = require("webpack").container;

const { component, mode } = require("webpack-nano/argv");

const commonConfig = merge([
  {
//  entry: [path.join(__dirname, "src", "bootstrap.js")],
    output: { publicPath: "/" },
  }
  ...

  // parts.extractCSS({ loaders: [parts.tailwind()] }),
  // {
  //   plugins: [
  //     new ModuleFederationPlugin({
  //       name: "app",
  //       remotes: {},
  //       shared: {
  //         react: { singleton: true },
  //         "react-dom": { singleton: true },
  //       },
  //     }),
  //   ],
  // },
]);

const shared = {
  react: { singleton: true },
  "react-dom": { singleton: true },
};
const componentConfigs = {
  app: merge(
    {
      entry: [path.join(__dirname, "src", "bootstrap.js")],
    },
    parts.page(),
    parts.federateModule({
      name: "app",
      remotes: { mf: "mf@/mf.js" },
      shared,
    })
  ),
  header: merge(
    {
      entry: [path.join(__dirname, "src", "header.js")],
    },
    parts.federateModule({
      name: "mf",
      filename: "mf.js",
      exposes: { "./header": "./src/header" },
      shared,
    })
  ),
};

if (!component) throw new Error("Missing component name");

// module.exports = merge(commonConfig, configs[mode], { mode });

module.exports = merge(
  commonConfig,
  configs[mode],
  { mode },
  componentConfigs[component]
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

执行 npm run build:mf -- --component header 来构建 header 模块,执行 npm run start:mf -- --component app 来构建 app 模块。

说明

引入 Module Federation 以后,Webpack 配置变得复杂了。但是,我们把应用拆成了多个可以独立开发独立部署的模块。每个模块都可以使用不同的技术进行开发。

Module Federation 需要有一个运行时来加载不同的模块,我们需要花费一些精力还思考如何实现这个运行时。

总结

Webpack 5 中的 Module Federation 从基础设施层面为微前端开发提供了工具支持。

ModuleFederationPlugin 是 Module Federation 的具体技术实现。

如果我们要在项目中使用 Module Federation,我们需要将异步加载入口模块。

使用 Module Federation 是的配置变得复杂,但是可以将模块做解耦,方便多团队协作。

关注微信公众号,获取最新推送~