认识
一、认识
在 2020
年上半年,Webpack
提出了一项非常激动人心的特性——Module Federation
(译为模块联邦),这个特性一经推出就获得了业界的广泛关注,甚至被称为前端构建领域的Game Changer
。实际上,这项技术确实很好地解决了多应用模块复用的问题,相比之前的各种解决方案,它的解决方式更加优雅和灵活。
Module Federation
(模块联邦) 是 Webpack 5
中引入的一项革命性特性,它使得多个独立构建的应用程序(或代码库)可以在运行时进行模块共享与复用, 而不必在构建时进行整合。每个子应用(或称微前端应用)可以独立构建和部署,同时可以共享或暴露自己的模块给其他应用使用,实现松耦合和按需加载。
Module Federation
(模块联邦)实现原理
-
暴露(
Exposes
)与消费(Remotes
): 暴露(Exposes
), 在Webpack
配置中,你可以指定哪些模块需要暴露给其他应用使用。这些模块会被打包成一个可以在运行时被其他应用动态加载的文件。消费(Remotes
), 在应用中,你可以通过配置remotes
,从其他构建产物中动态加载暴露出来的模块,就像加载本地模块一样使用它们。 -
运行时共享:
Module Federation
在应用启动时,会动态解析各个应用之间共享的模块依赖。它能够在加载模块时判断版本和依赖关系,确保在不同应用之间能够安全共享模块而不会产生冲突。 -
代码分割与缓存: 由于共享模块在运行时加载,
Module Federation
结合Webpack
的代码分割和缓存策略,可以大幅减少重复打包,提高整体加载速度和缓存利用率。
二、特点
2.1 优点
-
实现任意粒度的模块共享。这里所指的模块粒度可大可小,包括第三方
npm
依赖、业务组件、工具函数,甚至可以是整个前端应用!而整个前端应用能够共享产物,代表着各个应用单独开发、测试、部署,这也是一种微前端的实现。 -
优化构建产物体积。远程模块可以从本地模块运行时被拉取,而不用参与本地模块的构建,可以加速构建过程,同时也能减小构建产物。
-
运行时按需加载。远程模块导入的粒度可以很小,如果你只想使用
app1
模块的add函数,只需要在app1
的构建配置中导出这个函数,然后在本地模块中按照诸如import('app1/add')
的方式导入即可,这样就很好地实现了模块按需加载。 -
第三方依赖共享。通过模块联邦中的共享依赖机制,我们可以很方便地实现在模块间公用依赖代码,从而避免以往的
external
+CDN
引入方案的各种问题。
从以上的分析你可以看到,模块联邦近乎完美地解决了以往模块共享的问题,甚至能够实现应用级别的共享,进而达到微前端的效果。
2.2 缺点
-
维护共享库: 它并不是没有任何缺点,在配置过程中我们可以看到需要配置
share
字段,即不同项目需要共享的三方库,例如A
项目使用vue
,B
项目也使用vue
,那么就需要将vue
声明出来,插件会将vue
自动分割出来,这样A
,B
项目可以共享使用了,在remote
端和host
端都需要声明。 但是,这里对开发同学有非常不友好的点:-
需要将组件共享的包都手动找出来
-
共享有时是强制的,如果漏了某些包,可能会导致页面报错或崩溃,想象一下页面同时有两个
vue
-
同一个库,有时需要维护在约定的版本范围内,有时一个包太老或太新也会在加载时报错
-
简而言之,就是对共享包的维护成本很大!
-
三、对比
3.1 NPM 包
发布 NPM
包是一种常见的复用模块的做法,我们可以将一些公用的代码封装为一个 NPM
包,具体的发布更新流程是这样的:
-
公共库
lib1
改动,发布到NPM
-
所有的应用安装新的依赖,并进行联调。

封装 NPM
包可以解决模块复用的问题,但它本身又引入了新的问题:
-
开发效率问题。每次改动都需要发版,并所有相关的应用安装新依赖,流程比较复杂。
-
项目构建问题。引入了公共库之后,公共库的代码都需要打包到项目最后的产物后,导致产物体积偏大,构建速度相对较慢。
因此,这种方案并不能作为最终方案,只是暂时用来解决问题的无奈之举。
3.2 Monorepo
作为一种新的项目管理方式,Monorepo
也可以很好地解决模块复用的问题。在 Monorepo
架构下,多个项目可以放在同一个 Git
仓库中,各个互相依赖的子项目通过软链的方式进行调试,代码复用显得非常方便,如果有依赖的代码变动,那么用到这个依赖的项目当中会立马感知到。

不得不承认,对于应用间模块复用的问题,Monorepo
是一种非常优秀的解决方案,但与此同时,它也给团队带来了一些挑战:
-
所有的应用代码必须放到同一个仓库。如果是旧有项目,并且每个应用使用一个
Git
仓库的情况,那么使用Monorepo
之后项目架构调整会比较大,也就是说改造成本会相对比较高。 -
Monorepo
本身也存在一些天然的局限性,如项目数量多起来之后依赖安装时间会很久、项目整体构建时间会变长等等,我们也需要去解决这些局限性所带来的的开发效率问题。而这项工作一般需要投入专业的人去解决,如果没有足够的人员投入或者基建的保证,Monorepo
可能并不是一个很好的选择。 -
项目构建问题。跟 发
npm
包的方案一样,所有的公共代码都需要进入项目的构建流程中,产物体积还是会偏大。
3.3 Git Submodule
通过 git submodule
的方式,我们可以将代码封装成一个公共的 Git
仓库,然后复用到不同的应用中,但也需要经历如下的步骤:
-
公共库
lib1
改动,提交到Git
远程仓库; -
所有的应用通过
git submodule
命令更新子仓库代码,并进行联调。
你可以看到,整体的流程其实跟发 npm
包相差无几,仍然存在 npm
包方案所存在的各种问题。
3.4 依赖外部化(external)+ CDN 引入
即对于某些第三方依赖我们并不需要让其参与构建,而是使用某一份公用的代码。按照这个思路,我们可以在构建引擎中对某些依赖声明external
,然后在 HTML
中加入依赖的 CDN
地址:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="root"></div>
<!-- 从 CDN 上引入第三方依赖的代码 -->
<script src="https://cdn.jsdelivr.net/npm/react@17.0.2/index.min.js"><script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@17.0.2/index.min.js"><script>
</body>
</html>
如上面的例子所示,我们可以对react
和react-dom
使用 CDN
的方式引入,一般使用UMD
格式产物,这样不同的项目间就可以通过window.React
来使用同一份依赖的代码了,从而达到模块复用的效果。不过在实际的使用场景,这种方案的局限性也很突出:
-
兼容性问题。并不是所有的依赖都有
UMD
格式的产物,因此这种方案不能覆盖所有的第三方npm
包。 -
依赖顺序问题。我们通常需要考虑间接依赖的问题,如对于
antd
组件库,它本身也依赖了react
和moment
,那么react
和moment
也需要external
,并且在HTML
中引用这些包,同时也要严格保证引用的顺序,比如说moment
如果放在了antd
后面,代码可能无法运行。而第三方包背后的间接依赖数量一般很庞大,如果逐个处理,对于开发者来说简直就是噩梦。 -
产物体积问题。由于依赖包被声明
external
之后,应用在引用其CDN
地址时,会全量引用依赖的代码,这种情况下就没有办法通过Tree Shaking
来去除无用代码了,会导致应用的性能有所下降。
四、问题
4.1 认识 Module Federation?
Module Federation
(模块联邦) 是 Webpack 5
中引入的一项革命性特性,它使得多个独立构建的应用程序(或代码库)可以在运行时共享代码,而不必在构建时进行整合。每个子应用(或称微前端应用)可以独立构建和部署,同时可以共享或暴露自己的模块给其他应用使用,实现松耦合和按需加载。
Module Federation
(模块联邦)实现原理: 总体而言,实现模块联邦有三大主要的要素, Host
模块: 即本地模块,用来消费远程模块; Remote
模块: 即远程模块,用来生产一些模块,并暴露运行时容器供本地模块消费; Shared
依赖: 即共享依赖,用来在本地模块和远程模块中实现第三方依赖的共享;
-
暴露(
Exposes
)与消费(Remotes
): 暴露(Exposes
), 在Webpack
配置中,你可以指定哪些模块需要暴露给其他应用使用。这些模块会被打包成一个可以在运行时被其他应用动态加载的文件。消费(Remotes
), 在应用中,你可以通过配置remotes
,从其他构建产物中动态加载暴露出来的模块,就像加载本地模块一样使用它们。 -
运行时共享:
Module Federation
在应用启动时,会动态解析各个应用之间共享的模块依赖。它能够在加载模块时判断版本和依赖关系,确保在不同应用之间能够安全共享模块而不会产生冲突。 -
代码分割与缓存: 由于共享模块在运行时加载,
Module Federation
结合Webpack
的代码分割和缓存策略,可以大幅减少重复打包,提高整体加载速度和缓存利用率。
4.2 Module Federation 是如何实现的?
一、构建时配置与代码生成:
-
ModuleFederationPlugin
:Module Federation
的核心是Webpack
内置的ModuleFederationPlugin
。在Webpack
配置中,开发者通过这个插件指定:name
, 当前构建的容器名称(用于暴露给其它应用);filename
, 生成暴露入口文件的文件名(例如remoteEntry.js
);exposes
, 暴露给其它应用的模块映射,如{"./Button": "./src/Button"}
;remotes
, 费其它容器的远程模块, 如{"remoteApp": "remoteApp@http://localhost:3001/remoteEntry.js"}
;shared
, 共享依赖配置,用来指定哪些库(比如react
、react-dom
)需要在各个容器间共享。 -
编译阶段的代码生成: 在编译过程中,
ModuleFederationPlugin
会对构建的入口和模块进行特殊处理: 1. 对暴露的模块, 插件会在输出的remoteEntry.js
中生成一个包含模块映射和加载逻辑的容器对象; 2. 对消费的模块, 插件会在构建后的运行时代码中嵌入动态加载远程容器的逻辑,使得在运行时能够请求远程暴露的模块。
二、运行时模块加载与共享:
- 远程容器加载: 当消费者应用在运行时需要加载远程模块时,会通过动态脚本加载(通常使用
JSONP
或动态创建<script>
标签)加载对应的远程入口文件(如remoteEntry.js
)。加载后,这个远程入口会注册一个全局对象(通常在window
上,如remoteApp
),该对象包含如下核心方法:
-
get(moduleName)
: 返回一个Promise
,用于异步获取暴露的模块。 -
init(shareScope)
: 初始化共享模块的上下文(share scope
), 确保多个容器之间能够协同共享依赖。
- 共享依赖(
Share Scope
): 在运行时,Webpack
会创建一个共享依赖的 共享作用域。各个容器在初始化阶段会调用init
方法来将自己内部的共享模块注册到全局共享作用域中。这样:
-
当远程容器加载某个模块时,会首先在共享作用域中查找是否已有可用的版本,如果有,则复用该模块;如果没有,则按照远程容器内部的版本加载。
-
这种机制确保了多个独立构建的应用之间不会重复加载相同的依赖,同时也能进行版本协商,避免版本冲突。
- 动态模块引用: 消费者应用中,通过类似
import Button from 'remoteApp/Button'
的语法,实际在运行时会触发调用远程容器的get('./Button')
方法,返回Promise
并解析为相应的模块。这样,远程模块就能按需加载并在消费者应用中使用。
三、内部机制与运行时流程:
-
模块请求过程, 当消费者应用请求一个远程模块时,首先检查当前容器是否已经加载了相应的远程入口, 如果没有,则动态加载入口脚本。加载成功后,调用远程容器的
init
方法,传入当前应用的共享作用域,完成共享依赖的初始化。调用远程容器的get(moduleName)
方法,返回一个Promise
,该Promise
会异步解析并返回模块工厂函数。使用返回的模块工厂函数生成模块实例,并注入到消费者应用的模块系统中,使其可以像本地模块一样使用。 -
共享作用域与版本协商, 共享依赖部分采用了 共享作用域 (
share scope
)的概念,每个容器内部会维护一个对象映射,记录各共享模块的版本和引用。当不同容器需要同一模块时,会检查各自版本信息,选择一个合适的版本进行复用。这种机制允许在多个应用间动态共享依赖,同时降低包体积和加载时间。