Vite 模块联邦踩坑实录:带你闭坑
你是不是也遇到过这种情况:兴冲冲地想用 Vite 的模块联邦把项目拆分成微前端,结果 npm run dev 一跑,浏览器控制台一片红?remoteEntry.js 404?Top-level await is not available?Failed to resolve module specifier?
别慌,这篇文章就是来救你的。我把踩过的坑都填平了,你只管抄作业。
第一个坑:remoteEntry.js 404,人都傻了
崩溃现场
我按照官方文档配置好了远程应用(表单设计器),信心满满地 npm run dev,然后在主应用里引入远程组件。结果浏览器控制台直接给我来了一个:
GET http://localhost:3001/mf/remoteEntry.js 404 (Not Found)
我一脸懵逼:配置明明写了 filename: 'mf/remoteEntry.js',为啥找不到?
闭坑方案
后来我才发现,@originjs/vite-plugin-federation 这个插件有个大坑:
- 远程应用:开发模式(
npm run dev)不生成remoteEntry.js! - 主应用:开发模式(
npm run dev)可以正常使用。
也就是说,远程应用必须用 构建模式 才能生成 remoteEntry.js。我改了 package.json:
{
"scripts": {
"dev": "vite",
"dev:mf": "vite build --watch",
"dev:remote": "npm run dev:mf & npm run preview",
"preview": "vite preview --port 3001"
}
}
然后运行 npm run dev:remote,终于看到 build/assets/mf/remoteEntry.js 生成了!
注意路径是 /assets/mf/remoteEntry.js,不是 /mf/remoteEntry.js!Vite 构建时会自动把文件放到 assets 目录下。
第二个坑:Top-level await 报错,又懵了
崩溃现场
好不容易解决了 404,结果浏览器又给我来了一个:
Top-level await is not available in the configured target environment
我心想:啥玩意儿?我又没写 await,哪来的 top-level await?
闭坑方案
后来我翻了半天源码才发现,模块联邦生成的代码里用了 top-level await,而 Vite 默认的 target 是 es2020,不支持这个特性。
必须把 target 改成 esnext,而且要在 三个地方 都改:
build.targetesbuild.targetoptimizeDeps.esbuildOptions.target
我改了 vite.config.js:
export default defineConfig({
esbuild: {
target: 'esnext',
},
optimizeDeps: {
esbuildOptions: {
target: 'esnext',
},
},
build: {
target: 'esnext',
},
});
重新构建,终于不报错了!
第三个坑:Failed to resolve module specifier,崩溃三连
崩溃现场
前两个坑填完了,我以为终于可以跑起来了。结果浏览器又给我来了一个:
Failed to resolve module specifier "form_designer_remote"
我一看主应用的配置:
remotes: {
formDesigner: 'form_designer_remote@http://localhost:3001/assets/mf/remoteEntry.js'
}
这不是官方文档的写法吗?为啥不行?
闭坑方案
后来我发现,@originjs/vite-plugin-federation 的配置格式和 Webpack 的不一样!
Vite 的模块联邦配置必须用 对象格式,不能带 form_designer_remote@ 前缀:
remotes: {
formDesigner: {
external: 'http://localhost:3001/assets/mf/remoteEntry.js'
}
}
改完之后,终于跑起来了!泪目!
闭坑方案:完整可用的配置
远程应用配置(表单设计器)
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'form_designer_remote',
filename: 'mf/remoteEntry.js',
exposes: {
'./FormDesigner': './src/views/FormDesigner/FormDesigner.jsx',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
esbuild: {
target: 'esnext',
},
optimizeDeps: {
esbuildOptions: {
target: 'esnext',
},
},
build: {
target: 'esnext',
minify: false,
cssCodeSplit: false,
},
});
主应用配置(流程引擎)
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'process_engine_host',
remotes: {
formDesigner: {
external: 'http://localhost:3001/assets/mf/remoteEntry.js'
}
},
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
},
}),
],
server: {
port: 3900,
cors: true,
},
});
使用远程组件
// RemoteFormDesigner.tsx
import { lazy, Suspense } from 'react';
import { Spin } from 'antd';
const FormDesigner = lazy(() => import('formDesigner/FormDesigner'));
function RemoteFormDesigner(props) {
return (
<Suspense fallback={<Spin size="large" tip="加载表单设计器..." />}>
<FormDesigner {...props} />
</Suspense>
);
}
export default RemoteFormDesigner;
开发流程:一键启动
# 1. 远程应用:一键启动构建+预览
cd lowcodeform-frontend
npm run dev:remote
# 2. 主应用:启动开发服务器
cd ProcessEngineServer/client
npm run dev
访问主应用:http://localhost:3900,终于看到远程组件加载成功了!
开发联调架构流程
自动重新构建] B1[npm run preview] --> B2[启动静态服务器] B2 --> B3["http://localhost:3001/assets/mf/remoteEntry.js ✓"] end subgraph Host["主应用 (流程引擎 - 端口 3900)"] C1[npm run dev] --> C2[Vite 开发服务器] C2 --> C3["配置 remotes:
formDesigner: { external: 'http://localhost:3001/...' }"] C3 --> C4["懒加载组件:
const FormDesigner = lazy(() => import('formDesigner/FormDesigner'))"] C4 --> C5[运行时动态加载远程模块 ✓] end B3 -->|CORS 跨域访问| C3 style Remote fill:#e1f5ff style Host fill:#fff4e1 style B3 fill:#90EE90 style C5 fill:#90EE90
问题排查流程:遇到问题怎么办?
如果你也遇到了模块联邦的问题,可以按照这个流程图快速定位:
是否运行?} Q1 -->|否| A1[启动远程应用
npm run dev:remote] Q1 -->|是| Q2{访问 remoteEntry.js
是否返回 404?} A1 --> End([问题解决 ✓]) Q2 -->|是| A2[检查路径
应该是 /assets/mf/] Q2 -->|否| Q3{报错: Failed to
resolve module
specifier?} A2 --> End Q3 -->|是| A3[检查主应用配置
使用对象格式
不带前缀] Q3 -->|否| Q4{报错: Top-level
await not
available?} A3 --> End Q4 -->|是| A4[检查远程应用
三个 target 都是 esnext] Q4 -->|否| A5[其他问题
查看文档] A4 --> End A5 --> End style Start fill:#ffcccc style End fill:#90EE90 style A1 fill:#fff4e1 style A2 fill:#fff4e1 style A3 fill:#fff4e1 style A4 fill:#fff4e1
总结:三个关键点
- 远程应用必须用构建模式:
npm run dev:mf+npm run preview - 三个地方都配置
target: 'esnext':build.target、esbuild.target、optimizeDeps.esbuildOptions.target - 主应用用对象格式配置
remotes:不带前缀,路径是/assets/mf/remoteEntry.js
希望这篇文章能帮你少踩点坑。如果你还遇到其他问题,欢迎加作者交流!
终极方案:一劳永逸解决所有问题
说实话,上面这些坑踩下来,我已经对 @originjs/vite-plugin-federation 失去信任了。它的问题不是一个两个,而是设计层面的缺陷:
- 开发模式不能用,必须 build --watch,体验割裂
- target 要配三遍,封装不到位
- 配置格式和 Webpack 不兼容,迁移成本高
- 热更新不生效,改代码要手动刷新
- 共享依赖版本冲突时报错信息不友好
- 微前端场景下 CSS 样式冲突,主应用和子应用样式互相污染
如果你也受够了这些问题,可以试试我封装的插件组合(持续更新):
@jiayouzuo/vite-module-federation-core - 模块联邦核心插件
这个插件针对上述痛点做了完整优化:
- ✅ 开发模式直接可用:不用 build --watch,真正的 HMR 体验
- ✅ 自动处理 target:内部统一配置,不用写三遍
- ✅ 兼容 Webpack 配置习惯:迁移零成本
- ✅ 热更新正常工作:改代码自动刷新
- ✅ 智能共享依赖管理:自动处理版本冲突
- ✅ 暴露 exposes 配置:方便其他插件(如 CSS 作用域插件)读取
@jiayouzuo/vite-plugin-css-scope - CSS 作用域隔离插件
解决微前端场景下的样式冲突问题:
- ✅ 自动给 CSS 选择器添加作用域前缀
- ✅ 自动识别模块联邦暴露的组件并注入作用域
- ✅ 支持多种模块联邦插件(包括 vite-module-federation-core)
安装使用:
# 安装模块联邦核心插件
npm install @jiayouzuo/vite-module-federation-core
# 如果需要样式隔离,额外安装
npm install @jiayouzuo/vite-plugin-css-scope
配置示例(远程应用):
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@jiayouzuo/vite-module-federation-core';
import cssScope from '@jiayouzuo/vite-plugin-css-scope';
export default defineConfig({
plugins: [
// 可选:CSS 作用域隔离,需要再react之前进行
cssScope({
scope: 'remote-app',
include: ['src/'],
}),
react(),
federation({
name: 'remote_app',
filename: 'remoteEntry.js',
exposes: {
'./FormDesigner': './src/views/FormDesigner/FormDesigner.jsx',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
});
配置示例(主应用):
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@jiayouzuo/vite-module-federation-core';
export default defineConfig({
plugins: [
react(),
federation({
name: 'host_app',
remotes: {
remoteApp: {
external: 'http://localhost:3001/assets/remoteEntry.js',
from: 'vite',
format: 'esm',
},
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
});
就这么简单,不用配 target,不用 build --watch,直接 npm run dev 就能用。样式冲突?加上 css-scope 插件一键解决。
相关技术文献资源
如果你想深入了解 Vite 模块联邦的更多细节,可以参考以下资源:
- vite-plugin-federation GitHub 仓库 - 官方源码和 Issues
- vite-plugin-federation - 官方文档 - 完整的配置指南和 API 说明
- module-federation - 官方文档 - 完整的配置指南和 API 说明
- Issue #410 - remoteEntry.js 404 问题讨论