前言
领导:那个,咱的项目越来越大了,你研究下微前端,把几个子系统拆出来,但体验要像单页面一样顺滑。🍭🍭🍭
哥们我二话不说,直接上 Vite + Module Federation (模块联邦) 。
微前端介绍
微前端(Micro Frontends)架构已经成为大型前端工程的标配。从最早的 iframe 到阿里的 qiankun,再到如今 Webpack 5 引入的 Module Federation(模块联邦) ,微前端技术一直在进化。
而在 Vite 生态日益壮大的今天,如何利用 Vite + Module Federation 构建一套极速、轻量、高性能的微前端系统?本文将基于真实项目实战(Host 应用与 Equipment 子应用),手把手带你落地这套方案。
1. 为什么选择 Module Federation?
在深入实战前,我们先聊聊它与老牌霸主 qiankun 的区别:
| 特性 | qiankun (基于 single-spa) | Module Federation (模块联邦) |
|---|---|---|
| 核心原理 | 基于基座路由拦截,加载 HTML 入口,JS 沙箱隔离 | 基于模块加载,运行时动态加载远程模块 (Chunk) |
| 依赖共享 | 较难共享依赖,各子应用通常自带 React/Vue | 原生支持依赖共享,host 和 remote 可复用 react 实例 |
| 性能 | 需解析 HTML、CSS,开销相对较大 | 极速,仅加载必要的 JS Chunk |
| 开发体验 | 配置较繁琐,需关注沙箱隔离问题 | 无感集成,像 import 本地组件一样使用远程组件 |
| 技术栈 | 框架无关(React/Vue/Angular 混用) | 最好技术栈统一(如都用 React),否则难以共享依赖 |
结论:如果你的团队技术栈统一(例如都是 React + Vite),Module Federation 是目前性能最好、开发体验最佳的方案。
2. 架构概览
在我们的实战中,架构如下:
-
PDK (Host) : 主应用,负责整体布局、登录、菜单导航。它动态加载子应用。
- 运行端口: 5001 (假设)
-
Equipment (Remote) : 设备管理子应用,独立开发、独立部署,但也作为模块提供给 Host 使用。
- 运行端口: 5002
-
Other (Remote) : 其它子应用,独立开发、独立部署,但也作为模块提供给 Host 使用。
- 运行端口: other
3. 实战配置:Module Federation 落地
我们将使用 @module-federation/vite 插件来实现。
安装插件 npm install -D @module-federation/vite
3.1 子应用配置 (Remote: Equipment)
子应用的目标是暴露 (Expose) 自己的核心组件,供主应用消费。
文件:equipment/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { federation } from '@module-federation/vite'
export default defineConfig({
plugins: [
react(),
federation({
name: 'equipment', // 1. 定义模块名称
filename: 'remoteEntry.js', // 2. 生成的入口文件名称
// 3. 暴露模块:key 是导入路径,value是本地组件路径
exposes: {
'./EquipmentApp': './src/ EquipmentApp.jsx',
},
// 4. 共享依赖:避免 React 实例冲突(Hooks 报错)和减少包体积
shared: ['react', 'react-dom', 'react-router'],
})
],
server: {
port: 5002, // 固定端口
origin: 'http://localhost:5002', // 必须指定 origin,解决静态资源跨域路径问题
cors: true, // 允许跨域
},
build: {
target: 'chrome89', // 支持顶层 await
minify: false,
cssCodeSplit: false, // 样式不拆分,避免样式丢失
rollupOptions: {
output: {
// 加上 hash 避免缓存问题
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
})
关键点解析:
- exposes: 这里我们暴露了 EquipmentApp。这是子应用的“总入口”,它内部包含了子应用自己的路由系统。
- shared: 必须共享 react 和 react-router。如果不共享,Host 和 Remote 会各自持有一个 React 副本,导致 Context 丢失、Hooks 报错(如 Invalid hook call)。单例模式。
3.2 子应用入口改造
为了让子应用既能独立运行(作为 SPA),又能被嵌入运行(作为组件),我们需要对路由入口做特殊处理。
文件:equipment/src/EquipmentApp.jsx
import React from 'react'
import { useRoutes } from 'react-router'
import routers from '@/router/index.jsx'
export default function EquipmentApp({
basePath }) {
// 关键:接收主应用传来的 basePath
if (basePath) {
// 设置全局变量或 Context,让内部路由知道自己的根路径
window.__EQUIPMENT_BASE_PATH__ = basePath
}
// 使用 useRoutes 渲染路由表
const element = useRoutes(routers)
return element
}
在子应用的路由配置中(router/index.jsx),我们需要根据 basePath 动态生成路由路径,确保在主应用中挂载到 /equipment 下时,子路由(如 /equipment/list)能正确匹配。
3.3 主应用配置 (Host: PDK)
主应用的目标是消费 (Consume) 子应用提供的模块。
文件:pdk/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { federation } from '@module-federation/vite'
export default defineConfig({
plugins: [
react(),
federation({
name: 'host',
// 1. 定义远程模块
remotes: {
// key: 导入时的模块名
// value: 远程模块的地址
equipment: {
type: "module",
name: "equipment",
entry: "http://localhost:5002/
remoteEntry.js",
entryGlobalName: "equipment",
shareScope: "default",
},
},
filename: "remoteEntry.js",
shared: ["react", "react-dom", "react-router"],
})
],
// ... 其他配置
})
3.4 主应用使用远程组件
在 React Router 中,我们可以利用 React.lazy 来实现远程组件的动态加载,就像加载本地组件一样简单。
文件:pdk/src/router/index.jsx
import React from 'react'
import { createBrowserRouter } from 'react-router'
// 1. 动态导入远程组件
/* equipment/EquipmentApp' 对应 Host 配置中 remotes 的 key + Remote 配置中 exposes 的 key
*/
const EquipmentApp = React.lazy(() => import('equipment/EquipmentApp'))
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
// 2. 配置路由,将 /equipment/* 下的 所有请求都交给子应用处理
path: 'equipment/*',
element: (
<React.Suspense fallback=
{<div>Loading Equipment...</
div>}>
{/* 3. 传入 basePath,告诉子应 用它当前被挂载在哪里 */}
<EquipmentApp basePath="/
equipment" />
</React.Suspense>
)
}
]
}
])
子应用没有启动或者加载失败建议使用错误边界,加载一个错误组件。
4. 遇到的坑与最佳实践
4.1 样式隔离问题
虽然 Module Federation 不像 Shadow DOM 那样提供强样式隔离,但在 Vite + TailwindCSS 方案下,我们可以通过添加前缀来解决。
- 方案:在子应用的 tailwind.config.js 中设置 prefix(如 eq-),或者使用 CSS Modules。
4.2 路由冲突与 404
- 问题:主应用刷新 /equipment/list 时,Vite 开发服务器可能会返回 404。
- 解决:确保主应用的路由配置使用通配符 (如 path: 'equipment/'),并且子应用内部路由能够感知 basePath。
4.3 缓存问题
-
问题:更新子应用后,主应用加载的还是旧代码。
-
解决:
- Vite 配置:在 build.rollupOptions 中配置 [hash] 文件名(如上文配置所示)。
- Nginx 配置:确保 remoteEntry.js 永远不缓存 (Cache-Control: no-cache),而带 hash 的 JS 文件可以长期缓存。
4.4 类型提示 (TypeScript)
由于远程模块在编译时不存在,TS 会报错 Cannot find module 'equipment/EquipmentApp'。
-
解决:在 src/remotes.d.ts 中声明类型:
TypeScript declare module 'equipment/EquipmentApp' { const EquipmentApp: React. ComponentType<{ basePath?: string }>; export default EquipmentApp; }
5. 总结
通过 Vite + Module Federation,我们将 PDK 和 Equipment 拆分成了独立的工程,实现了:
- 独立部署:子应用更新无需重新构建主应用。
- 增量构建:开发时只需启动自己关注的模块,速度极快。
- 无感融合:用户体验就是单页应用 (SPA),没有 iframe 的割裂感。
相比于 qiankun,这套方案更现代、更轻量,特别适合 React + Vite 技术栈的团队。随着 Webpack 5 和 Vite 对模块联邦支持的不断完善,这必将是未来微前端的主流方向。
模块联邦地址,附带vue demo介绍 github.com/module-fede…