Webpack 5 Module Federation本地单测微前端加载错误处理咨询
问题描述
我在基于NX CLI的单仓库中采用微前端架构,宿主应用通过Webpack 5的Module Federation加载各微前端的remoteEntry.js,并按路由作为组件渲染。已实现的Error Boundary在开发/预发布/生产环境中表现正常——当remoteEntry.js获取失败或微前端崩溃时,会显示404页面,符合预期。
但单独运行宿主应用单元测试或**本地单独启动宿主(未启动微前端)**时,Webpack会直接抛出错误:
Loading script failed.
(error: http://localhost:3107/remoteEntry.js)
希望在这些场景下,能触发Error Boundary显示404页面,而非直接抛出错误中断应用。
宿主应用Webpack配置如下:
const nrwlConfig = require('@nrwl/react/plugins/webpack.js'); const { ModuleFederationPlugin } = require('webpack').container; const deps = require('../../package.json').dependencies; const { remoteEntries } = require('./remoteEntries'); const EnvType = { LOCAL: 'local', PLAYGROUND: 'playground', DEVELOPMENT: 'development', STAGING: 'staging', PRODUCTION: 'production', }; module.exports = (config) => { nrwlConfig(config); config.optimization.runtimeChunk = 'single'; return { ...config, plugins: [ ...config.plugins, new ModuleFederationPlugin({ name: 'hostApp', filename: 'remoteEntry.js', remotes: { app1: `app1@${getRemoteEntryUrl(remoteEntries.APP1)}`, app2: `app2@${getRemoteEntryUrl(remoteEntries.APP2)}`, app3: `app3@${getRemoteEntryUrl( remoteEntries.APP3 )}`, app4: `app4@${getRemoteEntryUrl( remoteEntries.APP4 )}`, app5: `app5@${getRemoteEntryUrl(remoteEntries.APP5)}`, app6: `app6@${getRemoteEntryUrl( remoteEntries.APP6 )}`, app7: `app7@${getRemoteEntryUrl( remoteEntries.APP7 )}`, }, shared: { ...deps, react: { singleton: true, eager: true, requiredVersion: deps.react, }, 'react-dom': { singleton: true, eager: true, requiredVersion: deps['react-dom'], }, }, }), ], }; }; const getRemoteEntryUrl = (entry) => { const { NODE_ENV } = process.env || EnvType.DEVELOPMENT; if (NODE_ENV !== EnvType.LOCAL) { return entry.remoteEntries[NODE_ENV]; } return `http://localhost:${entry.localPort}/remoteEntry.js`; };
解决方案
以下是几种可行的处理方案,可根据场景组合使用:
1. 动态加载微前端组件时捕获异常
Module Federation的静态remotes配置会在应用启动时尝试加载remoteEntry.js,绕过Error Boundary。改为动态加载,将加载逻辑放在组件内部,即可被Error Boundary捕获。
修改步骤:
- 可保留Webpack中
remotes静态配置,但不直接使用 - 在路由组件中封装动态加载函数,包裹
try/catch抛出错误
示例代码:
import React, { Suspense, lazy } from 'react'; import ErrorBoundary from './ErrorBoundary'; // 动态加载微前端的函数 const loadRemoteComponent = async (remoteName, modulePath) => { try { // 初始化remote链接 const remote = await import(`webpack/container/reference/${remoteName}`); await remote.init(__webpack_share_scopes__.default); const module = await import(`webpack/container/remote/${remoteName}/${modulePath}`); return module.default; } catch (error) { // 抛出错误让Error Boundary捕获 throw new Error(`微前端加载失败: ${error.message}`); } }; // 封装为lazy组件 const App1Component = lazy(() => loadRemoteComponent('app1', './App')); // 路由中使用 const AppRoutes = () => ( <ErrorBoundary fallback={<div>404 - 微前端服务不可用</div>}> <Suspense fallback={<div>加载中...</div>}> <Route path="/app1" element={<App1Component />} /> {/* 其他微前端路由同理 */} </Suspense> </ErrorBoundary> ); export default AppRoutes;
2. 为测试/本地场景配置Mock Remote
通过环境变量判断场景,将remotes指向本地Mock模块,避免请求不存在的remoteEntry.js。
修改Webpack配置的getRemoteEntryUrl函数:
const getRemoteEntryUrl = (entry) => { const { NODE_ENV, TEST_ENV } = process.env || {}; // 测试环境或本地单独启动时,返回mock文件路径 if (TEST_ENV || (NODE_ENV === EnvType.LOCAL && !process.env.MICRO_FRONTEND_ENABLED)) { return 'mock/remoteEntry.js'; } if (NODE_ENV !== EnvType.LOCAL) { return entry.remoteEntries[NODE_ENV]; } return `http://localhost:${entry.localPort}/remoteEntry.js`; };
在项目中创建mock/remoteEntry.js,返回错误提示组件:
// mock/remoteEntry.js module.exports = { get: () => Promise.resolve({ default: () => <div>404 - 微前端未启动</div> }) };
3. 自定义Remote加载逻辑捕获错误
将静态remotes改为函数形式,自定义加载逻辑,捕获失败后返回Mock容器。
修改Webpack的ModuleFederationPlugin配置:
new ModuleFederationPlugin({ // ...其他配置 remotes: { app1: async () => { try { const remoteUrl = getRemoteEntryUrl(remoteEntries.APP1); const container = await import(/* webpackIgnore: true */ remoteUrl); return container; } catch (error) { // 返回Mock容器,渲染错误组件 return { get: () => Promise.resolve({ default: () => <div>404 - 微前端服务不可用</div> }) }; } }, // 其他微前端同理配置 }, })
4. 单元测试中Mock微前端模块
在测试文件中直接Mock微前端的导入,避免实际加载远程资源:
// 测试文件顶部 jest.mock('webpack/container/reference/app1', () => ({ init: jest.fn().mockResolvedValue(true), })); jest.mock('webpack/container/remote/app1/App', () => ({ default: () => <div>Mock App1 组件</div>, })); // 或直接Mock整个加载函数 jest.mock('./loadRemoteComponent', () => ({ __esModule: true, default: jest.fn().mockResolvedValue(() => <div>Mock 组件</div>), }));
内容的提问来源于stack exchange,提问作者Raz Levy
相关产品推荐
相关产品推荐

