微前端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>
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 });
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 }]
]
}
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"
}
}
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);
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');
我们调整一下 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 },
},
}),
],
},
]);
...
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;
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>
);
}
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,
}),
],
});
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]
);
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 是的配置变得复杂,但是可以将模块做解耦,方便多团队协作。
关注微信公众号,获取最新推送~
加微信,深入交流~