Webpack 5 引入的 Module Federation 为前端架构打开了一扇新的大门,很多团队将其视为微前端落地的关键基础设施。Sail Universe 航行宇宙这个提法,本质上是对模块联邦所构建的分布式应用网络的一种比喻。在这个宇宙中,每个独立构建的应用都可以成为宿主或远程模块的提供者,通过运行时加载的方式共享代码,而无需在构建阶段彼此依赖。下面先展示一张示意图来帮助理解这种架构关系。

Module Federation 的核心机制:从构建时依赖到运行时共享
在传统的前端工程中,代码共享主要依赖 npm 包或者 monorepo 中的本地模块引用,这些方式都要求所有参与方在构建阶段就确定好版本与依赖关系。一旦某个共享模块需要升级,所有消费方都必须重新安装依赖并重新构建发布,流程繁琐且容易造成版本冲突。Webpack 5 的 Module Federation 改变了这一点,它让模块共享发生在浏览器运行时,通过加载远程入口文件(remoteEntry.js)来获取暴露的模块,构建阶段不再需要感知远程模块的具体实现。
Module Federation 的基本模型包含两个角色:宿主(host)和远程模块(remote)。宿主负责加载远程模块,远程模块则在自身的构建配置中通过 exposes 字段声明自己对外暴露哪些模块。Webpack 在构建远程应用时会生成一个特殊的运行时文件 remoteEntry.js,该文件包含了远程模块的模块映射和加载逻辑。宿主应用在运行时通过动态 import 语法并结合远程模块的地址,向浏览器发起请求获取 remoteEntry.js,然后由 Webpack 的容器机制完成模块的实例化与依赖解析。
这种机制的核心优势在于,远程模块可以独立部署、独立升级,只要接口一致,宿主无需重新构建就能获取到最新的远程代码。同时,依赖共享通过 shared 字段进行配置,Webpack 会尽量复用宿主已有的依赖版本,避免重复加载,并通过版本协商机制解决不兼容问题。这就像宇宙中的舰队之间通过标准协议进行通信,每个星球保持自治,但又能协同作战。
// webpack.config.js (远程应用)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
output: {
publicPath: 'http://localhost:3001/',
},
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./utils': './src/utils/formatDate',
},
shared: {
react: { singleton: true, eager: true },
'react-dom': { singleton: true, eager: true },
},
}),
],
};
上面的配置将一个远程应用命名为 remoteApp,并暴露了 Button 组件和 formatDate 工具函数。注意 publicPath 必须设置为远程应用的完整访问地址,否则宿主在加载 remoteEntry.js 时无法正确解析后续的 chunk 文件。共享依赖 react 和 react-dom 被标记为单例,确保整个页面只存在一份 React 实例,这是避免状态错乱的关键。
宿主端配置与远程模块消费示例
宿主应用同样需要在 webpack 配置中声明 Module Federation 插件,并指明它需要消费哪些远程模块。与远程应用不同的是,宿主端通常不需要设置 exposes,而是通过 remotes 字段来注册远程模块的地址。这样,在宿主代码中就可以像使用本地模块一样动态 import 远程模块,Webpack 会在运行时自动完成远程容器的加载。
// webpack.config.js (宿主应用)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, eager: true },
'react-dom': { singleton: true, eager: true },
},
}),
],
};
在宿主应用的业务代码中,可以通过 import() 语法加载远程组件,Webpack 会自动处理远程入口的请求和模块初始化。下面的示例展示了如何动态加载远程按钮组件,并在 React 中渲染。这种方式不需要在构建宿主时拥有远程组件的源码,只需要约定好暴露的模块路径即可。
// 宿主应用中的 App.jsx
import React, { lazy, Suspense } from 'react';
const RemoteButton = lazy(() => import('remoteApp/Button'));
function App() {
return (
<div>
<h1>Host Application</h1>
<Suspense fallback="Loading Button...">
<RemoteButton label="Click me" />
</Suspense>
</div>
);
}
export default App;
注意远程模块的加载是异步的,因此使用 React 的 lazy 和 Suspense 组件进行包装是推荐做法。此外,远程模块中的路径 remoteApp/Button 前面的 remoteApp 必须与宿主配置中 remotes 的键名保持一致,后面的 Button 则对应远程应用 exposes 中定义的 ./Button。这种约定让跨应用的模块引用变得非常直观。
如果远程组件依赖了宿主没有共享的第三方库,比如一个日期处理库,远程应用需要自行打包这些依赖,否则运行时会报模块未找到的错误。这正是 shared 配置需要精心设计的原因。通常建议将核心框架如 React、Vue、Angular 等设置为单例共享,而业务相关的工具函数则不共享,由远程应用单独打包,以避免版本冲突和复杂的协商逻辑。
航行宇宙实践中的避坑与性能优化
在实际项目中落地 Module Federation 时,单例冲突是最常见的问题之一。例如宿主应用使用 React 17,而远程应用使用 React 18,即使双方都声明了 singleton: true,Webpack 也会根据版本范围进行协商,若版本不兼容会回退到各自加载自己的副本,此时可能出现多个 React 实例,导致 Hooks 上下文丢失或组件渲染异常。解决方法是统一团队间的框架版本,或者在共享配置中明确指定 requiredVersion 并设置合理的版本范围。
// 更严谨的 shared 配置
shared: {
react: {
singleton: true,
requiredVersion: '^17.0.0',
eager: true,
},
'react-dom': {
singleton: true,
requiredVersion: '^17.0.0',
eager: true,
},
}
样式隔离是另一个不可忽视的难点。远程模块加载后会将其样式插入到宿主页面的 <head> 中,如果远程组件使用了全局 CSS 或者非模块化的类名,很容易污染宿主的样式。推荐在远程应用中使用 CSS Modules 或者 CSS-in-JS 方案,确保样式作用域隔离。同时,可以通过在远程模块的构建配置中禁用样式提取到全局的方式,将样式作为模块的一部分随组件加载,减少全局冲突。
性能方面,远程模块的加载会引入额外的网络请求和运行时开销。为了优化首屏性能,可以采取按需加载的策略,仅在用户触发特定交互时才动态 import 远程模块,而不是在应用启动时就加载所有远程依赖。另外,远程入口文件 remoteEntry.js 应该尽可能小,只包含模块映射和加载逻辑,真正的业务代码拆分到独立的 chunk 中。Webpack 5 的持久化缓存和代码分割策略同样适用于远程应用,合理配置 optimization.splitChunks 可以进一步减小传输体积。
最后需要强调的是,Module Federation 并不是银弹,它更适合那些由多个团队独立开发、需要独立部署、但又在同一个页面上协同工作的场景。如果团队规模较小或者应用之间耦合度很高,传统的 monorepo 或者 npm 包方式可能更简单直接。理解 Sail Universe 航行宇宙的本质,不是追求技术上的新奇,而是根据业务需求选择合适的架构,真正让前端应用像宇宙中的舰队一样,既能独立航行,又能协同作战。

