Webpack 5 发布以来,最受关注的特性之一就是模块联邦(Module Federation)。它让多个独立构建、独立部署的应用在运行时共享模块成为可能,被不少人视为微前端落地的重要基础设施。所谓的 Nonlinear Thinking 非线性思维,指的正是这种打破传统单体线性构建模式的思路:不再把所有代码在一个构建流程里一次性打齐,而是让构建产物之间在运行时动态协商、按需共享。

传统构建模式的痛点与非线性思路的由来
在模块联邦出现之前,多个前端应用想要复用代码,主要有两条路。第一条是发布 npm 包,各应用各自安装、各自打包。这种方式最直观,但代价是每个应用的产物里都包含一份完整依赖,React、组件库、工具函数被反复打包,页面加载体积成倍增长。第二条是 externals 加 CDN script 标签,把公共依赖挂到全局变量上。体积问题解决了,但又带来新的麻烦:依赖版本必须全局统一,任何一个应用想升级 React 都得所有应用一起动,全局命名空间也容易产生冲突。
这两种方案本质上是线性思维:构建期决定一切,运行时只是被动执行。而模块联邦把决策点后移到了运行时,宿主应用(Host)可以在运行时向远程应用(Remote)请求某个模块,远程应用暴露的模块像普通 import 一样被消费,共享依赖则在加载时通过版本协商自动去重。构建产物不再是封闭的整体,而是一个个可以互相通信的容器。
这种模式对微前端场景尤其友好。团队 A 负责的订单应用和团队 B 负责的商品应用可以独立开发、独立部署、独立发版,运行时由外壳应用把它们组合在一起,公共库只加载一份。
Module Federation 的核心概念与工作原理
模块联邦围绕三个核心角色展开。第一个是远程容器(Container),即暴露模块给外界消费的应用;第二个是宿主(Host),即消费远程模块的应用;第三个是共享依赖(Shared Dependencies),双方都声明自己需要的公共库,运行时由框架负责协商。值得注意的是,宿主和远程的身份并不是固定的,一个应用既可以暴露模块,也可以消费别人的模块,身份完全由配置决定。
底层的实现依赖几个关键部件:ModuleFederationPlugin 负责在构建期生成容器入口和远程模块的代理桩;运行时的 remoteEntry 文件是一个轻量引导脚本,宿主通过它加载远程容器;加载完成后,容器内部的 get 和 init 接口完成模块初始化与依赖协商。当宿主 import 一个远程模块时,webpack 并不会在构建期把该模块的代码打进来,而是生成一个异步代理,等运行时真正执行到这行 import 才发起请求。
共享依赖的协商机制值得细看。宿主和远程都在 shared 配置里声明依赖及版本范围,加载时双方会比对版本:如果宿主已加载的版本满足远程的要求,就直接复用;如果都不满足,才会各自加载自己的一份。通过 requiredVersion、singleton、eager 等参数,可以精细控制协商行为,比如强制全局只保留一个 React 实例,避免多个 React 副本导致的 hooks 报错。
完整配置示例:宿主与远程如何协作
下面用一个最小可运行的例子演示两端配置。远程应用暴露一个按钮组件,宿主应用异步消费它,双方共享 react。
先看远程端的配置:
// remote/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;
module.exports = {
mode: 'development',
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button.js'
},
shared: {
react: { singleton: true, requiredVersion: deps.react }
}
})
]
};
再看宿主端的配置,注意 remotes 字段里的地址指向远程应用产出的 remoteEntry 文件:
// host/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;
module.exports = {
mode: 'development',
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js'
},
shared: {
react: { singleton: true, requiredVersion: deps.react }
}
})
]
};
宿主业务代码中的使用方式和普通异步组件几乎一样,唯一特殊的是模块路径要用 remoteName/xxx 的格式:
// host/src/App.js
const RemoteButton = React.lazy(() => import('remoteApp/Button'));
function App() {
return (
<div>
<h1>宿主应用</h1>
<React.Suspense fallback="加载中">
<RemoteButton />
</React.Suspense>
</div>
);
}
export default App;
这段配置有几个容易踩坑的地方。首先是 singleton: true,对 React 这类对实例唯一性敏感的库必须开启,否则可能出现两个 React 副本共存导致报错。其次是 eager 选项默认为 false,共享依赖会被拆成异步 chunk,如果你在入口同步使用 React,需要设置 eager: true 或配合异步边界引导。最后,远程应用跨域加载 remoteEntry 时,务必配置好 CORS 响应头。
架构收益与适用边界
模块联邦带来的收益是实实在在的。构建层面,各团队彻底解耦,互不阻塞发版;产物层面,公共依赖在运行时去重,页面体积明显下降;开发层面,借助 @module-federation/enhanced 这类生态工具,还能实现运行时依赖的动态更新。相比 iframe 方案,它没有通信序列化和样式隔离的历史包袱;相比 npm 包复用,它省去了漫长的发版等待。
但它并非银弹。远程模块的加载依赖网络,远程应用挂掉会直接影响宿主页面,因此生产环境必须做好错误边界和降级方案。类型共享也是一个现实问题,跨应用的 TypeScript 类型目前需要借助额外的类型生成插件来同步。此外,如果你们的团队规模不大、应用之间没有独立部署的需求,引入模块联邦反而会增加构建配置复杂度,得不偿失。
总体来看,模块联邦体现的是一种构建产物的去中心化思想:代码的所有权和代码的运行时组织被分离了。理解了这一点,再回头看 Webpack 5 的其他改动,比如更好的 tree shaking 和持久缓存,就能感受到这一版本在工程化方向上的整体野心。是否采用这套方案,取决于你的组织形态;但理解它的工作原理,对构建现代大型前端应用一定有帮助。
Webpack 5Module Federation前端工程化修改时间:2026-09-17 00:55:00