Webpack 5 引入的 Designing Universe 设计宇宙,官方更准确的叫法是围绕 Module Federation 模块联邦构建的一整套设计体系。它的核心目标只有一个:让多个独立构建、独立部署的应用,能够在浏览器里像一个整体那样协作运行。过去我们要实现跨项目共享组件,往往依赖 npm 发包或者 externals 加 CDN 脚本,成本高且版本管理混乱。而模块联邦允许一个应用在运行时动态加载另一个应用暴露出来的模块,甚至可以共享 React、Vue 这样的公共依赖,避免重复打包。这篇文章就来把这套设计思想的来龙去脉和落地方法讲清楚。

一、Designing Universe 想解决什么问题
要理解这套设计理念,得先回到前端工程的真实痛点。假设你的团队维护着主站、活动页、管理后台三个应用,它们技术栈一致,都依赖 React 和一套内部组件库。传统做法下,每个应用都要单独打包这份依赖,用户在三个应用之间跳转时,同样的 React 代码要被下载解析多次。依赖越多,这种浪费越严重。
另一种常见方案是把公共依赖挂到 window 全局变量上,用 externals 排除打包。这种方式看似省流量,实际上把版本管理的责任全部推给了页面维护者。一旦某个应用升级了 React 版本,而全局脚本还停留在旧版本,运行时就会报出各种难以排查的 Hooks 报错。
Designing Universe 的思路是反过来的:与其让开发者手工协调依赖,不如让构建工具自己理解依赖关系。每个应用既可以声明我提供了哪些模块,也可以声明我需要共享哪些依赖以及接受哪些版本范围。Webpack 5 会在运行时根据这些声明,自动协商出最合适的依赖版本,能复用就复用,需要加载新版本就按需加载。这种运行时协商机制,就是整个设计宇宙的核心引擎。
二、模块联邦的核心配置详解
模块联邦的配置都在 webpack.config.js 的 ModuleFederationPlugin 插件中完成。先看一个宿主应用,也就是消费方的配置示例。
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
// 宿主应用自身也要有 name
plugins: [
new ModuleFederationPlugin({
name: 'host',
// 远程模块的地址清单
remotes: {
// key 是别名,value 指向远程容器的入口文件
widgetApp: 'widgetApp@http://localhost:3001/remoteEntry.js'
},
// 声明共享依赖及其版本要求
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
})
]
};再看提供方的配置。提供方负责暴露自己的模块,供其他应用消费。
new ModuleFederationPlugin({
name: 'widgetApp',
filename: 'remoteEntry.js',
// 暴露给外部的模块,key 是对外路径
exposes: {
'./Button': './src/components/Button',
'./Dialog': './src/components/Dialog'
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
});这里有几个关键点值得展开。name 是当前容器在联邦中的唯一标识,remotes 里的别名必须与远程容器的 name 一致,否则运行时找不到模块。exposes 定义的路径会映射成远程模块的加载地址,宿主引用时写成 widgetApp/Button 即可。shared 配置里的 singleton 设为 true,意味着整个页面只允许存在一个 React 实例,这对依赖内部全局状态的库至关重要。如果不设置 singleton,多个 React 副本共存会导致 Invalid Hook Call 这类诡异报错。
在业务代码里使用远程模块也非常直观,直接用动态 import 就行。
const RemoteButton = React.lazy(() => import('widgetApp/Button'));
function App() {
return (
<React.Suspense fallback={<div>加载中</div>}>
<RemoteButton onClick={() => alert('来自远程应用')}>点我</RemoteButton>
</React.Suspense>
);
}三、运行时加载与依赖协商机制
模块联邦最精妙的部分在于运行时行为。当宿主应用加载 remoteEntry.js 时,这个文件并不是普通的 JS bundle,而是一个容器入口。它内部维护着一张暴露模块的清单,只有在宿主真正 import 远程模块时,对应的 chunk 才会被下载执行。也就是说,远程应用即使暴露了五十个模块,宿主只用了两个,其余四十八个永远不会产生网络请求,这是按需加载在联邦层面的自然延伸。
依赖协商的过程发生在页面初始化阶段。宿主和远程容器都会各自评估 shared 里声明的依赖:如果宿主已经加载了符合版本范围的 React,远程容器就直接复用它,不再重复下载;如果版本不匹配且允许非单例,Webpack 会加载一个独立的副本供该容器使用;如果声明了 singleton 且版本冲突,控制台会给出警告,运行时采用先加载的那个版本。这套机制让版本管理从人工约定变成了自动协商,大幅降低了跨团队协作的沟通成本。
需要特别注意的是 scope 的概念。默认情况下所有容器共享同一个全局作用域,如果你希望某个远程容器拥有隔离的依赖环境,可以在 shared 配置里指定不同的 scope 名称,形成多个平行的依赖宇宙。这也是设计宇宙这个说法的另一层含义:每个应用既是独立的星球,又通过共享依赖的引力场彼此关联。
四、持久化缓存与构建性能的配合
Designing Universe 不只是模块联邦,Webpack 5 的持久化缓存是支撑这套体系的另一块基石。微前端架构下项目数量多、构建频繁,如果每次都要全量编译,开发体验会非常糟糕。开启文件系统缓存后,二次构建速度往往能提升到原来的数倍。
module.exports = {
cache: {
type: 'filesystem',
buildDependencies: {
// 配置文件变化时让缓存失效
config: [__filename]
},
cacheDirectory: path.resolve(__dirname, '.webpack_cache')
}
};持久化缓存基于内容哈希工作,Webpack 会把每个模块的处理结果连同依赖图谱一起序列化到磁盘。下次构建时,只要文件内容没变、loader 和插件版本没变,就直接跳过编译环节。buildDependencies 用来声明影响构建结果的额外因素,把 webpack.config.js 自己加进去是个好习惯,否则改了配置但缓存不失效,会得到难以理解的旧产物。
此外,Webpack 5 移除了 Node.js polyfill 的自动注入,改用更精细的 asset modules 处理图片字体等资源,这些改动共同指向同一个设计哲学:构建工具应该对产物有更准确的认知,而不是替开发者做模糊的猜测。这与模块联邦的显式声明思路是一脉相承的,理解了这一点,也就理解了整个 Designing Universe 的设计出发点。
五、落地建议与常见坑
在实际落地时,建议先把组件库这种稳定性高的模块纳入联邦,业务页面其次,路由级别的前端拆分最后再做。共享依赖的 requiredVersion 一定要写清楚,推荐用上 singleton 防止多实例问题。TypeScript 项目需要注意,远程模块的类型无法直接推断,可以维护一份类型声明文件或者通过 npm 包分发 d.ts 文件。
另一个高频坑是 publicPath 配置。远程容器的异步 chunk 默认基于页面域名解析,如果远程应用部署在独立域名下,必须显式设置 publicPath,否则动态 chunk 会 404。排查这类问题时,先看 Network 面板里请求地址指向哪里,基本都能快速定位。
总的来说,Webpack 5 的这套设计体系为微前端提供了开箱即用的工程化方案,它不引入新的运行时框架,只依赖浏览器原生的动态加载能力,侵入性小、可控性强。当你面临多应用协作、依赖冗余或者团队边界划分的问题时,模块联邦值得作为首选方案认真评估。
Webpack 5Designing Universe模块联邦修改时间:2026-09-03 15:23:16