多个子应用并行加载,子应用嵌套
- 同一个基座并行加载两个或者多个子应用
可以使用loadMicroApp加载多个子应用
2. 多路由系统共存带来的 冲突/抢占 问题如何解决?
let historyPath=window.location.pathname.startWith('/vue1/')?process.env.BASE_URL+'/vue1/':process.env.BASE_URL const router = createRouter({ history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? historyPath : process.env.BASE_URL), // history: createWebHashHistory(), routes: constantRoutes, })
同时调用start
和loadMicroApp
会导致子应用render两次,虽然对页面结构和样式没有影响,但是接口都会调用两次,所以在跳出子应用的时候一定要loadMicroApp.unmount()
卸载不需要的子应用
qiankun代码入口函数封装
import type { MicroAppStateActions } from 'qiankun'; import QiankunBridge from '@/qiankun/qiankun-bridge' import { initGlobalState, registerMicroApps, start, loadMicroApp, addGlobalUncaughtErrorHandler, runAfterFirstMounted } from 'qiankun'; import { getAppStatus, unloadApplication } from 'single-spa'; export default class Qiankun { private actions: MicroAppStateActions | null = null private appMap: any = {} private prefetchAppMap: any = {} init() { this.registerApps() this.initialState() this.prefetchApp(); this.errorHandler(); } registerApps(){ const parentRoute = useRouter(); registerMicroApps([{ name: 'demo-vue', entry: `${publicPath}/demo-vue`, container: `#demo-vue`, activeRule: `${publicPath}${'demo-vue'}`, props: { parentRoute, QiankunBridge:new QiankunBridge() } }, ...]); } initialState(){ const initialState = {}; // 初始化 state this.actions = initGlobalState(initialState); this.actions.onGlobalStateChange((state, prev) => { // state: 变更后的状态; prev 变更前的状态 console.log(state, prev); }); } setState(state: any) { this.actions?.setGlobalState(state); } //预加载 prefetchApp() { start({ prefetch: 'all', singular: false, }); } //按需加载 demandLoading(apps){ let installAppMap = { ...store.getters["tabs/installAppMap"], }; if (!installAppMap[config.name]) { installAppMap[config.name] = loadMicroApp({ ...config, configuration: { // singular: true sandbox: { experimentalStyleIsolation: true }, }, props: { getGlobalState: actions.getGlobalState, fn: { parentRoute: useRouter(), qiankunBridge: qiankunBridge, }, }, }); } } /** * @description: 卸载app * @param {Object} app 卸载微应用name, entry * @returns false */ async unloadApp(app) { // await clearCatchByUrl(getPrefetchAppList(addVisitedRoute, router)[0]) const appStatus = getAppStatus('utcus'); if (appStatus !== 'NOT_LOADED') { unloadApplication(app.name); // 调用unloadApplication时,Single-spa将执行以下步骤。 // 在要卸载的注册应用程序上调用卸载生命周期。 // 将应用程序状态设置为NOT_LOADED // 触发重新路由,在此期间,单spa可能会挂载刚刚卸载的应用程序。 // 由于unloadApplication调用时可能会挂载已注册的应用程序,因此您可以指定是要立即卸载还是要等待直到不再挂载该应用程序。这是通过该waitForUnmount选项完成的。 } } //重新加载微应用 reloadApp(app) { this.unloadApp(app).then(() => { loadMicroApp(app); }); } //加载单个app loadSingleApp(name) { if (!this.appMap[name]) { this.appMap[name] = loadMicroApp(this.prefetchAppMap[name]); } } // 切出单个app,和unloadApp用法不同unloadApp 是卸载start方法生成的应用,unmountSingleApp是卸载loadMicroApp方法生成的应用 async unmountSingleApp(name) { if (this.appMap[name]) { await this.appMap[name].unmount(); this.appMap[name] = null; } } //错误处理 errorHandler() { addGlobalUncaughtErrorHandler((event: any) => { console.log('addGlobalUncaughtErrorHandler', event); if ( event?.message && event?.message.includes('died in status LOADING_SOURCE_CODE') ) { Message('子应用加载失败,请检查应用是否运行', 'error', false); } //子应用发版更新后,原有的js会找不到,所以会报错 if (event?.message && event?.message.includes("Unexpected token '<'")) { Message('检测到项目更新,请刷新页面', 'error', false); } }); } }
应用事件通信
应用场景:子应用a调用子应用b的事件
const isDuplicate = function isDuplicate(keys: string[], key: string) { return keys.includes(key); }; export default class QiankunBridge { private handlerMap: any = {} // 单例判断 static hasInstance = () => !!(window as any).$qiankunBridge constructor() { if (!QiankunBridge.hasInstance()) { ; (window as any).$qiankunBridge = this; } else { return (window as any).$qiankunBridge; } } //注册 registerHandlers(handlers: any) { const registeredHandlerKeys = Object.keys(this.handlerMap); Object.keys(handlers).forEach((key) => { const handler = handlers[key]; if (isDuplicate(registeredHandlerKeys, key)) { console.warn(`注册失败,事件 '${key}' 注册已注册`); } else { this.handlerMap = { ...this.handlerMap, [key]: { key, handler, }, }; console.log(`事件 '${key}' 注册成功`); } }); return true; } removeHandlers(handlerKeys: string[]) { handlerKeys.forEach((key) => { delete this.handlerMap[key]; }); return true; } // 获取某个事件 getHandler(key: string) { const target = this.handlerMap[key]; const errMsg = `事件 '${key}' 没注册过`; if (!target) { console.error(errMsg); } return ( (target && target.handler) || (() => { console.error(errMsg); }) ); } }
子应用a注册
import React from "react"; export async function mount(props) { if (!instance) { React.$qiankunBridge = props.qiankunBridge; render(props); } } React.$qiankunBridge && React.$qiankunBridge.registerHandlers({ event1: event1Fn });
子应用b调用
Vue.$qiankunBridge.getHandler('event1')
项目部署,提示用户更新系统
插件在注册 serviceWorker
时判断 registration
的 waiting
状态, 从而判定 serviceWorker
是否存在新版本, 再执行对应的更新操作, 也就是弹窗提示; 有个弊端就是项目有可能需要经常发版改一些bug,导致更新弹窗频繁出现,所以只能弃用。
对于主项目简单一点的方案就是把版本号数据写入cookie里,通过webpack生成一个json文件部署到服务器,前端代码请求到json文件的版本号做对比,如果不是最新版就弹窗系统更新弹窗。
子项目更新的话,js,css资源会重新编译生成新的链接,所以请求不到, addGlobalUncaughtErrorHandler
监听到资源请求错误,直接提示更新弹窗就好
addGlobalUncaughtErrorHandler((event: any) => { //子应用发版更新后,原有的文件会找不到,所以会报错 if (event?.message && event?.message.includes("Unexpected token '<'")) { Message('检测到项目更新,请刷新页面', 'error', false); } });
主应用组件重新渲染导致子应用dom消失
问题出现场景:ipad移动端切到宽屏,布局发生变价,vue组件重新渲染导致微服务里面的dom消失 。
解决方案:用single-spa
的unloadApplication
方法卸载子应用,用qiankun
的loadMicroApp
方法重新加载该子应用。
详见上文的 reloadApp
方法
切换应用后原有子应用路由变化监听失效
function render(props = {}) { const { container } = props router=Router() //添加此行代码,react子应用清空下dom重新调用下ReactDOM.render即可 instance = createApp(App) instance .use(router) .mount( container ? container.querySelector('#app-vue') : '#app-vue' ) }