一、前言
很荣幸由我设计并实现了公司自研的微前端框架,目前该框架已在多个业务项目中广泛应用。
本文是我结合内部培训、产品文档及相关实践梳理而成的总结,旨在对微前端技术体系进行系统性的整理与沉淀。
本篇是系列文章的第二篇,分享一些关于微前端的封装和实践。
二、产品简介
XX微前端是基于qiankun和公司权限管理系统实现的微前端架构解决方案,旨在帮助大家能更简单、无痛地构建一个生产可用微前端架构系统。
它在XX基础模板的基础上,兼容适配了XX已有的用户认证、权限控制、埋点功能,并集成了一系列微前端能力,包括应用加载、应用通信、应用隔离、应用缓存等。

三、核心封装与实现
3.1 应用缓存
在主应用中使用v-show指令控制子应用容器,以实现子应用的切换与缓存。
xml
<!-- 子应用容器 -->
<template v-for="item in apps">
<div
class="app-container"
v-show="currentAppName && currentAppName === item.name"
:id="`container-${item.name}`"
:key="item.name"
></div>
</template>
3.2 应用加载
采用qiankun的loadMicroApp实现多标签页场景下的应用加载。

核心逻辑说明:监听路由变化=》根据路由匹配子应用=》loadMicroApp手动加载子应用,给挂载到指定容器。
vue
// 1. 监听路由变化
watch: {
$route: {
immediate: false,
handler(route) {
this.openMicroApp(route);
}
}
},
// 2. 根据路由信息获取菜单数据及子应用信息
const resource = getMenuByRoute(route);
subApp = resource && this.getAppById(resource.rootResourceId);
// 2.1 菜单资源数据映射关系
{
id: item.resourceId, // web应用资源id
name: item.resourceCode, // web应用资源编码
entry: item.resourceUrl // web应用基础路径
}
// 3. 调用loadMicroApp加载子应用
const { name, entry } = subApp;
const appInstances = loadMicroApp({
name,
entry,
container: `#container-${name}`,
props: {}
});
以上代码是简化后的示意代码,实际实现中还包含有对容器节点、路由、loading等细节的处理。
3.3 应用通信
3.3.1 基于props
qiankun的loadMicroApp 方法支持通过props配置选项,传递数据或者方法给子应用。
javascript
// 1. 主应用定义并通过props.data传递
const mainAppName = 'app-main'
const appInstances = loadMicroApp({
name,
entry,
container: `#container-${name}`,
props: {
data: {
mainAppName
}
}
});
// 2. 子应用接收
export async function mount(props) {
render(props);
}
function render(props) {
const { data = {}, container } = props || {};
console.log(data.mainAppName)
}
3.3.2 基于window
通过挂载全局属性和方法,供其他应用使用。
scss
// 1. 主应用定义
window.$mainAxxxxxxx = initAxxxxxxx(router);
// 2. 子应用通过window调用
window.$mainAxxxxxxx.xxx();
3.3.3 基于事件
框架选用Node.js的EventEmitter,由主应用集成并对外提供event-bus基于事件总线的应用间通信能力。包含事件的注册、派发和移除功能。
javascript
// 1. 主应用实例化事件对象
import { EventEmitter } from 'events';
const eventBus = new EventEmitter();
// 2. 监听事件
eventBus.on('event1', (params1, params2) => {
// ...do some things
});
// 3. 传递实例给子应用
loadMicroApp({
props: {
data: {
eventBus
}
}
});
// 4. 子应用接收并派发事件
function render(props) {
const { data = {}, container } = props || {};
if (data.eventBus) {
data.eventBus.emit('event1', params1, params2);
}
}
// 5. 子应用也可以 监听其他应用派发的事件
data.eventBus.on('xxx',xxx)
注意事项:
- 在绝大多数情况下,不鼓励使用全局的事件总线在组件之间进行通信。虽然在短期内往往是最简单的解决方案,但从长期来看,它维护起来总是令人头疼。可以用,但不建议大量使用。
- 删除在其他地方添加的监听器是不好的做法,特别是当
EventEmitter实例是由其他组件或模块(例如套接字或文件流)创建时。存在较大风险隐患。
3.3.4 基于vuex
通过共享主应用store, 可以快速将基座的vuex注册到微应用自己的vuex实例上。
kotlin
// 1. 主应用定义并通过props传递store
import qiankunCommonStore from '@/store/modules/common';
loadMicroApp({
name,
entry,
container: `#container-${name}`,
props: {
data: {
store: qiankunCommonStore,
}
}
});
// 2. 子应用store注册到自己的vuex实例上
function render(props) {
const { data = {}, container } = props || {};
if (store && store.hasModule && data.store) {
store.registerModule('mainAppStore', data.store);
}
}
// 3.1 使用:取值
const userInfo = this.$store.state.mainAppStore.userInfo
// 3.2 使用:赋值
this.$store.commit('mainAppStore/setUserInfo', { ...this.userInfo, newData: 'home-new-data' });
3.3.5 基于initGlobalState
qiankun提供的基于全局状态的通信方式。模板中未集成,如需使用,可参看qiankun文档自行扩展。
未集成的原因是,作者并不推荐该API并计划在未来版本中移除。
globalState 不是合理的微前端通信方案,会加剧应用之间的耦合.。 -- qiankun官方issues
3.4 应用跳转
3.4.1 应用内跳转
调用自身路由router的方法
javascript
onJump(path) {
this.$router.push({
path,
query
});
},
3.4.2 应用间跳转
使用主应用路由方法
javascript
onJumpOther(path) {
this.$root.mainAppRouter.push({
path,
query
});
},
3.5 应用隔离
3.5.1 js沙箱
众所周知javaScript的全局作用域存在命名冲突、变量污染等风险,为了解决该问题,qiankun借助 ES6 的 proxy ,创建js代理沙箱,实现了应用间环境的隔离。
qiankun已有实现,默认为开启状态。
参考资料:
3.5.2 样式隔离
为了确保不同组件或模块之间的样式不会相互影响,从而提高代码的可维护性和可复用性。qiankun提供有两种样式隔离方案,strictStyleIsolation和experimentalStyleIsolation。
qiankun默认开启strictStyleIsolation严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。
参考资料:
3.5.3 本地存储隔离
为了防止各微应用间操作本地存储时,数据覆盖、误删除等问题,框架封装并集成了本地存储功能模块。原理是在基座运行时,覆写localStorage、sessionStorage方法,给本地存储的key 统一加上前缀。
我们对此封装了一个组件,代码 和API就不赘述了,这里仅分享,在去覆写的时候遇到的一个卡点问题和解决方案。
问题描述:修改Storage接口的原型方法后,乾坤无法隔离, 导致所有应用(包括主应用)的相关方法均被修改。
解决方案:在引入qiankun之前删除window.localStorage,随后再重新声明,此时的window.localStorage是隔离的,在主子应用分别定义,互不影响。
javascript
// 主应用
window.originStorage = window.localStorage;
delete window.localStorage;
window.localStorage = {
getItem(...ags) {
console.log("this", this);
return window.originStorage.getItem(...ags);
},
};
// 子应用
window.localStorage = {
getItem(key, ...ags) {
console.log("insub");
return window.originStorage.getItem(`app-vue${key}`, ...ags);
},
};
原理是qiankun的沙箱(proxySandBox),会从window对象拷贝不可配置的属性并filter掉,不走proxy代理,比如location、localStorage等。现在咱们在qiankun沙箱逻辑执行前,把localStorage属性移除,它拷贝不到,后面咱们再加上的localStorage就会走proxy然后被隔离起来。
3.6 权限控制
整体策略是,各微应用分别进行权限控制,谁的页面谁管控。
在微前端场景下,由于各微应用的路由监听的是同一url,当url切换时,所有微应用的路由守卫都会触发。需要对不是自己应用路由放行。
vbnet
// 微前端运行时的路由守卫
function microAppHook(to, from, next){
const isAppNameMatched = to.meta.appName ? to.meta.appName === appName : true,
isNotMyRoute = !to.path || !to.name || !isAppNameMatched;
if (isNotMyRoute) {
// 非当前应用路由,跳过处理
next();
return;
}
// 权限控制:白名单、角色菜单权限判断
if (hasPermission(to) || hasPermission(to, false)) {
next();
}else{
next({path: '/404', replace: true});
}
}
四、后续改进
-
性能提升
- 内存占用高:在低版本火狐浏览器内存占用经常性超过2G,页面卡顿明显
- 加载速度慢:超过5M的子应用资源,请求和加载长达3-5秒
-
简化路由、菜单资源的处理
- 当前关于菜单资源的处理较复杂且不灵活,有较大提升空间