qiankun进阶
对于qiankun框架来说已经提供了很多微前端的基础能力,但是在中后台管理系统复杂场景的也有很多,这时候就需要qiankun提供一些进阶的能力来帮助开发者去更好的开发。
提取公共库
提取公共依赖可以参考:qiankun 如何提取出公共的依赖库
- qiankun不建议共享依赖,担心原型链污染等问题。 single-spa推荐共享大型依赖,需要小心处理污染问题,它们都是推荐使用webpack的external来共享依赖库。
- 我们也推荐共享大的公共依赖,也是使用webpack的external来共享依赖库,不过是每个子应用加载时都重复再加载一次库,相当于节省了相同库的下载时间,也保证了不同子应用间不会产生原型链污染,属于折中的方案。
页签切换优化(keep-alive)
在经典的后端管理系统中,打开一个新页面都会对应打开一个页签,切换回上一个打开的页签并保留原页签的缓存能力是较常见的需求。但qiankun方案在自动匹配加载一个子应用时,会卸载上一个子应用,原本的数据也会丢失,无法实现缓存。对于此问题,我们将基于路由自动匹配加载微应用的方式改为调用api手动加载,同时缓存子应用实例及页面,完美解决页签切换过程中数据丢失问题。
本地缓存封装
在qiankun微前端技术方案中,各微应用浏览器的本地存储状态是共享的,存在各个业务微应用本地缓存的key值同名隐患,容易引起各业务微应用操作本地存储时数据覆盖、误删除等问题。为了解决这个问题,我们使用代理模式对浏览器本地缓存进行处理,同时新增缓存api,默认为业务微应用统一加上应用唯一标识作为本地存储的key 值前缀,避免同名问题。
const storageProxy = new Proxy(window.localStorage, {
set: (target: typeof localStorage, prop: PropertyKey, value: any, receiver) => {
if (typeof prop === "string") {
const oldValue = target[prop];
if (!equal(oldValue, value)) {
return Reflect.set(target, prop, value, receiver);
}
}
return false;
},
get: (target, prop, receiver) => {
// 在执行localstorage.setItem方法时,会出现报错
// 发现仅仅一步Reflect.get(target, prop);是不行的
// 所以单独每个方法拿出来重写了一下
if (prop === "setItem") {
return function (key: string, value: any) {
return Reflect.set(target, `${namespace}.${key}`, value);
};
}
return Reflect.get(target, prop, receiver);
},
});
应用通信扩展
qiankun微前端自带的一个实现全局通信的api是initGlobalState,使用不够灵活,在微应用中只能修改已存在的一级属性,并且qiankun会在下一个大版本移除此api。针对此情况,可以扩展新的通信方式,一是通过vuex的方式,在基座主应用维护一个全局的common模块,下发到子应用中,作为各应用可访问的公共状态,实现数据共享;二是使用eventBus的方式,在基座主应用定义并下发eventBus实例,通过发布订阅者模式实现各应用间事件的通信。以上两种方式可以较好的实现项目开发中各种数据通信需求。
子应用嵌套
子应用嵌套即子应用可以嵌入其它子应用。在qiankun中子应用是可以独立运行的,运行后登录页、Layout基础模块包括菜单、注销,还能正常开发和使用。这个时候就需要把登录页、Layout、App三个模块迁移到common模块,通过引入的方式,然后根据window.__POWERED_BY_QIANKUN__判断当前运行环境是否独立运行做相对应的逻辑处理。
子应用并行
子应用并行即子应用同时展示,将当前子应用放到另外一个子应用进行展示,同时展示多次,可能是同一个页面,也可能是不同的页面。
注意子应用共存时,Vue子应用需要使用abstract路由,React使用MemoryRouter
export const router = new VueRouter({
mode: 'hash',
base: process.env.BASE_URL,
routes,
})
export const abstractRouter = new VueRouter({
mode: 'abstract',
base: process.env.BASE_URL,
routes,
})
function render(props?: Prop) {
let container: null | HTMLElement = null
if (props && props.container) {
container = props.container
}
const vueContainer = container ? (container.querySelector('#app') as Element) : '#app'
if (props?.path) {
routerInstance = abstractRouter
} else {
routerInstance = router
}
instance = new Vue({
router: routerInstance,
pinia: createPinia(),
render: (h) => h(App),
}).$mount(vueContainer)
if (props?.path) {
routerInstance.push(props.path)
}
}