Module Federation in Webpack5(上)

3,035 阅读8分钟

从提供依赖共享的第三方或者其他的Webpack构建中import()模块!运行时引入!欢迎来到Module Federation

Module Federation的起源项目为webpack-external-import,现已经并入 Webpack5, v2.2.4为最后独立发布的版本。

原始文章信息

修改部分

  1. 原文为PPT,章节为本人划分。
  2. 部分图片转换为文字翻译插入文章中。
  3. 个人添加的内容标记为"注"。

动机

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在多应用中伸缩性不好
  • Dllexternals都需要为共享依赖做很多额外的人工处理

我们需要一个可伸缩的解决方案,提供以下要求之间的取舍:

  • 良好的构建性能
  • 良好的Web性能
  • 解决依赖共享

所以我们发明了Module Federation

Module Federation

使用Module Federation时,每个应用块都是一个独立的构建,这些构建都将编译为容器

容器可以被其他应用或者其他容器应用。

一个被引用的容器被称为remote, 引用者被称为hostremote暴露模块给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中),或者更严格版本警告(直接给错误而不是警告)。