从提供依赖共享的第三方或者其他的Webpack构建中import()
模块!运行时引入!欢迎来到Module Federation!
Module Federation的起源项目为webpack-external-import,现已经并入 Webpack5, v2.2.4为最后独立发布的版本。
原始文章信息
- 原标题:Module Federation in Webpack5
- 原作者: sokra
- 原链接: github.com/sokra/slide…
修改部分
- 原文为PPT,章节为本人划分。
- 部分图片转换为文字翻译插入文章中。
- 个人添加的内容标记为"注"。
动机
Module Federation的动机是为了不同开发小组间共同开发一个或者多个应用。
应用将被划分为更小的应用块,一个应用块,可以是比如"Header"或者"Sidebar"的前端组件,也可以是逻辑组件比如"Data Fetching Logic"或者其他业务逻辑。
每个应用块由不同的组开发。
应用或应用块共享其他其他应用块或者库。
这也就是Micro Frontends(MFES)的开发模式。
现有的方案
先看看现有的Webpack方案:
原生ESM模块
- 无需构建链接的模块
- 外部模块采用原生加载
挑战:
- 没有exports优化
- 性能
- 需要preloading
- 高请求次数
- 按需加载需要多次往返
- 只能支持ESM,不支持CommonJS, CSS和其他资源文件
单个构建
- 应用和应用块一起构建
- 外部模块在构建的时候确定并且被引用
挑战:
- 每次更新都需要全量构建
- 部署花费的时间很长
- 多应用不在独立
- 或者应用间在运行时不再共享公共模块
DllPlugin
- 应用块被编译成Dll
- 应用构建时引用Dll
- 外部模块在运行时从Dll中被引用
挑战:
- 每次应用块改变的时候,应用都需要重新构建
- 额外的部署花费时间
- 处理编译时依赖的额外架构
- 额外的Dll需要生成
- 为了共享依赖
- 为了手动优化使用过的模块
- 所有引用的Dll都需要额外的script标签
- Dll没有按需加载
externals
- 应用块被构建成库(一个暴露模块一个入口)
- 应用打包时声明应用块为
externals
- 外部模块在运行时被引用,比如从全局变量
挑战:
- 额外的库需要创建
- 为了共享依赖
- 所有引用的都
externals
需要额外的script标签 externals
没有按需加载
总结
- 原生ESM模块在大规模应用中,web性能不好
- 单个构建在大规模应用中,构建性能不好
- Dll在多应用中伸缩性不好
- Dll和externals都需要为共享依赖做很多额外的人工处理
我们需要一个可伸缩的解决方案,提供以下要求之间的取舍:
- 良好的构建性能
- 良好的Web性能
- 解决依赖共享
所以我们发明了Module Federation。
Module Federation
使用Module Federation
时,每个应用块都是一个独立的构建,这些构建都将编译为容器。
容器可以被其他应用或者其他容器应用。
一个被引用的容器被称为remote, 引用者被称为host,remote暴露模块给host, host则可以使用这些暴露的模块,这些模块被成为remote模块。
使用独立构建,我们可以为整个系统获得一个良好的构建性能。
概览
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/19/172cb4ff1a4e243d~tplv-t2oaga2asx-image.image)看图说话,这就是Module Federation
概览, 展示了2个概念: 暴露模块和共享模块。
容器通过异步的方式暴露模块。你将在使用容器中的模块前,请求容器加载(下载)你想要的模块。异步暴露模块将允许构建结果将不同的暴露模块和他们的依赖一起,放在不同的文件中。从而使得只有需要使用的模块会被加载,但是容器依旧将不同的模块一起打包。而且也将使用webpack的chunk机制(vendor分割或者创建一个文件包含不同暴露模块之间的公共依赖等).这将帮助我们有效降低请求数量和下载大小,从而获得良好的Web性能。
容器的消费者(也就是应用者)需要能处理异步加载暴露模块(注: 同步import代码语义保持不变,但是运行时应转换为异步加载), Webpack在这里做了特殊的处理,我们后续会解释。
图上还展示了共享模块的概念。 每一个部分,比如容器, 应用,都可以在共享scope中,添加共享模块(携带版本信息),同时也可以从 共享scope中, 加载共享模块(需要执行版本检查)。 共享scope将会通过给每个消费者提供版本要求内的最大可用版本的方式,对共享模块进行冗余剔除。
共享模块依旧异步暴露和异步加载。所以提供共享模块没有额外的下载消耗,只有需要的共享的模块将会被下载。
一个例子
上面是一个独立构建的例子。
HomePage(Team A开发)使用了组件Dropdown(Team 开发)。HomePage按需引用了LoginModal(Team A开发),LoginModal使用了Button组件 (Team 开发)。
全局的所有模块几乎都依赖了react。
让我们放出Module Federation跑跑。
Team B
从Team B的角度,它只关心这些东西。
Team B希望构建一个容器,并且给予这些模块标记。
Button和Dropdown是"Exposed",他们将可以被其他小组的人引用。react将是"Shared", 它将可以被其他小组的人共享。
现在轮到Webpack上场了
webpack将会为这个容器生成一个容器入口. 这个模块将包含所有暴露模块和共享模块的引用和加载方式。
每个暴露模块都和他们的依赖一起,被放入独立的文件。
每个共享模块也会被放入一个独立的文件。
当从容器中加载Button时,只会加载button chunk和react chunk. Dropdown也是同理。
当加载Dropdown,但是其他部分已经提供了另一个版本的react(可能是更高版本),则将会加载dropdown chunk以及其他部分提供的react chunk(通过其他部分请求,如果这个react chunk尚未加载)。
Team A
现在来看看Team A如何使用Team B的这个容器。
来自Team B的模块并不是直接被打包进来(注: 引入Team B提供的容器入口的时候),而是只打包了remote module的标记。在运行时将会引用容器并且会从容器加载模块。
webpack在这里做了特殊处理,就是异步请求。一个常规的import
是同步的,并不会等待模块下载。webpack在这里做了黑魔法,将所有的异步remote module的加载处理成了异步语义(就像import()
). 此时,就可以在加载本chunk的其他普通模块的同时,通过容器并行加载remote module。
比如上图中,当HomePage请求打开LoginModal的时候,LoginModal和Button的代码将并行加载。
共享模块也是类似。
ModuleFederationPlugin
我们通过设置ModuleFederationPlugin来使用Module Federation,配置属性少不了。
对于创建容器,重点是exposes属性,这个属性指明了这个容器的消费者将可以获取什么模块。可以给暴露的模块取名,同时要说明对应的内部模块的入口。任何Webpack能处理的模块它都支持,JS,TS,CSS, WebAssembly等等。
对于消费(引用)容器来说,要设置remotes属性, 这个属性是一个对象,包含了所有当前构建可用的容器。key值是当前代码可访问的容器暴露模块的scope(注:就是前缀)。任何以key值开头的模块请求都在运行时请求remote module。值是容器的位置。默认情况下,容器的入口来自script externals(注意: 引用的容器的entry文件通过script标签的方式手动引入页面)。当然也可以指定URL、script文件,或者全局对象都可以。这个脚本(注意: 引用的容器的entry文件)在运行时会被加载,而且可以从全局对象中访问。
如果想共享模块,就需要设置shared属性。最简单的例子就是列举一堆共享的模块名称,他们将会以当前安装的版本提供,并且被消费(注:被其他人引用)时候会按照引用者package.json中的版本要求来加载。
每个选项都有一些高级选项,一个值得注意的选项是共享模块里的singleton: true
。它能确保运行时模块的单例。这能满足有些库比如react
等不应初始化多次的要求。这种情况下,版本要求不符将会在运行时产生一个警告信息。
还有更多高级选项比如覆写版本或者版本要求,关闭版本推断,文件名,允许使用不同来源的库或者外部依赖(NodeJS中),或者更严格版本警告(直接给错误而不是警告)。