一、微前端落地的老难题:依赖共享的坑
1.1 为什么容器和子应用的依赖会打架
很多团队做大型前端项目时,会用微前端把不同业务拆成多个子应用,容器应用负责统一调度。比如商品团队做商品子应用,用户团队做用户子应用,都用到了lodash这个工具库。本来共享依赖是为了减少重复加载,提升性能,结果经常出问题:比如商品子应用用了lodash 1.2版本,排序功能没问题;用户子应用偷偷升级到了1.3版本,里面改了排序的API,结果两个应用一起加载时,商品列表的排序就乱了,甚至报错。还有一种情况,比如订单子应用需要先加载用户子应用的用户信息模块才能渲染,结果容器加载时先加载了订单,再加载用户,页面就显示“用户信息不存在”,这些都是常见的依赖共享坑。
1.2 常见的两个核心坑:版本冲突、加载时序
版本冲突就是不同应用用了同一个依赖的不同版本,运行时互相干扰;加载时序就是不同模块的加载顺序不对,导致依赖的模块没加载完就用,出现undefined或者报错。这些问题之前很多团队都是在运行时靠代码处理,比如动态加载、版本判断,但这样不仅代码乱,还容易漏,上线后才发现问题,代价很大。
二、用Webpack配置提前堵上这些坑(核心解决方法)
2.1 先搞懂Module Federation的依赖共享逻辑
Module Federation就像公司里的共享茶水间,大家可以一起用冰箱、微波炉这些公共物品。但如果每个人带的冰箱型号不一样,或者有人先占了微波炉,后面的人就用不了,这就是之前的问题。而Webpack的配置阶段,就是大家提前坐下来开个会,说好共享的规则,比如“只能用同一型号的冰箱”、“谁先用微波炉谁先登记”,这样从一开始就避免冲突。
2.2 配置阶段的第一个规避:锁定共享依赖的版本
在Webpack的Module Federation配置里,只要在shared项里设置singleton为true,还指定版本,就能保证所有应用都用同一个版本的依赖。比如如果容器用了lodash 1.4.1,子应用也必须用这个版本,不然Webpack构建时就直接报错,不用到运行时才查。
2.3 配置阶段的第二个规避:解决加载时序的“同步/异步”选择
在remotes项里,每个子应用的地址可以指定加载时机,比如设成async或者sync,或者在代码里控制异步加载的顺序,就能避免子应用还没加载完就用的问题。比如订单应用必须等用户应用加载完,就可以在容器的remotes里设置订单应用依赖用户应用,或者在代码里先导入用户应用的模块,再导入订单应用。
2.4 完整的配置示例(单一技术栈:Webpack5 + JavaScript)
这里给两个完整的配置,一个是容器应用,一个是子应用,带详细注释,一看就懂。 首先是容器应用的Webpack配置:
// 容器应用的Webpack5配置,技术栈:Webpack5 + JavaScript
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const packageJson = require('./package.json');
module.exports = {
mode: 'development', // 开发模式,生产环境换成production
devServer: {
port: 3000, // 容器应用运行的端口,方便本地调试
},
plugins: [
new ModuleFederationPlugin({
name: 'containerApp', // 容器应用的唯一标识,其他子应用用来引用它的模块
remotes: {
// 引用用户子应用,指定它的运行地址和暴露的入口文件(webpack自动生成的)
userApp: 'userApp@http://localhost:3001/remoteEntry.js',
},
shared: {
// 核心:共享依赖的规则,解决版本冲突和单例问题
lodash: {
singleton: true, // 只加载一次lodash,避免不同版本共存
version: packageJson.dependencies.lodash, // 锁定lodash版本为容器应用里的版本
eager: false, // 异步加载,需要用到才加载,不占首屏资源
},
react: {
singleton: true,
version: packageJson.dependencies.react,
requiredVersion: packageJson.dependencies.react, // 要求其他应用的react必须和容器版本一致,不一致构建报错
},
},
}),
],
};
然后是用户子应用的Webpack配置,和容器的规则对齐:
// 用户子应用的Webpack5配置,技术栈:Webpack5 + JavaScript
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const packageJson = require('./package.json');
module.exports = {
mode: 'development',
devServer: {
port: 3001, // 子应用的端口,和容器配置里的地址对应
},
plugins: [
new ModuleFederationPlugin({
name: 'userApp', // 子应用的唯一标识,给容器应用引用
exposes: {
// 暴露子应用的模块,容器可以直接用,这里暴露了用户信息组件
'./UserInfo': './src/components/UserInfo.js',
},
shared: {
// 和容器的共享规则完全对齐,保证版本一致
lodash: {
singleton: true,
version: packageJson.dependencies.lodash,
requiredVersion: packageJson.dependencies.lodash,
},
react: {
singleton: true,
version: packageJson.dependencies.react,
requiredVersion: packageJson.dependencies.react,
},
},
}),
],
};
示例里的关键是singleton和requiredVersion,这两个参数是核心,很多新手配置时只写shared,没设这两个,还是会出问题。
三、这样做的应用场景、优缺点和注意事项
3.1 什么场景下一定要用这个配置技巧
适合大型前端项目,尤其是多个团队并行开发微前端的情况:比如电商平台的商品、用户、订单三个子应用,都用到了lodash、react等通用库,而且经常需要迭代,之前版本混乱,每次上线都出依赖冲突的bug;还有就是子应用之间有明确的依赖顺序,比如订单依赖用户,商品依赖订单,加载时如果顺序乱就会报错,这个配置就能提前解决。另外,刚入门微前端的团队,也可以用这个方法快速规避常见的坑,不用踩runtime的坑。
3.2 这种方式的优缺点
优点很明显:首先,提前在构建阶段就发现问题,比如版本不一致,Webpack会直接给报错信息,不用到生产环境才发现,调试起来省很多时间;其次,singleton模式减少了依赖的重复打包,原来每个子应用都打包一份react,现在只打包一次,页面加载速度更快,体积更小;最后,配置简单,只要对齐各个应用的shared规则,不用改太多业务代码,适合快速落地。
缺点也有:比如灵活性降低,如果你某个子应用需要单独升级依赖的版本,其他应用没跟上,就会报错,需要整体协调升级节奏;还有,对于一些本身不支持单例的库,比如一些复杂的UI组件库,设成singleton可能会出问题,需要测试后再用。
3.3 配置时的几个关键注意点
第一个,不要随便给所有依赖都设singleton,比如业务自己写的通用工具库,可以设,但第三方库如果有特殊的加载要求,就不要随便设;第二个,requiredVersion的范围要合理,不要写死成固定版本,比如可以写^1.4.1,允许小版本升级,不用每次子应用都同步;第三个,eager参数,只有首屏需要用到的依赖才设成true,其他都设成false,避免增加首屏加载时间;第四个,要测试不同应用一起加载的情况,比如本地同时启动容器和两个子应用,看有没有报错,确保配置生效。
四、总结
微前端落地时的依赖共享问题,不是只能在运行时处理,通过Webpack5的Module Federation配置阶段,设置好shared里的singleton、requiredVersion等参数,就能提前规避版本冲突和加载时序的坑。这个方法不需要改太多业务代码,只要统一各个应用的配置规则,就能让微前端的依赖管理更稳定,适合大型团队的协作开发,减少上线后的故障,提升开发效率。
评论
围绕“微前端落地时依赖共享依旧棘手,Module Federation里容器应用与子应用之间的版本冲突、加载时序难题,都能在Webpack构建配置阶段提前规避”参与讨论