原由
我们公司有一个运营系统,粗略估计有30多个互不关联的业务系统在上面开发。代码量很大,本地开发时启动会把一个配置不差的电脑卡成半死,打包只能在打包机上,本地打包不敢想象。而且这么多业务系统代码管理上面也是一个巨大的工程,同一个发布日可能有四五个业务线同时发布,所以每次发布验收的时候时间相对较晚,因为要等其他人发布。所以在这种情况下就想到了用微前端的方式将对应的业务系统代码从这个臃肿的系统中抽离出来
技术选型
原有代码是运用dva进行开发的,不存在技术栈不一致的情况,所以用不到SingleSpa之类的整合不同技术栈的框架。然后这个项目有用到dva的异步加载功能,于是最后只是改变了页面文件的加载逻辑。
具体实现
如上图所示,将子业务系统代码从原有代码中抽离出来单独打包,为减少子系统与父系统的代码冗余,子系统使用了external抽离公共模块(react、antd、dva等),父系统需引用这些模块的cdn资源,或者将这些资源提前导出到windows中
// 每个子模块按照所需的设置
externals: {
'react': 'react',
'antd': 'antd',
'dva': 'dva',
...
},
为了方便父系统找到每个页面对应的组件静态资源,子系统打包时用umd的方式。这样当父系统引用每个子系统的入口文件时可以在windows中找到对应名字的组件
// 子模块webpack
output: {
path: xxx,
filename: '[name].[chunkhash:8].js',
chunkFilename: 'js/[name].[chunkhash:8].chunk.js',
libraryTarget: 'umd',
library: xxx, //模块的名称
},
子系统入口文件
// 这里可以设置publicPath,在上面可以判断一下js运行环境赋予对应的publicPath
__webpack_public_path__ = 'xxxxxx';
export default {
version: '1.0.0',
// 子系统的页面文件或者model文件
moudle: () => import('xxxx'),
};
父系统中导入
// 插入js
const insertScript = (e) => {
return new Promise((reslove, reject) => {
const i = document.createElement('script');
i.setAttribute('type', 'text/javascript');
i.setAttribute('src', e);
function onload() {
if (!(this.readyState && this.readyState !== 'loaded' && this.readyState !== 'complete')) {
i.onload = null;
i.onreadystatechange = i.onload;
reslove();
}
}
function onerror(ee) {
reject(ee);
}
i.onreadystatechange = onload;
i.onload = onload;
i.onerror = onerror;
document.querySelector('head').appendChild(i);
});
};
// 组件缓存
const subappRoutes = [];
// 每个子业务系统对应的libraryName
const library = window['xxx'];
// 每个子业务系统对应的入口文件地址,可以在服务端配置
const url = 'xxx'
const AsyncComponent = async (pathname) => {
const id = pathname;
// 子工程资源是否加载完成
let asyncLoaded = false;
if (subappRoutes[id]) {
// 如果已经加载过该子工程的模块,则不再加载,直接取缓存的routes
ayncLoaded = true;
} else if (library && library[pathname]) {
// 加载对应的组件
const res = await library[pathname]();
subappRoutes[id] = res.default;
asyncLoaded = true;
} else {
try {
await insertScript(url);
if (library && library[pathname]) {
const res = await library[pathname]();
subappRoutes[id] = res.default;
asyncLoaded = true;
}
} catch (error) {
// 错误处理
}
}
return asyncLoaded ? subappRoutes[id] : ERROR;
};
export default [
{
name: 'xxx',
path: 'xxx',
models: () => AsyncComponent('xxx'),
component: () => AsyncComponent('xxx'),
},
...
];
总结
上面的操作就可以将子业务系统从父系统中抽离出来,和SingleSpa优点是这样可以不用引用额外的框架,除了路由引入方式需要修改、子系统需要额外打包以外没有更多的代码开发,减少了工作量。而且抽离出来后开发还是和以前一样。缺点就是每次开发需要启动两个项目,子系统不能脱离父系统而单独存在