module federation
概述
前端的工程化已经持续了很长时间,从最开始的手动更新和维护,之后以文件为单位的自动化工具的产生,在开发和部署的过程中进行一系列的操作,最终组成一个较大的 SPA 或者 MPA。随着业务的发展,网页应用越来越多,以文件为单位进行构建则难以维护与复用。webpack5 引入了一个重要的特性 module federation
,目的是解决多业务线,多工程下的模块复用问题。
使用方法
关于module federation
的用法,官方给出了很详细的示例,我们以automatic-vendor-sharing为例简单来看一下是如何使用的:
app1
先看一下 app1 的 webpack 关键配置:
1 | new ModuleFederationPlugin({ |
这里的关键信息是ModuleFederationPlugin
中声明了remotes,然后我们就可以使用了,如下所示:
1 | import LocalButton from './Button'; |
app2
我们回到 app1 再看一下 app1 的 webpack 配置,注意到 app1 配置了name,filename,以及exposes等信息,在开发和部署时会生成对应的文件,供其它 app 使用,我们来看一下 app2 的 webpack 关键配置:
1 | new ModuleFederationPlugin({ |
app2 中也调用了 app1 的组件:
1 | import LocalButton from './Button'; |
如上示例所示,两个应用相互调用对方暴露出来的模块,那么扩展到多个应用也同样合适,这样就可以较好的解决模块复用问题
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()
阶段开始遍历并收集依赖 - 对import和export调用
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 时调用 callbackonTransformDeps()
最终调用 DepInfo 实例,将相关依赖收集到 cache 中,并生成MFSU_CACHE.json
依赖处理
获取到依赖之后,我们就可以调用ModuleFederationPlugin()
生成对应的文件了,具体逻辑在DepBuilder中的这段代码:
1 | mfConfig.plugins.push( |
和上述示例中的导出模块配置基本一致,配置了相应的 filename 以及 exposes,生成的文件供后续加载使用
依赖加载
生成相应的导出文件之后,还是和示例一致,我们依然调用ModuleFederationPlugin()
来加载,具体逻辑在中的这段代码:
1 | if (!mfsu) { |
配置相应的 remotes 来加载之前 exposes 的文件
总结
webpack 5 引入的module federation
较好的解决了不同模块之间的复用问题,在微前端以及 MFSU 中可以看到不同情况下的实践,对效率的提升和复用性都有很好的帮助。但是没有完全通用的方法,只有合适的方法。它也有一些问题:
module federation
与 webpack 强耦合- 只有 webpack v5 的版本支持
module federation
,而从低版本的 webpack 升级到 v5 版本往往需要较大的工作量
这对module federation
的推行有一定的阻碍,期待后续社区的推动让module federation
成为标准。