基于Webpack5 Module Federation的业务解耦实践示例
本文介绍如何利用Webpack5 Module Federation技术,将数栈中的AppMenus导航组件从dt-common中剥离,实现与子产品的业务解耦。通过配置远程暴露与宿主引用,解决多子产品协同变更繁琐的问题,并详细阐述了本地调试、远程部署及异常处理的具体步骤与配置细节。
前言
本文涉及数栈项目中使用的特定术语,在此统一进行解释说明。
- dt-common:各子产品共同引入的公共依赖包,功能类似于NPM包。
- AppMenus:用于在子产品间快速跳转的导航栏组件,统一维护于dt-common中,由各个子产品引入使用。
- Portal:所有子产品的统一访问入口。
- APP_CONF:存储子产品相关配置信息的变量或对象。
背景
在迭代过程中,大量需求涉及AppMenus的修改,这些需求的生效需要各子产品配合进行统一变更。目前数栈前端项目中,AppMenus的相关逻辑位于dt-common内,而dt-common以独立目录形式存在于每个子项目中。因此,此类需求变更的分支需包含所有子产品,导致前端开发和测试工作存在大量重复。

本文旨在通过Webpack5 Module Federation的实践,实现AppMenus与各子产品的解耦。具体做法是将AppMenus作为共享资源从dt-common中分离,确保其更新部署无需其他子产品配合。

本地实现
Portal项目
- 将AppMenus拆分至Portal项目的Components目录下,Portal内部对AppMenus的引用视为正常的组件内部引用。
配置Module Federation相关参数,以便其他项目能够远程依赖该组件。
const federationConfig = {
name: 'portal',
filename: 'remoteEntry.js',
// 当前组件需要暴露出去的组件
exposes: {
'./AppMenus': './src/views/components/app-menus',
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: deps.react
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: deps['react-dom'],
},
},
};
const plugins = [
...baseKoConfig.plugins,
{
key: 'WebpackPlugin',
action: 'add',
opts: {
name: 'ModuleFederationPlugin',
fn: () => new ModuleFederationPlugin({ ...federationConfig }),
},
},
].filter(Boolean);- 在dt-common中修改Navigator组件对AppMenus的引用方式,改为通过props.children实现。
子产品项目
配置Module Federation的配置文件。
const federationConfig = {
name: 'xxx',
filename: 'remoteEntry.js',
// 关联需要引入的其他应用
remotes: {
// 本地相互访问采取该方式
portal: 'portal@http://127.0.0.1:8081/portal/remoteEntry.js',
},
shared: {
antd: {
singleton: true,
eager: true,
requiredVersion: deps.antd,
},
react: {
singleton: true,
eager: true,
requiredVersion: deps.react,
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: deps['react-dom'],
},
},
};修改AppMenus的引用方式。
const AppMenus = React.lazy(() => import('portal/AppMenus'));
<Navigator
{...this.props}
>
<React.Suspense fallback="loading">
<AppMenus {...this.props} />
</React.Suspense>
</Navigator>
// 需要 ts 定义
// typings/app.d.ts 文件
declare module 'portal/AppMenus' {
const AppMenus: React.ComponentType<any>;
export default AppMenus;
}注意本地调试时,子产品需将Portal的访问路径代理至Portal服务端口,才能访问Portal暴露出的相关chunk.js文件。
module.exports = {
proxy: {
'/portal': {
target: 'http://127.0.0.1:8081', // 本地
//target: 'portal 对应的地址', 本地 -〉 devops 环境
changeOrigin: true,
secure: false,
onProxyReq: ProxyReq,
},
}
}远程部署
部署至服务器时,由于Portal项目中的AppMenus属于远程组件(即共享依赖),而子产品为宿主环境,因此部署时需同时部署Portal项目与子产品。上述配置中,主要变更的是加载地址。Portal项目无需变更,需变更的是子产品中的相关逻辑。
//remote.tsx
import React from 'react';
function loadComponent(scope, module) {
return async () => {
// Initializes the share scope. This fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__('default');
const container = window[scope]; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
};
}
const urlCache = new Set();
const useDynamicScript = (url) => {
const [ready, setReady] = React.useState(false);
const [errorLoading, setErrorLoading] = React.useState(false);
React.useEffect(() => {
if (!url) return;
if (urlCache.has(url)) {
setReady(true);
setErrorLoading(false);
return;
}
setReady(false);
setErrorLoading(false);
const element = document.createElement('script');
element.src = url;
element.type = 'text/javascript';
element.async = true;
element.onload = () => {
console.log('onload');
urlCache.add(url);
setReady(true);
};
element.onerror = () => {
console.log('error');
setReady(false);
setErrorLoading(true);
};
document.head.appendChild(element);
return () => {
urlCache.delete(url);
document.head.removeChild(element);
};
}, [url]);
return {
errorLoading,
ready,
};
};
const componentCache = new Map();
export const useFederatedComponent = (remoteUrl, scope, module) => {
const key = `${remoteUrl}-${scope}-${module}`;
const [Component, setComponent] = React.useState(null);
const { ready, errorLoading } = useDynamicScript(remoteUrl);
React.useEffect(() => {
if (Component) setComponent(null);
// Only recalculate when key changes
}, [key]);
React.useEffect(() => {
if (ready && !Component) {
const Comp = React.lazy(loadComponent(scope, module));
componentCache.set(key, Comp);
setComponent(Comp);
}
// key includes all dependencies (scope/module)
}, [Component, ready, key]);
return { errorLoading, Component };
};
//layout header.tsx
const Header = () => {
....
const url = `${window.APP_CONF?.remoteApp}/portal/remoteEntry.js`;
const scope = 'portal';
const module = './AppMenus'
const { Component: FederatedComponent, errorLoading } = useFederatedComponent(
url,
scope,
module
);
return (
<Navigator logo={<Logo />} menuItems={menuItems} licenseApps={licenseApps} {...props}>
{errorLoading ? (
<WarningOutlined />
) : (
FederatedComponent && (
<React.Suspense fallback={<Spin />}>
{<FederatedComponent {...props} top={64} showBackPortal />}
</React.Suspense>
)
)}
</Navigator>
);
}如何调试
子产品本地环境 → Portal本地环境
Portal与某个资产在不同端口上运行。
Portal无需变更,子产品需进行以下相关文件配置。
在此场景下,remoteApp指向本地启动的Portal项目环境;同时启动项目时需将/partal请求代理至本地环境。
// proxy -> 代理修改
// header 引用的远程地址 -> 修改
window.APP_CONF?.remoteApp = 'http://127.0.0.1:8081'
proxy: {
'/portal': {
target: 'http://127.0.0.1:8081'
}
}子产品本地环境 → Portal服务器环境
本地启动console服务。
服务器上部署Portal对应的Module Federation分支。
与上述情况类似,区别在于此时Portal已部署完成,remote和代理地址只需修改为部署后的Portal地址即可。
// proxy -> 代理修改
// header 引用的远程地址 -> 修改
window.APP_CONF?.remoteApp = 'xxx'
proxy: {
'/portal': {
target: 'xxx'
}
}子产品服务器环境 → Portal服务器环境
子产品与Portal分别部署至服务器环境,修改子产品的config配置,添加window.APP_CONF.remoteApp指向Portal的服务器环境地址。
异常处理
- 当Portal部署不当或版本不对应,导致未暴露AppMenus远程组件时,采取异常处理思路:若请求remoteEntry.js出现error,则不展示AppMenus相关组件。
- 若Portal已部署但其他子产品未接入Module Federation,则不影响子产品的正常展示;子产品当前使用的应为dt-common中的AppMenus。
如何开发AppMenus

问题记录
依赖版本不一致
【Error】Could not find "store" in either the context or props of "Connect(N)". Either wrap the root component in a \<Provider>, or explicitly pass "store" as a prop to "Connect(N)".

报错路径指向portal/xxx,可定位到是AppMunes出现问题。原因是子产品React-Redux与Portal React-Redux版本不一致,需在对应子产品的federationConfig中将react-redux处理为共享依赖。
总结
本文主要从业务层面结合Webpack 5 Module Federation,实现AppMenus的解耦。主要涉及dt-common、Portal及子产品的变更。通过解耦,我们发现AppMenus的开发流程大幅简化,有效提高了工作效率。
以上就是基于Webpack5 Module Federation的业务解耦实践示例的详细内容,更多关于Webpack5 Module Federation业务解耦的资料请关注本站其它相关文章!
JS脚本在App中运行报错怎么解决
2026-11-04 23:11:03
JS怎么使用padStart方法给数字补零
2026-11-04 11:12:03
JS是什么意思?JavaScript全称及基本定义
2026-11-04 08:05:29
JS 使用 toFixed(1) 保留一位小数的基本用法与注意事项
2026-11-03 18:34:57
JS字符串转整数:parseInt与Number的区别及用法
2026-11-03 17:26:54
