起因是,我要在一个大型后台管理应用中剥离出一个管理模块,这样做的好处当然是可以单独开发和发布,但是坏处也有,比如部分全局变量污染,UI不一致,状态通信传导等。虽然市面上已经有很多微前端方案了,比如qiankun等,但是这都需要对这个大仓库应用进行大动工,对原应用造成一些负载和可能意想不到的错误,非我所愿也,所以在这里我打算自己搞个简化的方案。 原应用是react+dva,
基座应用改造
我们设计的主应用核心是;
- 主应用不直接打包子应用的代码,而是运行时按需加载子应用的 JS 资源。
- 将子应用的路由注册到主应用的路由系统中,实现无缝跳转。
- 通过全局变量(
window)共享库,避免重复加载,也可形成单例。 我们先改造子应用路由代码,为了解决刚才说的UI和一些重要库不一致问题,我们将直接暴露库到Window中,有: - antd
- React主库 另外状态通信的问题,我们直接使用dva去修饰取得的子应用Com,并把部分方法如connect暴露到window中。
代码如下
ini
import React from 'react';
import {
Alert,
Drawer,
} from 'antd';
import { connect } from 'dva';
window.React = React;
window.CONNECT = connect;
window.Antd_ZI = {
Drawer,
Alert,
};
const configUrl = {
test: 'https://zi.test.cn',
product: 'https://zi.prod.com',
};
const env = 'product';
const mapStateToProps = (state) => {
const currentState = state.adManage;
const { userAuth, userInfo } = state.app;
return { ...currentState, userAuth, userInfo };
};
const insertScript = (e, isStyle = false) => {
return new Promise((reslove, reject) => {
const i = isStyle ? document.createElement('link') : document.createElement('script');
isStyle || i.setAttribute('type', 'text/javascript');
isStyle || i.setAttribute('src', e);
isStyle && i.setAttribute('href', e);
isStyle && i.setAttribute('rel', 'stylesheet');
isStyle && i.setAttribute('type', 'text/css');
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);
});
};
function ERROR() {
return '加载静态资源失败,请稍后重试';
}
const subappRoutes = [];
const AyncComponent = async (pathname) => {
const id = pathname;
// 子工程资源是否加载完成
let ayncLoaded = false;
if (subappRoutes[id]) {
// 如果已经加载过该子工程的模块,则不再加载,直接取缓存的routes
ayncLoaded = true;
} else if (window.ziPLATFORMSLOT && window.ziPLATFORMSLOT[pathname]) {
const res = await window.ziPLATFORMSLOT[pathname]();
subappRoutes[id] = res.default;
ayncLoaded = true;
// return subappRoutes[id];
} else {
try {
await insertScript(`${configUrl[env]}/js/index.js?_=${new Date().getTime()}`);
if (window.ziPLATFORMSLOT && window.ziPLATFORMSLOT[pathname]) {
const res = await window.ziPLATFORMSLOT[pathname]();
subappRoutes[id] = res.default;
ayncLoaded = true;
}
} catch (error) {
console.log('加载js失败', error);
}
}
return ayncLoaded ? connect(mapStateToProps)(subappRoutes[id]) : ERROR;
};
export default [
{
name: '资源管理',
path: 'ziMedia',
component: () => AyncComponent('ziMedia'),
},
{
name: '黑名单管理',
path: 'ziBlack',
component: () => AyncComponent('ziBlack'),
},
];
子应用改造
子应用中,入口文件我们需要定义publicPath保证资源的正确引用
javascript
const configUrl = {
test: 'https://zi.test.cn',
product: 'https://zi.prod.com',
};
const env = 'product';
// webpack 的魔法变量,用于指定异步加载的 JS/CSS 文件的基地址(如 `import()` 动态导入的模块)
__webpack_public_path__ = `${configUrl[env]}/js/`;
const routes = {
version: '1.0.0',
ziMedia: () => import('./ziMedia/index'),
ziBlack: () => import('./ziBlack/index'),
};
export default routes;
在子应用的页面中,我们基本不需要做改造,但是在webpack的配置中,我们在base中需要:
css
{
...
output:{
...
libraryExport: 'default',
library: 'ziPLATFORMSLOT',
libraryTarget: 'umd',
umdNamedDefine: true,
},
externals: {
'react': 'React',
'antd': 'Antd_ZI',
// 'react-dom':'ReactDOM'
}
}
在prod配置中,为了保证子js能正确加载执行,我们使用InlineChunkHtmlPlugin插件,避免微前端环境下因路径问题导致运行时脚本加载失败。 至此,完成。