在传统的打包思路里,一个前端工程就是一个封闭的世界:所有模块在构建期被静态分析,最终塞进同一个或少数几个 bundle 文件中。而 Webpack 5 引入的 Module Federation(模块联邦)打破了这一边界,多个独立构建、独立部署的应用可以在运行时互相加载对方的模块,共享同一份依赖。支撑这套机制的运行时体系,官方称之为 Executing Universe(执行宇宙)——每一个独立的执行环境是一个 universe,它们通过容器(Container)互联,共同构成一个可以跨应用协作的模块宇宙。理解执行宇宙,是从单纯打包工具使用者进阶到大型前端架构设计者的关键一步。

一、执行宇宙的核心概念:从封闭 bundle 到开放容器
要理解 Executing Universe,需要先回到 Webpack 的运行时模型。Webpack 4 及之前版本的运行时是围绕单棵模块依赖树设计的:入口模块作为根节点,所有依赖在构建期被链接成 __webpack_require__ 的调用链。这种模型假设所有模块在构建时都可见,任何跨工程的模块引用都无能为力。
Webpack 5 在运行时层面引入了三个新角色:Container(容器)、Exposed Module(暴露模块)和 Remote(远程应用)。容器本质上是一个挂载在全局或全局对象上的异步接口,它对外提供 get 和 init 两个方法:init 负责初始化共享依赖的作用域,get 负责按需获取暴露出去的模块。宿主应用(Host)通过异步加载远程容器,就能在自己的宇宙里执行另一个宇宙的代码。
举个例子,宿主应用从远端加载一个按钮组件时,运行时的大致流程是:先通过 script 标签或动态 import 拉取远程入口文件,该文件会在约定的全局变量上注册容器;随后调用容器的 init 方法协商共享依赖版本;最后调用 get("Button") 拿到模块工厂函数并执行。整个过程对业务代码完全透明,开发者写出来的仍然是一个普通的 import。
二、实战配置:搭建一个最小的模块联邦示例
下面用一个 host 应用和一个 remote 应用来演示最小可运行配置。先看 remote 端如何暴露模块,重点是 ModuleFederationPlugin 的 exposes 与 filename 配置:
// remote/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
// remote 必须暴露为某种环境,web 环境即可
output: {
publicPath: 'http://localhost:3001/',
},
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
remote 应用在这里做了两件事:一是通过 exposes 把本地的 Button 组件以 ./Button 这个标识暴露出去;二是声明 react 和 react-dom 为共享依赖并标记为单例。singleton 意味着整个执行宇宙中只允许存在一份 React 实例,这对依赖 Hooks 和 Context 的 React 应用至关重要,否则两个 React 副本会导致状态管理彻底混乱。
再看 host 端的配置,通过 remotes 声明远程应用的地址:
// host/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
new HtmlWebpackPlugin(),
],
};
配置完成后,host 中的业务代码可以直接以异步组件的方式使用远程模块:
// host/src/App.js
import React, { Suspense } from 'react';
// 远程模块必须异步加载,配合 Suspense 提供加载态
const RemoteButton = React.lazy(() => import('remoteApp/Button'));
export default function App() {
return (
<Suspense fallback={<div>加载远程组件中...</div>}>
<RemoteButton />
</Suspense>
);
}
值得注意的是,远程模块必须走异步加载路径,因为容器的 init 协商本身就是异步的。这也是模块联邦与普通动态 import 最大的体验差异:它要求组件边界天然是异步的。实践中通常会把 lazy 加载封装成高阶组件或路由级别的组件,避免在每个使用点重复写 Suspense。
三、共享依赖的去重机制与性能收益
执行宇宙最精妙的部分是 shared 依赖的运行时协商。每个参与联邦的应用在构建时都会生成一份共享模块清单,记录依赖的版本区间、加载地址等信息。当 host 和 remote 同时声明共享 react 时,运行时会比较双方的版本:如果 host 已经加载了满足 remote 版本区间的 React,remote 会直接复用,不再下载第二份;反之,remote 会自行加载自己需要的版本,两份代码在各自的共享作用域中互不干扰。
这个机制带来的收益体现在两个方面。其一是网络体积:在微前端架构下,子应用不必各自打包一份体积可观的类库,公共依赖在整页只会传输一次。其二是内存与一致性:singleton 模式保证了上下文的一致,事件系统、状态管理库不会因为多副本而出现诡异的行为。
共享依赖还可以进一步控制加载策略。比如通过 requiredVersion 指定精确的版本范围,通过 eager 决定依赖是否随入口一起打包。需要注意的是 eager: true 会把共享模块打进当前 bundle,失去按需加载的意义,一般只在 SSR 或入口极小的场景下使用:
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
strictVersion: true, // 版本不满足时直接报错,避免隐式降级
},
}
在大型工程里建议开启 strictVersion,让版本不匹配在开发阶段就暴露出来,而不是等到线上出现难以排查的运行时错误。团队协作时还应约定统一的版本策略文档,把各应用的依赖版本区间固化下来,避免协商结果不可预测。
四、微前端实践中的常见坑与应对
第一个常见的坑是 publicPath 配置错误。远程容器的 chunk 是按需加载的,如果 remote 的 output.publicPath 指向了错误地址,运行时会去 host 域名下请求不存在的 chunk 文件,报出的错往往是模糊的加载失败。解决方法是显式设置 remote 的 publicPath 为其部署地址,或在运行时通过 __webpack_public_path__ 动态计算。
第二个坑是循环依赖与初始化顺序。容器的 init 只应被调用一次,如果多个 remote 之间互相引用且都共享了同一批依赖,可能触发重复初始化警告。Webpack 运行时内部对 init 做了幂等处理,但共享作用域的作用时机仍需注意:在任何共享模块被使用之前必须完成协商。遇到偶发的 undefined 错误时,优先检查是否存在绕过异步边界同步引用远程模块的写法。
第三个坑与样式和全局状态有关。执行宇宙只管 JS 模块的加载与共享,不会处理样式隔离。多个子应用使用同名全局 class 或全局变量时会互相覆盖,需要配合 CSS Modules、CSS-in-JS 或运行时样式沙箱(如 shadow DOM 方案)来隔离。此外,跨应用的状态共享也不应依赖模块联邦直接导出可变对象,更稳妥的做法是通过事件总线或专门的状态层交互。
总体来看,Executing Universe 不是一个孤立的功能点,而是 Webpack 5 对运行时架构的一次重新设计。它让独立部署的应用在浏览器中组成一个协作整体,同时保留了各自构建、各自发版的自由。掌握容器、共享作用域和异步边界这三个核心概念,再结合版本管理与样式隔离的工程约定,就能把这套机制稳妥地落地到微前端项目中,真正享受跨团队模块复用带来的效率提升。
Webpack 5Executing Universe构建性能优化修改时间:2026-09-13 06:48:32