module federation

概述

前端的工程化已经持续了很长时间,从最开始的手动更新和维护,之后以文件为单位的自动化工具的产生,在开发和部署的过程中进行一系列的操作,最终组成一个较大的 SPA 或者 MPA。随着业务的发展,网页应用越来越多,以文件为单位进行构建则难以维护与复用。webpack5 引入了一个重要的特性 module federation,目的是解决多业务线,多工程下的模块复用问题。

使用方法

关于module federation的用法,官方给出了很详细的示例,我们以automatic-vendor-sharing为例简单来看一下是如何使用的:

线上 demo

app1

先看一下 app1 的 webpack 关键配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
remotes: {
app2: 'app2@http://localhost:3002/remoteEntry.js',
},
exposes: {
'./Button': './src/Button',
},
shared: {
...deps,
react: {
singleton: true,
},
'react-dom': {
singleton: true,
},
},
}),

这里的关键信息是ModuleFederationPlugin中声明了remotes,然后我们就可以使用了,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import LocalButton from './Button';
import React from 'react';

const RemoteButton = React.lazy(() => import('app2/Button'));

const App = () => (
<div>
<h1>Bi-Directional</h1>
<h2>App 1</h2>
<LocalButton />
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
</div>
);

export default App;

app2

我们回到 app1 再看一下 app1 的 webpack 配置,注意到 app1 配置了namefilename,以及exposes等信息,在开发和部署时会生成对应的文件,供其它 app 使用,我们来看一下 app2 的 webpack 关键配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
},
exposes: {
'./Button': './src/Button',
},
shared: {
...deps,
react: {
singleton: true,
},
'react-dom': {
singleton: true,
},
},
}),

app2 中也调用了 app1 的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import LocalButton from './Button';
import React from 'react';

const RemoteButton = React.lazy(() => import('app1/Button'));

const App = () => (
<div>
<h1>Bi-Directional</h1>
<h2>App 2</h2>
<LocalButton />
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
</div>
);

export default App;

如上示例所示,两个应用相互调用对方暴露出来的模块,那么扩展到多个应用也同样合适,这样就可以较好的解决模块复用问题

Chunk Graph

在无module federation的情况下,webpack 输出结果通常由三部分构成:

  • main chunk: 包含 runtime 以及 index,是整个应用的入口
  • vendor chunk: 包含诸 react,react-dom 等的三方依赖
  • async chunk: 以import()等形式引入的组件

如下所示:

引入module federation的情况下,webpack 输出结果变化如下:

可以观察到有些内容发生了改变:

  • remoteEntry chunk: 其它模块暴露出来可供直接使用的 chunk
  • shared chunk: 多个模块公共使用的 chunk,和无module federation构建下的 vendor chunk 类似
  • exposes chunk: 自身模块暴露出来的 chunk,可供其它模块使用

MFSU

既然我们可以使用module federation来加载和调用远程的内容,那么我们换个思路,先收集本地的相关依赖,然后利用ModuleFederationPlugin将本地的依赖提取出来,在后续的使用中直接调用,那么就可以跳过很多的编译环节,以加快开发启动速度和打包速度。umi 基于这个思路,实现了mfsu,即module federation speed up,整体上使启动速度得到质的提升。我们来看一下 umi v3.5.20 版本是怎么做的:

收集依赖

mfsu 采用 Babel 来进行 AST 的遍历和依赖收集,关于 Babel Plugin 是如何工作的可以参考之前的文章,主要逻辑在babel-plugin-import-to-await-require下,涉及到的细节比较多,主要步骤如下所示:

  • 在 Babel 的Program.exit()阶段开始遍历并收集依赖
  • importexport调用isMatchLib()判断哪些依赖需要收集,这里针对 umi,dumi,bigfish 等做了特殊处理,不需要收集,同时剔除了webpack externals配置和绝对路径等引入的内容。
  • 处理import相关语法
    • import xxx from 'yyy'形式的引入替换为const xxx = await import('yyy')形式
    • import * as xxx from 'yyy'形式的引入替换为const xxx = await import('yyy')形式
    • import { xxx } from 'yyy'形式的引入替换为const { xxx } = await import('yyy')形式
  • 处理export相关语法,如export { xxx } from 'yyy', export * from 'yyy'等形式,处理方法和import类似
  • isMatchLib()返回 true 时调用 callback onTransformDeps()最终调用 DepInfo 实例,将相关依赖收集到 cache 中,并生成MFSU_CACHE.json

依赖处理

获取到依赖之后,我们就可以调用ModuleFederationPlugin()生成对应的文件了,具体逻辑在DepBuilder中的这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
mfConfig.plugins.push(
//@ts-ignore
new webpack.container.ModuleFederationPlugin({
library: {
type: 'global',
name,
},
name,
filename: remoteEntryFilename,
exposes,
})
);

和上述示例中的导出模块配置基本一致,配置了相应的 filename 以及 exposes,生成的文件供后续加载使用

依赖加载

生成相应的导出文件之后,还是和示例一致,我们依然调用ModuleFederationPlugin()来加载,具体逻辑在中的这段代码:

1
2
3
4
5
6
7
8
9
10
11
if (!mfsu) {
const mfName =
(api.config.mfsu && api.config.mfsu.mfName) || DEFAULT_MF_NAME;
memo.plugins.push(
new webpack.container.ModuleFederationPlugin({
name: 'umi-app',
remotes: {
[mfName]: `${mfName}@${MF_VA_PREFIX}remoteEntry.js`,
},
}),
);

配置相应的 remotes 来加载之前 exposes 的文件

总结

webpack 5 引入的module federation较好的解决了不同模块之间的复用问题,在微前端以及 MFSU 中可以看到不同情况下的实践,对效率的提升和复用性都有很好的帮助。但是没有完全通用的方法,只有合适的方法。它也有一些问题:

  • module federation与 webpack 强耦合
  • 只有 webpack v5 的版本支持module federation,而从低版本的 webpack 升级到 v5 版本往往需要较大的工作量

这对module federation的推行有一定的阻碍,期待后续社区的推动让module federation成为标准。

Comments