Engineered Universe 可以理解成 Webpack 5 为现代前端工程提供的一套可组合架构蓝图。它并不是官方文档里的某个孤立特性,而是由模块联邦、持久化缓存、资源模块、更激进的 Tree Shaking 以及内置 Worker 支持等能力共同构成的工程化体系。其目标是让多个独立构建、独立部署的前端应用,在同一运行时环境中像宇宙中的不同星系一样彼此引用、共享依赖,同时保持各自的构建边界。

过去要实现类似能力,通常需要借助 externals、single-spa 或 iframe 隔离,但这些方案要么共享粒度太粗,要么运行时协调成本过高。Webpack 5 的模块联邦从构建产物层面提供了更细粒度的模块共享机制,配合持久化缓存与资源模块,让开发体验和构建性能都有明显提升。下文会从核心机制、配置方式、优化策略和避坑指南四个角度展开。
一、模块联邦是工程宇宙的引力核心
模块联邦解决的核心问题是:多个独立构建的应用,如何在运行时互相引用对方的模块,同时还能共享公共依赖。在 Webpack 5 之前,跨应用共享模块通常需要把公共库抽成独立的 chunk 并通过全局变量暴露,或者使用 external 把依赖排除到 CDN,再手动管理加载顺序。这些做法在小规模场景下勉强可用,一旦应用数量增加,版本冲突和加载时序就会变得难以维护。
ModuleFederationPlugin 允许一个应用暴露自己的模块,另一个应用在运行时加载这些模块,就像加载本地模块一样。远程应用通过 exposes 字段声明可对外提供的模块,宿主应用通过 remotes 字段声明需要消费的远程模块。更重要的是,shared 字段可以配置公共依赖的单例策略,让 React、Vue 这类库在多个远程应用之间只加载一次,从而同时减少体积冲突和状态不一致问题。
下面是一个远程应用的配置示例,它暴露了一个 Button 组件,并声明 React 为共享单例依赖。
// 远程应用 webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
output: { publicPath: 'auto' },
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
宿主应用在 remotes 中声明远程应用地址后,就可以在代码里通过 import('remoteApp/Button') 直接使用远程模块。这种动态引用方式让模块联邦天然适合微前端、组件市场、多团队协作等工程宇宙场景。
二、持久化缓存与资源模块降低工程宇宙的构建成本
工程宇宙意味着一个仓库中可能存在多个独立的构建入口,如果每次冷启动都需要完整遍历依赖图,开发体验会随着应用数量增加而急剧下降。Webpack 5 引入的持久化缓存可以把模块解析结果、代码生成结果以及 chunk 信息写入磁盘,下一次构建时直接复用这些中间产物,从而大幅缩短二次构建时间。
启用持久化缓存只需要在配置中声明 cache 类型为 filesystem,并指定 buildDependencies,确保配置文件变化时缓存自动失效。这样即使在 CI 环境中,只要缓存目录被正确保留,构建时间也能从分钟级降低到秒级。
module.exports = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
},
};
与缓存同样重要的是资源模块。Webpack 5 将 file-loader、url-loader 和 raw-loader 的能力整合为四种内置类型:asset/resource、asset/inline、asset/source 和 asset。配置规则从原来需要安装多个 loader 并拼接 options,简化为直接声明 type 字段。例如处理图片时,可以使用 type: 'asset',Webpack 会根据资源大小自动决定是输出文件还是内联为 data URI,这减少了配置复杂度,也让不同子应用之间的资源处理策略保持一致。
在工程宇宙中,持久化缓存保障了多应用协同开发时的构建速度,资源模块则统一了静态资源的处理方式。两者共同降低了团队在构建层面的认知负担,让开发者可以把更多精力放在业务模块的边界设计上。
三、Tree Shaking、Worker 支持与 Top Level Await 强化工程宇宙边界
Webpack 5 对 Tree Shaking 进行了更细致的优化,尤其是对嵌套模块和副作用标记的处理更加准确。工程宇宙中的每个子应用都会单独打包,如果公共库没有被正确标记为无副作用,那么即使只用了其中一个函数,也可能把整个库打进去。Webpack 5 的 sideEffects 配置可以让模块声明自己是否存在副作用,构建器据此更安全地删除未使用的导出,从而让每个远程产物更小、加载更快。
内置 Worker 支持是另一个边界强化的体现。Webpack 5 不再需要 worker-loader,可以直接通过 new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }) 创建 Web Worker。Worker 脚本会作为独立 chunk 输出,并被主 bundle 正确引用。对于工程宇宙中需要执行计算密集型任务的子应用,这个特性降低了 Worker 接入的成本。
Top Level Await 的支持则让远程模块的初始化逻辑更自然。在没有顶层 await 时,异步初始化通常需要包装在异步函数中,或者借助生命周期钩子。Webpack 5 允许在模块顶层直接使用 await,模块加载完成后才会执行后续依赖,这让远程模块可以自行完成异步配置请求,而不需要宿主应用额外协调。
const config = await fetch('/api/remote-config').then(res => res.json());
export default config;
这些能力组合在一起,让工程宇宙中的每个单元都具备更强的自治性:它们可以独立进行依赖裁剪、独立启动 Worker、独立完成异步初始化,而不会影响其他远程模块的运行。
四、落地工程宇宙时的共享依赖策略与避坑指南
模块联邦虽然强大,但共享依赖配置不当会引发隐蔽的运行时问题。最常见的是多个远程应用声明了同一个依赖的不同版本,却在本地以不对称的方式加载。例如远程应用 A 声明共享 React 18,远程应用 B 声明共享 React 17,宿主应用如果没有统一策略,就可能同时加载两个 React 实例,导致 Hook 状态丢失或 Context 失效。解决方式是使用 singleton: true 强制依赖在全局只实例化一次,并通过 requiredVersion 声明可接受版本范围。
另一个容易忽略的问题是远程模块的加载顺序。模块联邦的远程入口需要先加载才能解析对应的异步 chunk,而共享依赖的加载优先级会影响最终产物。建议在开发阶段通过 webpack 的 stats 输出确认共享模块是否被正确去重,并在生产构建后使用浏览器 DevTools 的 Network 面板观察是否有重复的公共库请求。
跨域问题也需要提前处理。远程入口文件通常部署在独立域名或独立路径下,如果宿主应用与远程应用不同源,就需要正确配置 output.publicPath 以及远程服务器的 CORS 响应头。Webpack 5 的 publicPath: 'auto' 可以自动推导 chunk 加载地址,但在多级路由或按需加载远程子应用时,建议手动验证资源路径是否与页面 URL 匹配。
落地工程宇宙的思路应当是渐进式的。先选择一个依赖关系简单、边界清晰的子应用作为远程应用,验证模块联邦和持久化缓存的配置;然后逐步把更多子应用接入,统一共享依赖策略;最后再结合 Tree Shaking 和 Worker 支持做性能优化。这样可以在不破坏现有开发流程的前提下,逐步获得 Webpack 5 工程宇宙带来的协作效率与构建收益。