背景
由于umi提供了自己的微前端插件plugin-qiankun,而主应用并不是使用umi搭建的,就会涉及主应用直接使用qiankun,而子应用由于入口被umi封装,无法在入口文件导出微前端需要的生命周期,所以只能直接使用plugin-qiankun。
实现方式
- 主应用
componentDidMount() {
this.microApp = loadMicroApp({
name: 'app',
entry: '//localhost:xxxx',
container: this.containerRef.current
});
}
render() {
return ()
}
- 微应用
config配置,umi一键开启qiankun
qiankun: {
slave: {},
}
src/app.ts
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
console.log('app1 bootstrap', props);
},
// 应用 render 之前触发
async mount(props) {
console.log('app1 mount', props);
},
// 应用卸载之后触发
async unmount(props) {
console.log('app1 unmount', props);
},
};
遇到的问题
1、本地开发环境下,可以加载子应用,但是会有一堆dev-server资源加载404(我猜是dev-server需要某个配置,比如publicPath,但是没有找到配置的地方),转换思路,部署以后访问,跳过dev-server。
2、部署完成后,又发现以下问题:
- 访问子应用遇到跨域问题(ngingx配置解决跨域)
server {
listen 8008;
# listen somename:8080;
server_name localhost;
location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
root D:/xxx/xxx;
index index.html index.htm;
}
}
- 子应用打包后的字体文件资源路径错误
解决方案:配置字体文件的publicPath
在config中配置chainWebpack
chainWebpack(config) {
config.module.rule('fonts').use('file-loader').options({
publicPath: process.env.NODE_ENV === 'production' ? 'http://localhost:xxxx/' : '/', // 给字体文件指定文件加载路径
name: 'static/[name].[hash:8].[ext]',
esModule: false
});
// console.log(config.toString())
},
这里有个技巧,如果不熟悉chainWebpack的写法,可以用console.log(config.toString())在终端打印出webpack一样的配置作为配置参考
-
父子应用的样式冲突
贴一张qiankun关于样式冲突解决方案的图,仿照着做就好了。
umi的config中有这样的一个配置theme,上面提到的@ant-prefix设置可以放在此处,如果你不放心,依然可以使用console.log(config.toString())验证。
theme: {
// ...darkTheme,
'primary-color': defaultSettings.primaryColor,
'@dt-prefix': myPackage.name,
},
然后正确设置ConfigProvider
const wrapApp = (props) => {
return (
{props?.children}
);
}
所有的页面都通过wrapApp包裹,路由如下配置:
{
path: '/',
component: '../layouts/wrap',
routes: [
{ path: '/', redirect: '/layouts' },
{
path: '/layouts',
component: './xxx',
}
]
}
自此,所有的问题都得以解决。
主应用与微应用的通信问题
官方示例
主应用:
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
微应用:
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
props.setGlobalState(state);
}
实践
实际使用中会涉及到几个问题(问题都在子应用中):
1、props.setGlobalState方法该存放在哪?
首先我们要与主应用通信,需要调用这个方法,一开始我想的都是放到状态管理dva的model中,但是现实很残酷,在mount生命周期执行的时候根本还拿不到dva的实例,所以存到dva是不现实的,所以最后我是放到一个变量中,分别定义了该变量的get和set方法。
2、主应用传过来的state存放在哪?
因为我们要根据state的改变来刷新页面,所以这个state必须放到dva中,好在props.onGlobalStateChange监听的回调中是能拿到dva实例的,所以我们找到dispatch方法就可以更新state
放代码
app.ts
import { setPageStatus, setSetGlobalState, resetDispatch } from '@/utils/signalCommunication'
const myPackage = require('../package.json')
export const qiankun = {
// 应用加载之前
async bootstrap() {
// eslint-disable-next-line no-console
console.log(`${myPackage.name} bootstrap`)
},
// 应用 render 之前触发
async mount(props) {
// eslint-disable-next-line no-console
console.log(`${myPackage.name} mount`)
props.onGlobalStateChange((state) => {
// state: 变更后的状态; prev 变更前的状态
if (state.type !== 'default') { // default专门给主应用使用
setPageStatus(state.type)
}
});
setSetGlobalState(props.setGlobalState)
},
// 应用卸载之后触发
async unmount() {
// eslint-disable-next-line no-console
console.log(`${myPackage.name} unmount`)
resetDispatch()
},
}
utils/signalCommunication.ts
import { getDvaApp } from 'umi';
let dispatch
let setGlobalState // 存放setGlobalState方法
const getDispatch = () => {
if (dispatch) {
return dispatch
}
const app: any = getDvaApp()
// eslint-disable-next-line no-underscore-dangle
dispatch = app ? app._store?.dispatch : null
return dispatch
}
// 微应用卸载的时候需要去重置dispatch,不然再次挂载拿不到真实的dva实例
export const resetDispatch = () => {
dispatch = null
}
export const setPageStatus = (payload) => {
const dispatch = getDispatch()
if (dispatch) {
dispatch({ type: 'global/setPageStatus', payload })
}
}
export const getSetGlobalState = () => {
return setGlobalState
}
export const setSetGlobalState = (func) => {
setGlobalState = func
}