Remix集成@module-federation/vite失败,SSR构建报错求助
Remix + @module-federation/vite SSR构建失败问题排查
问题背景
我正在构建一个微前端联邦应用PoC,其中:
host是使用@module-federation/vite配置联邦的Remix应用remote是仅通过Vite和@module-federation/vite生成的待消费微前端(MFE)
核心需求是Remix应用在访问特定路由(测试环境为/mfe)时加载该MFE。MFE侧可正常生成文件,但Remix主机添加federation插件后构建失败,且需要保持ssr: true(Remix默认配置)。
报错信息
vite v5.4.10 building SSR bundle for production... ✓ 0 modules transformed. x Build failed in 12ms [add-entry] ENOENT: no such file or directory, open 'C:\DEV\poc_remix\virtual:remix\server-build' at Object.readFileSync (node:fs:441:20) at Object.buildStart (C:\DEV\poc_remix\node_modules\@module-federation\vite\lib\index.cjs:109:41) at file:///C:/DEV/poc_remix/node_modules/rollup/dist/es/shared/node-entry.js:20789:40 at async Promise.all (index 8) at async PluginDriver.hookParallel (file:///C:/DEV/poc_remix/node_modules/rollup/dist/es/shared/node-entry.js:20717:9) at async file:///C:/DEV/poc_remix/node_modules/rollup/dist/es/shared/node-entry.js:21664:13 at async catchUnfinishedHookActions (file:///C:/DEV/poc_remix/node_modules/rollup/dist/es/shared/node-entry.js:21135:16) at async rollupInternal (file:///C:/DEV/poc_remix/node_modules/rollup/dist/es/shared/node-entry.js:21661:5) at async Module.build (file:///C:/DEV/poc_remix/node_modules/vite/dist/node/chunks/dep-BWSbWtLw.js:65443:14) at async viteBuild (C:\DEV\poc_remix\node_modules\@remix-run\dev\dist\vite\build.js:212:5) { errno: -4058, code: 'PLUGIN_ERROR', syscall: 'open', path: 'C:\\DEV\\poc_remix\\virtual:remix\\server-build', pluginCode: 'ENOENT', plugin: 'add-entry', hook: 'buildStart' }
配置代码
主机应用vite.config.ts
export default defineConfig({ plugins: [ federation({ name: "remix_host", remotes: { mfe: { type: "module", name: "mfe", entry: "http://localhost:3000/mfe.js", }, }, exposes: {}, filename: "mfe.js", shared: { react: { singleton: true }, }, }), remix({ future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, v3_singleFetch: true, v3_lazyRouteDiscovery: true, }, routes: (defineRoutes) => defineRoutes((route) => { route("/", "routes/home.tsx", { index: true }); route("/mfe", "routes/test-mfe.tsx"); }), }), tsconfigPaths(), ], build: { target: "chrome89", }, });
远程MFE的vite.config.ts
export default defineConfig({ plugins: [ federation({ name: "mfe", filename: "mfe.js", exposes: { "./mfeButton": "./src/mfeButton", }, remotes: {}, shared: { react: { singleton: true, }, }, }), react(), ], build: { target: "chrome89", }, server: { port: 3000, }, });
主机应用package.json依赖部分
"dependencies": { "@module-federation/vite": "^1.1.5", "@remix-run/node": "^2.13.1", "@remix-run/react": "^2.13.1", "@remix-run/serve": "^2.13.1", "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@remix-run/dev": "^2.13.1", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "autoprefixer": "^10.4.19", "eslint": "^8.38.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "postcss": "^8.4.38", "typescript": "^5.1.6", "vite": "^5.1.0", "vite-tsconfig-paths": "^4.2.1" },
远程MFE的package.json依赖部分
"dependencies": { "@module-federation/vite": "^1.1.5", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@eslint/js": "^9.13.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", "eslint": "^9.13.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "globals": "^15.11.0", "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", "vite": "^5.4.10" }
test-mfe.tsx代码
/* eslint-disable import/no-unresolved */ import { lazy } from "react"; const RemoteMfeButton = lazy( async () => // @ts-expect-error loading remote await import("mfe/mfeButton") ); export default function TestMFE() { return ( <> <p>I AM A MFE</p> <RemoteMfeButton /> </> ); }
已尝试调整方案
- 移除所有Remix future标志
- 在
federation配置中添加/移除entryGlobalName: 'remote'和shareScope: 'default' - 在
federation配置中添加/移除manifest: true - 调整插件列表顺序
问题与解答
1. 是否存在我未了解到的Remix与@module-federation/vite兼容限制?
确实存在兼容性限制。Remix的SSR构建依赖虚拟模块virtual:remix/server-build,但@module-federation/vite当前版本(v1.1.5)无法正确处理Vite虚拟模块,会将其当作物理文件尝试读取,直接触发报错。
2. 该限制是否来自@module-federation/vite而非Remix?
是的。问题根源在@module-federation/vite的add-entry插件逻辑:它使用fs.readFileSync读取入口路径,但没有处理虚拟模块的特殊情况,而Remix的SSR构建流程完全符合Vite规范。
3. 报错发生在server-build阶段,是否可仅在客户端构建中启用联邦?若可以,如何实现?
可以仅在客户端构建中启用联邦,具体实现分两步:
步骤1:条件注册联邦插件
修改主机应用的vite.config.ts,通过Vite的isServer参数判断,仅在客户端构建时加载联邦插件:
export default defineConfig(({ isServer }) => ({ plugins: [ // 仅客户端构建启用模块联邦 !isServer && federation({ name: "remix_host", remotes: { mfe: { type: "module", name: "mfe", entry: "http://localhost:3000/mfe.js", }, }, exposes: {}, filename: "mfe.js", shared: { react: { singleton: true }, }, }), remix({ future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, v3_singleFetch: true, v3_lazyRouteDiscovery: true, }, routes: (defineRoutes) => defineRoutes((route) => { route("/", "routes/home.tsx", { index: true }); route("/mfe", "routes/test-mfe.tsx"); }), }), tsconfigPaths(), ].filter(Boolean), // 过滤空值,避免注册无效插件 build: { target: "chrome89", }, }));
步骤2:确保MFE仅在客户端加载
修改test-mfe.tsx,用Remix的useIsServer钩子避免服务器端渲染远程组件:
/* eslint-disable import/no-unresolved */ import { lazy, Suspense } from "react"; import { useIsServer } from "@remix-run/react"; const RemoteMfeButton = lazy( async () => // @ts-expect-error loading remote await import("mfe/mfeButton") ); export default function TestMFE() { const isServer = useIsServer(); if (isServer) { // 服务器端返回占位内容 return <p>Loading MFE...</p>; } return ( <> <p>I AM A MFE</p> <Suspense fallback={<p>Loading button...</p>}> <RemoteMfeButton /> </Suspense> </> ); }
此方案既避免了SSR构建时的虚拟模块读取错误,又符合模块联邦仅在浏览器端运行的逻辑。
内容的提问来源于stack exchange,提问作者Gabriele Buffolino
相关产品推荐
相关产品推荐

