前言
本文主要是对于qiankun微前端框架的简单体验,并且对于其微应用挂载、应用通信、应用隔离等方面的技术实现进行了了解,方便日后对巨石应用进行微前端改造,代码demo在这里 qiankun_experience。
实践过程
创建主应用
当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子
const AppsInfo = [
{
name: 'react-app', // app name registered
entry: '//localhost:3000',
container: '#app-qiankun',
activeRule: '/react',
},
{
name: 'vue-app', // app name registered
entry: '//localhost:3001',
container: '#app-qiankun',
activeRule: '/vue',
}
]
创建微应用
微应用主要是需要暴露出三个生命周期函数,bootstrap用于标识初始化应用,然后mount和unmount用于挂载和卸载微应用。然后按照官网修改打包方式为umd,并更改打包
同时为了dev环境下无障碍还需要解决跨域问题,如果是基于webpack打包的话,只需要在DevServer里增加允许跨域的header即可。
然后为了路由兼容,还需要在微应用的router控制中,增加一个baseURL(对应activeRule)来实现匹配到activeRule之后,路由由子应用进行接管
# header配置
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
},
}
# router控制
## react-router-dom
<BrowserRouter basename={baseURL} >
</BrowserRouter>
## vueRouter
new VueRouter({
base: baseURL,
routes,
mode: 'history',
})
这里还要注意一个细节,如果报这样的错误
Application died in status NOT_MOUNTED: Target container with #container not existed after xxx mounted!
说明主应用和子应用如果挂载时,render函数选择的挂载节点是相同的,官方给出的解决方案是修改id查询范围,
技术实现
应用的隔离和切换
这里对应到上边说的主子应用路由配置的问题,当匹配到子应用的路由时,加载子应用的内容,然后将路由控制交给子应用(activeWhen如果匹配成功再卸载当前应用,挂载新应用) qiankun在这里直接使用了社区技术方案 Single-SPA解决这个问题,这里我们去看一下single-SPA的关于这部分的重点代码: 主要有两个部分:
- 通过劫持路由事件,来调用reroute方法
- 在reroute方法中对APP进行调度,判断根据路由判断shouldActive,然后进行load和mount,并对部分app进行unmounted
# 1. 劫持路由变化事件 如hashchange pushstate等,监听后执行reroute
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
.......
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
.......
# 2、 reroute函数,主要调度函数,start()后执行的函数
function reroute() {
...
if (isStarted()) { // 如果主应用开启
appChangeUnderway = true;
appsThatChanged = appsToUnload.concat(appsToLoad, appsToUnmount, appsToMount);
return performAppChanges(); // 展示应用
} else {
appsThatChanged = appsToLoad;
return loadApps(); // 加载应用
}
function loadApps() { // 应用加载
......
}
function performAppChanges() {
return Promise.resolve().then(function () {
......
var unloadPromises = appsToUnload.map(toUnloadPromise);
var unmountUnloadPromises = appsToUnmount.map(toUnmountPromise).map(function (unmountPromise) {
return unmountPromise.then(toUnloadPromise);
});
var allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
var unmountAllPromise = Promise.all(allUnmountPromises);
var loadThenMountPromises = appsToLoad.map(function (app) {
return toLoadPromise(app).then(function (app) {
return tryToBootstrapAndMount(app, unmountAllPromise);
// 遍历所有需要load的APP,查看是否有activeRule能够匹配,能匹配则load
});
});
var mountPromises = appsToMount.filter(function (appToMount) {
return appsToLoad.indexOf(appToMount) < 0;
}).map(function (appToMount) {
return tryToBootstrapAndMount(appToMount, unmountAllPromise);
// 遍历所有需要挂载的APP,
});
return unmountAllPromise.catch(function (err) { // 卸载APP
callAllEventListeners();
throw err;
}).then(function () {
......
});
});
}
}
主子应用隔离
样式隔离
一提到样式隔离,我们其实马上就可以想到使用web Component,开启shadow Dom,这种方案完全样式隔离,但是存在很大的局限性,因为目前大部分组件库对于弹窗类的组件都是挂在document.body下的,这样就导致微应用的弹窗溢出到主应用中从而丢失样式。
这里就需要了解到qiankun获取资源列表是通过运行时获取用html entry,通过import-html-entry拉取应用,子应用项目通过html为入口,运行自己内部的css,js
所以更好的是使用别的方法,qiankun对于微应用之间的样式隔离采用的是动态样式表的方式,所以对于不同的微应用qiankun只需要在微应用容器中卸载旧的html,挂载新的html,就可以自动实现单例微应用间的样式隔离。 但是这样还是没有解决主子应用的样式隔离,这里一般还是采用工程化的思路,给微应用的class增加前缀后缀,或者使用属性选择器的方式,这块就需要自己团队进行实践了。
js隔离
qiankun对于js隔离采用了两种方式
- 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
- 基于 Proxy 实现的沙箱
基于diff快照实现的方式,实际上都是在
window上进行的操作,只是每个沙箱都有一个mount时的快照windowSnapshot和一份修改记录modifyPropsMap, 在激活时windowSnapshot记录下当前window的全部状态,然后将之前的modifyPropsMap在window上进行更新 在退出的时候将修改在modifyPropsMap中进行记录,通过windowSnapshot对window进行还原
key: "active",
value: function active() {
var _this = this;
// 记录当前快照
this.windowSnapshot = {};
iter(window, function (prop) {
_this.windowSnapshot[prop] = window[prop];
}); // 恢复之前的变更
Object.keys(this.modifyPropsMap).forEach(function (p) {
window[p] = _this.modifyPropsMap[p];
});
this.sandboxRunning = true;
}
}, {
key: "inactive",
value: function inactive() {
var _this2 = this;
this.modifyPropsMap = {};
iter(window, function (prop) {
if (window[prop] !== _this2.windowSnapshot[prop]) {
// 记录变更,恢复环境
_this2.modifyPropsMap[prop] = window[prop];
window[prop] = _this2.windowSnapshot[prop];
}
});
this.sandboxRunning = false;
}
}]);
基于proxy实现的沙箱,是通过复制一份fakewindow,然后通过proxy设置set和get,让fakewindow只存在于对应的微应用下,每个微应用下一份全局变量,这样就完全避免了变量污染,代码大意如下
var _createFakeWindow = createFakeWindow(rawWindow),
fakeWindow = _createFakeWindow.fakeWindow,
var proxy = new Proxy(fakeWindow, {
set: function set(target, p, value) {
1. 判断是否在激活状态
2. 是否可写
3. 将更新添加到更新Map中
……
return true;
}
return true;
},
get: function get(target, p) {
...
}
微应用之间如何共享数据
官方提供了基于发布订阅模式的action操作,在主应用中对state进行注册,然后自动注入到微应用中,但是并没有getGlobalState,并且qiankun源码对onGlobalStateChange做了限制,每个应用只能有一个onGlobalStateChange,所以个人感觉这个全局状态管理API并不是特别好用。
个人尝试的时候还是使用了redux,一方面没有技术栈限制,另一方面可以以更小细粒度管理状态,且subscribe没有个数限制,只需要在props里传入一个store即可,然后在微应用中获取并使用。当然本质上还是通过主应用对数据进行下发的。
# 主应用
const initState = {
...
};
// 创建reducer
const reducer = (state = inistate, action) => {
...
}
// 根据reducer创建store
const store = createStore(reducer)
// 将store传入props中
{
name: 'vue-app', // app name registered
entry: '//localhost:3001',
container: '#app-qiankun',
activeRule: '/vue',
props: {
store
}
}
# 微应用
const { store } = props // 从props中获取store
store.subscribe(...)
store.dispatch(...)
总结
这里只是对qiankun的大体体验,和对自己觉得不太明白的地方做了一下深入了解,关于应用的部署以及静态资源相关的没有进行尝试,感觉官方文档基本都有涉及,这里就不多赘述了。总的来说对于To B的巨石应用来讲,如果要使用微前端解决方案的话,qiankun确实可以做到开箱即用,对子应用基本无侵入。以上为个人浅薄见解,还望大佬多多指教。