掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流

站在用戶的角度思考問題,與客戶深入溝通,找到雙江網(wǎng)站設(shè)計(jì)與雙江網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計(jì)與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個(gè)性化、用戶體驗(yàn)好的作品,建站類型包括:網(wǎng)站制作、成都網(wǎng)站設(shè)計(jì)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、域名注冊(cè)、網(wǎng)頁(yè)空間、企業(yè)郵箱。業(yè)務(wù)覆蓋雙江地區(qū)。
作者|vivo 互聯(lián)網(wǎng)前端團(tuán)隊(duì)- Tang Xiao
本文梳理了基于阿里開源微前端框架qiankun,實(shí)現(xiàn)多頁(yè)簽及子應(yīng)用緩存的方案,同時(shí)還類比了多個(gè)不同方案之間的區(qū)別及優(yōu)劣勢(shì),為使用微前端進(jìn)行多頁(yè)簽開發(fā)的同學(xué),提供一些參考。
我們常見的瀏覽器多頁(yè)簽、編輯器多頁(yè)簽,從產(chǎn)品角度來說,就是為了能夠?qū)崿F(xiàn)用戶訪問可記錄,快速定位工作區(qū)等作用;那對(duì)于單頁(yè)應(yīng)用,可以通過實(shí)現(xiàn)多頁(yè)簽,對(duì)用戶的訪問記錄進(jìn)行緩存,從而提供更好的用戶體驗(yàn)。
前端可以通過多種方式實(shí)現(xiàn)多頁(yè)簽,常見的方案有兩種:
相對(duì)于第一種方式,第二種方式將DOM格式存儲(chǔ)在序列化的JS對(duì)象當(dāng)中,只渲染需要展示的DOM元素,減少了DOM節(jié)點(diǎn)數(shù),提升了渲染的性能,是當(dāng)前主流的實(shí)現(xiàn)多頁(yè)簽的方式。
那么相對(duì)于傳統(tǒng)的單頁(yè)面應(yīng)用,通過微前端qiankun進(jìn)行改造后的前端應(yīng)用,在多頁(yè)簽上實(shí)現(xiàn)會(huì)有什么不同呢?
改造前的單頁(yè)面應(yīng)用技術(shù)棧是Vue全家桶(vue2.6.10 + element2.15.1 + webpack4.0.0+vue-cli4.2.0)。
vue框架提供了keep-alive來支持緩存相關(guān)的需求,使用keep-alive即可實(shí)現(xiàn)多頁(yè)簽的基本功能,但是為了支持更多的功能,我們?cè)谄浠A(chǔ)上重新封裝了vue-keep-alive組件。
相對(duì)較于keep-alive通過include、exclude對(duì)緩存進(jìn)行控制,vue-keep-alive使用更原生的發(fā)布訂閱方式來刪除緩存,可以實(shí)現(xiàn)更完整的多頁(yè)簽功能,例如同個(gè)路由可以根據(jù)參數(shù)的不同派生出多個(gè)路由實(shí)例(如打開多個(gè)詳情頁(yè)頁(yè)簽)以及動(dòng)態(tài)刪除緩存實(shí)例等功能。
下面是vue-keep-alive自定義的拓展實(shí)現(xiàn):
created() {
// 動(dòng)態(tài)刪除緩存實(shí)例監(jiān)聽
this.cache = Object.create(null);
breadCompBus.$on('removeTabByKey', this.removeCacheByKey);
breadCompBus.$on('removeTabByKeys', (data) => {
data.forEach((item) => {
this.removeCacheByKey(item);
});
});
}
vue-keep-alive組件即可傳入自定義方法,用于自定義vnode.key,支持同一匹配路由中派生多個(gè)實(shí)例。
// 傳入`vue-keep-alive`的自定義方法
function updateComponentsKey(key, name, vnode) {
const match = this.$route.matched[1];
if (match && match.meta.multiNodeKey) {
vnode.key = match.meta.multiNodeKey(key, this.$route);
return vnode.key;
}
return key;
}
qiankun是由螞蟻金服推出的基于Single-Spa實(shí)現(xiàn)的前端微服務(wù)框架,本質(zhì)上還是路由分發(fā)式的服務(wù)框架,不同于原本 Single-Spa采用JS Entry用的方案,qiankun采用HTML Entry 方式進(jìn)行了替代優(yōu)化。
使用qiankun進(jìn)行微前端改造后,頁(yè)面被拆分為一個(gè)基座應(yīng)用和多個(gè)子應(yīng)用,每個(gè)子應(yīng)用都運(yùn)行在獨(dú)立的沙箱環(huán)境中。
相對(duì)于單頁(yè)面應(yīng)用中通過keep-alive管控組件實(shí)例的方式,拆分后的各個(gè)子應(yīng)用的keep-alive并不能管控到其他子應(yīng)用的實(shí)例,我們需要緩存對(duì)所有的應(yīng)用生效,那么只能將緩存放到基座應(yīng)用中。
這個(gè)就存在幾個(gè)問題:
通過在Github issues及掘金等平臺(tái)的一系列資料查找和對(duì)比后,關(guān)于如何在qiankun框架下實(shí)現(xiàn)多頁(yè)簽,在不修改qiankun源碼的前提下,主要有兩種實(shí)現(xiàn)的思路。
實(shí)現(xiàn)思路:
示例:
app-vue-hash
app-vue-history
about
具體的DOM展示(通過display:none;控制不同子應(yīng)用DOM的顯隱):
方案優(yōu)勢(shì):
方案不足:
實(shí)現(xiàn)思路:
方案優(yōu)勢(shì):
方案不足:
vue組件實(shí)例化過程簡(jiǎn)介
這里簡(jiǎn)單的回顧下vue的幾個(gè)關(guān)鍵的渲染節(jié)點(diǎn):
vue關(guān)鍵渲染節(jié)點(diǎn)(來源:掘金社區(qū))
因此,方案二相對(duì)于方案一,就是多了最后patch的過程。
根據(jù)兩種方案優(yōu)勢(shì)與不足的評(píng)估,同時(shí)根據(jù)我們項(xiàng)目的具體情況,最終選擇了方案二進(jìn)行實(shí)現(xiàn),具體原因如下:
在上面一部分我們簡(jiǎn)單的描述了方案二的一個(gè)實(shí)現(xiàn)思路,其核心思想就是是通過緩存子應(yīng)用實(shí)例的vnode,那么這一部分,就來看下它的一個(gè)具體的實(shí)現(xiàn)的過程。
在vue中,keep-alive組件通過緩存vnode的方式,實(shí)現(xiàn)了組件級(jí)別的緩存,對(duì)于通過vue框架實(shí)現(xiàn)的子應(yīng)用來說,它其實(shí)也是一個(gè)vue實(shí)例,那么我們同樣也可以做到通過緩存vnode的方式,實(shí)現(xiàn)應(yīng)用級(jí)別的緩存。
通過分析keep-alive源碼,我們了解到keep-alive是通過在render中進(jìn)行緩存命中,返回對(duì)應(yīng)組件的vnode,并在mounted和upda
// keep-alive核心代碼
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// 更多代碼...
// 緩存命中
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
// 設(shè)置keep-alive,防止再次觸發(fā)created等生命周期
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
// mounted和updated時(shí)緩存當(dāng)前組件的vnode
mounted() {
this.cacheVNode()
}
updated() {
this.cacheVNode()
}
相對(duì)于keep-alive需要在mounted和updated兩個(gè)生命周期中對(duì)vnode緩存進(jìn)行更新,在應(yīng)用級(jí)的緩存中,我們只需要在子應(yīng)用卸載時(shí),主動(dòng)對(duì)整個(gè)實(shí)例的vnode進(jìn)行緩存即可。
// 父應(yīng)用提供unmountCache方法
function unmountCache() {
// 此處永遠(yuǎn)只會(huì)保存首次加載生成的實(shí)例
const needCached = this.instance?.cachedInstance || this.instance;
const cachedInstance = {};
cachedInstance._vnode = needCached._vnode;
// keepalive設(shè)置為必須 防止進(jìn)入時(shí)再次created,同keep-alive實(shí)現(xiàn)
if (!cachedInstance._vnode.data.keepAlive) cachedInstance._vnode.data.keepAlive = true;
// 省略其他代碼...
// loadedApplicationMap用于是key-value形式,用于保存當(dāng)前應(yīng)用的實(shí)例
loadedApplicationMap[this.cacheKey] = cachedInstance;
// 省略其他代碼...
// 卸載實(shí)例
this.instance.$destroy();
// 設(shè)置為null后可進(jìn)行垃圾回收
this.instance = null;
}
// 子應(yīng)用在qiankun框架提供的卸載方法中,調(diào)用unmountCache
export async function unmount() {
console.log('[vue] system app unmount');
mainService.unmountCache();
}
將vnode緩存到內(nèi)存中后,再將原有的instance卸載,重新進(jìn)入子應(yīng)用時(shí),就可以使用緩存的vnode進(jìn)行render渲染。
// 創(chuàng)建子應(yīng)用實(shí)例,有緩存的vnode則使用緩存的vnode
function newVueInstance(cachedNode) {
const config = {
router: this.router,
store: this.store,
render: cachedNode ? () => cachedNode : instance.render, // 優(yōu)先使用緩存vnode
});
return new Vue(config);
}
// 實(shí)例化子應(yīng)用實(shí)例,根據(jù)是否有緩存vnode確定是否傳入cachedNode
this.instance = newVueInstance(cachedNode);
this.instance.$mount('#app');
那么,這里不禁就會(huì)有些疑問:
首先我們回答一下第一個(gè)問題,為什么在切換子應(yīng)用時(shí),要卸載掉原來的子應(yīng)用實(shí)例,有兩個(gè)考慮方面:
對(duì)于第二個(gè)問題,情況會(huì)更加復(fù)雜一點(diǎn),下面一個(gè)部分,就主要來看下主要遇到了哪些問題,又該如何去解決。
首先我們需要明確這兩個(gè)問題的原因:
大致的解決實(shí)現(xiàn)如下:
// 實(shí)例化子應(yīng)用vue-router
function initRouter() {
const { router: originRouter } = this.baseConfig;
const config = Object.assign(originRouter, {
base: `app-kafka/`,
});
Vue.use(VueRouter);
this.router = new VueRouter(config);
}
// 創(chuàng)建子應(yīng)用實(shí)例,有緩存的vnode則使用緩存的vnode
function newVueInstance(cachedNode) {
const config = {
router: this.router, // 在vue init過程中,會(huì)重新調(diào)用vue-router的init方法,重新啟動(dòng)對(duì)popstate事件監(jiān)聽
store: this.store,
render: cachedNode ? () => cachedNode : instance.render, // 優(yōu)先使用緩存vnode
});
return new Vue(config);
}
function render() {
if(isCache) {
// 場(chǎng)景一、重新進(jìn)入應(yīng)用(有緩存)
const cachedInstance = loadedApplicationMap[this.cacheKey];
// router使用緩存命中
this.router = cachedInstance.$router;
// 讓當(dāng)前路由在最初的Vue實(shí)例上可用
this.router.apps = cachedInstance.catchRoute.apps;
// 使用緩存vnode重新實(shí)例化子應(yīng)用
const cachedNode = cachedInstance._vnode;
this.instance = this.newVueInstance(cachedNode);
} else {
// 場(chǎng)景二、首次加載子應(yīng)用/重新進(jìn)入應(yīng)用(無緩存)
this.initRouter();
// 正常實(shí)例化
this.instance = this.newVueInstance();
}
}
function unmountCache() {
// 省略其他代碼...
cachedInstance.$router = this.instance.$router;
cachedInstance.$router.app = null;
// 省略其他代碼...
}
多頁(yè)簽的方式增加了父子組件通信的頻率,qiankun有提供setGlobalState通信方式,但是在單應(yīng)用模式下,同一時(shí)間僅支持和一個(gè)子應(yīng)用進(jìn)行通行,對(duì)于unmount 的子應(yīng)用來說,無法接收到父應(yīng)用的通信,因此,對(duì)于不同的場(chǎng)景,我們需要更加靈活的通信方式。
子應(yīng)用——父應(yīng)用:使用qiankun自帶通信方式;
從子到父的通信場(chǎng)景較為簡(jiǎn)單,一般只有路由變化時(shí)進(jìn)行上報(bào),并且僅為激活狀態(tài)的子應(yīng)用才會(huì)上報(bào),可直接使用qiankun自帶通信方式;
父應(yīng)用——子應(yīng)用:使用自定義事件通信;
父應(yīng)用到子應(yīng)用,不僅需要和active狀態(tài)的子應(yīng)用通信,還需要和當(dāng)前處于緩存中子應(yīng)用通信;
因此,父應(yīng)用到子應(yīng)用,通過自定義事件的方式,能夠?qū)崿F(xiàn)父應(yīng)用和多個(gè)子應(yīng)用的通信。
// 自定義事件發(fā)布
const evt = new CustomEvent('microServiceEvent', {
detail: {
action: { name: action, data },
basePath, // 用于子應(yīng)用唯一標(biāo)識(shí)
},
});
document.dispatchEvent(evt);
// 自定義事件監(jiān)聽
document.addEventListener('microServiceEvent', this.listener);
使用緩存最重要的事項(xiàng)就是對(duì)緩存的管理,在不需要的時(shí)候及時(shí)清理,這在JS中是非常重要但很容易被忽略的事項(xiàng)。
應(yīng)用級(jí)緩存
頁(yè)面級(jí)緩存
最后,我們從整體的視角來了解下多頁(yè)簽緩存的實(shí)現(xiàn)方案。
因?yàn)椴粌H僅需要對(duì)子應(yīng)用的緩存進(jìn)行管理,還需要將vue-keep-alive組件注冊(cè)到各個(gè)子應(yīng)用中等事項(xiàng),我們將這些服務(wù)統(tǒng)一在主應(yīng)用的mainService中進(jìn)行管理,在registerMicroApps注冊(cè)子應(yīng)用時(shí)通過props傳入子應(yīng)用,這樣就能夠?qū)崿F(xiàn)同一套代碼,多處復(fù)用。
// 子應(yīng)用main.js
let mainService = null;
export async function mount(props) {
mainService = null;
const { MainService } = props;
// 注冊(cè)主應(yīng)用服務(wù)
mainService = new MainService({
// 傳入對(duì)應(yīng)參數(shù)
});
// 實(shí)例化vue并渲染
mainService.render(props);
}
export async function unmount() {
mainService.unmountCache();
}
最后對(duì)關(guān)鍵流程進(jìn)行梳理:
該方案也是基于vue現(xiàn)有特性支持實(shí)現(xiàn)的,在react社區(qū)中對(duì)于多頁(yè)簽實(shí)現(xiàn)并沒有統(tǒng)一的實(shí)現(xiàn)方案,筆者也沒有過多的探索,考慮到現(xiàn)有項(xiàng)目是以vue技術(shù)棧為主,后期升級(jí)也會(huì)只升級(jí)到vue3.0,在一段時(shí)間內(nèi)是可以完全支持的。
相較于社區(qū)上大部分通過方案一進(jìn)行實(shí)現(xiàn),本文提供了另一種實(shí)現(xiàn)多頁(yè)簽緩存的一種思路,主要是對(duì)子應(yīng)用緩存處理上有些許的不同,大致的思路及通信的方式都是互通的。
另外本文對(duì)qiankun框架的使用沒有做太多的發(fā)散總結(jié),官網(wǎng)和Github上已經(jīng)有很多相關(guān)問題的總結(jié)和踩坑經(jīng)驗(yàn)可供參考。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流