基于微前端qiankun的多頁簽緩存方案實踐
本文梳理了基于阿里開源微前端框架qiankun,實現(xiàn)多頁簽及子應用緩存的方案,同時還類比了多個不同方案之間的區(qū)別及優(yōu)劣勢,為使用微前端進行多頁簽開發(fā)的同學,提供一些參考。
本文梳理了基于阿里開源微前端框架qiankun,實現(xiàn)多頁簽及子應用緩存的方案,同時還類比了多個不同方案之間的區(qū)別及優(yōu)劣勢,為使用微前端進行多頁簽開發(fā)的同學,提供一些參考。
一、多頁簽是什么?
我們常見的瀏覽器多頁簽、編輯器多頁簽,從產(chǎn)品角度來說,就是為了能夠實現(xiàn)用戶訪問可記錄,快速定位工作區(qū)等作用;那對于單頁應用,可以通過實現(xiàn)多頁簽,對用戶的訪問記錄進行緩存,從而提供更好的用戶體驗。
前端可以通過多種方式實現(xiàn)多頁簽,常見的方案有兩種:
- 通過CSS樣式display:none來控制頁面的顯示隱藏模塊的內容;
- 將模塊序列化緩存,通過緩存的內容進行渲染(與vue的keep-alive原理類似,在單頁面應用中應用廣泛)。
相對于第一種方式,第二種方式將DOM格式存儲在序列化的JS對象當中,只渲染需要展示的DOM元素,減少了DOM節(jié)點數(shù),提升了渲染的性能,是當前主流的實現(xiàn)多頁簽的方式。
那么相對于傳統(tǒng)的單頁面應用,通過微前端qiankun進行改造后的前端應用,在多頁簽上實現(xiàn)會有什么不同呢?
1.1 單頁面應用實現(xiàn)多頁簽
改造前的單頁面應用技術棧是Vue全家桶(vue2.6.10 + element2.15.1 + webpack4.0.0+vue-cli4.2.0)。
vue框架提供了keep-alive來支持緩存相關的需求,使用keep-alive即可實現(xiàn)多頁簽的基本功能,但是為了支持更多的功能,我們在其基礎上重新封裝了vue-keep-alive組件。
相對較于keep-alive通過include、exclude對緩存進行控制,vue-keep-alive使用更原生的發(fā)布訂閱方式來刪除緩存,可以實現(xiàn)更完整的多頁簽功能,例如同個路由可以根據(jù)參數(shù)的不同派生出多個路由實例(如打開多個詳情頁頁簽)以及動態(tài)刪除緩存實例等功能。
下面是vue-keep-alive自定義的拓展實現(xiàn):
created() { // 動態(tài)刪除緩存實例監(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,支持同一匹配路由中派生多個實例。
// 傳入`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; }
1.2 使用qiankun進行微前端改造后,多頁簽緩存有什么不同
qiankun是由螞蟻金服推出的基于Single-Spa實現(xiàn)的前端微服務框架,本質上還是路由分發(fā)式的服務框架,不同于原本 Single-Spa采用JS Entry用的方案,qiankun采用HTML Entry 方式進行了替代優(yōu)化。
使用qiankun進行微前端改造后,頁面被拆分為一個基座應用和多個子應用,每個子應用都運行在獨立的沙箱環(huán)境中。
相對于單頁面應用中通過keep-alive管控組件實例的方式,拆分后的各個子應用的keep-alive并不能管控到其他子應用的實例,我們需要緩存對所有的應用生效,那么只能將緩存放到基座應用中。
這個就存在幾個問題:
- 加載:主應用需要在什么時候,用什么方式來加載子應用實例?
- 渲染:通過緩存實例來渲染子應用時,是通過DOM顯隱方式渲染子應用還是有其他方式?
- 通信:關閉頁簽時,如何判斷是否完全卸載子應用,主應用應該使用什么通信方式告訴子應用?
二、方案選擇
通過在Github issues及掘金等平臺的一系列資料查找和對比后,關于如何在qiankun框架下實現(xiàn)多頁簽,在不修改qiankun源碼的前提下,主要有兩種實現(xiàn)的思路。
2.1 方案一:多個子應用同時存在
實現(xiàn)思路:
在dom上通過v-show控制顯示哪一個子應用,及display:none;控制不同子應用dom的顯示隱藏。
url變化時,通過loadMicroApp手動控制加載哪個子應用,在頁簽關閉時,手動調用unmount方法卸載子應用。
示例:
<template> <div id="app"> <header> <router-link to="/app-vue-hash/">app-vue-hash</router-link> <router-link to="/app-vue-history/">app-vue-history</router-link> <router-link to="/about">about</router-link> </header> <div id="appContainer1" v-show="$route.path.startsWith('/app-vue-hash/')"></div> <div id="appContainer2" v-show="$route.path.startsWith('/app-vue-history/')"></div> <router-view></router-view> </div> </template> <script> import { loadMicroApp } from 'qiankun'; const apps = [ { name: 'app-vue-hash', entry: 'http://localhost:1111', container: '#appContainer1', props: { data : { store, router } } }, { name: 'app-vue-history', entry: 'http://localhost:2222', container: '#appContainer2', props: { data : store } } ] export default { mounted() { // 優(yōu)先加載當前的子項目 const path = this.$route.path; const currentAppIndex = apps.findIndex(item => path.includes(item.name)); if(currentAppIndex !== -1){ const currApp = apps.splice(currentAppIndex, 1)[0]; apps.unshift(currApp); } // loadMicroApp 返回值是 app 的生命周期函數(shù)數(shù)組 const loadApps = apps.map(item => loadMicroApp(item)) // 當 tab 頁關閉時,調用 loadApps 中 app 的 unmount 函數(shù)即可 }, } </script>
具體的DOM展示(通過display:none;控制不同子應用DOM的顯隱):
方案優(yōu)勢:
- loadMicroApp是qiankun提供的API,可以方便快速接入;
- 該方式不卸載子應用,頁簽切換速度比較快。
方案不足:
- 子應用切換時不銷毀DOM,會導致DOM節(jié)點和事件監(jiān)聽過多,嚴重時會造成頁面卡頓;
- 子應用切換時未卸載,路由事件監(jiān)聽也未卸載,需要對路由變化的監(jiān)聽做特殊的處理。
2.2 方案二:同一時間僅加載一個子應用,同時保存其他應用的狀態(tài)
實現(xiàn)思路:
- 通過registerMicroApps注冊子應用,qiankun會通過自動加載匹配的子應用;
- 參考keep-alive實現(xiàn)方式,每個子應用都緩存自己實例的vnode,下次進入子應用時可以直接使用緩存的vnode直接渲染為真實DOM。
方案優(yōu)勢:
- 同一時間,只是展示一個子應用的active頁面,可減少DOM節(jié)點數(shù);
- 非active子應用卸載時同時會卸載DOM及不需要的事件監(jiān)聽,可釋放一定內存。
方案不足:
- 沒有現(xiàn)有的API可以快速實現(xiàn),需要自己管理子應用緩存,實現(xiàn)較為復雜;
- DOM渲染多了一個從虛擬DOM轉化為真實DOM的一個過程,渲染時間會比第一種方案稍多。
vue組件實例化過程簡介
這里簡單的回顧下vue的幾個關鍵的渲染節(jié)點:
vue關鍵渲染節(jié)點(來源:掘金社區(qū))
- compile:對template進行編譯,將AST轉化后生成render function;
- render:生成VNODE虛擬DOM;
- patch :將虛擬DOM轉換為真實DOM;
因此,方案二相對于方案一,就是多了最后patch的過程。
2.3 最終選擇
根據(jù)兩種方案優(yōu)勢與不足的評估,同時根據(jù)我們項目的具體情況,最終選擇了方案二進行實現(xiàn),具體原因如下:
- 過多的DOM及事件監(jiān)聽,會造成不必要的內存浪費,同時我們的項目主要以編輯器展示和數(shù)據(jù)展示為主,單個頁簽內內容較多,會更傾向于關注內存使用情況;
- 方案二在子應用二次渲染時多了一個patch過程,渲染速度不會慢多少,在可接受范圍內。
三、具體實現(xiàn)
在上面一部分我們簡單的描述了方案二的一個實現(xiàn)思路,其核心思想就是是通過緩存子應用實例的vnode,那么這一部分,就來看下它的一個具體的實現(xiàn)的過程。
3.1 從組件級別的緩存到應用級別的緩存
在vue中,keep-alive組件通過緩存vnode的方式,實現(xiàn)了組件級別的緩存,對于通過vue框架實現(xiàn)的子應用來說,它其實也是一個vue實例,那么我們同樣也可以做到通過緩存vnode的方式,實現(xiàn)應用級別的緩存。
通過分析keep-alive源碼,我們了解到keep-alive是通過在render中進行緩存命中,返回對應組件的vnode,并在mounted和updated兩個生命周期鉤子中加入對子組件vnode的緩存。
// 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 } // 設置keep-alive,防止再次觸發(fā)created等生命周期 vnode.data.keepAlive = true } return vnode || (slot && slot[0]) } // mounted和updated時緩存當前組件的vnode mounted() { this.cacheVNode() } updated() { this.cacheVNode() }
相對于keep-alive需要在mounted和updated兩個生命周期中對vnode緩存進行更新,在應用級的緩存中,我們只需要在子應用卸載時,主動對整個實例的vnode進行緩存即可。
// 父應用提供unmountCache方法 function unmountCache() { // 此處永遠只會保存首次加載生成的實例 const needCached = this.instance?.cachedInstance || this.instance; const cachedInstance = {}; cachedInstance._vnode = needCached._vnode; // keepalive設置為必須 防止進入時再次created,同keep-alive實現(xiàn) if (!cachedInstance._vnode.data.keepAlive) cachedInstance._vnode.data.keepAlive = true; // 省略其他代碼... // loadedApplicationMap用于是key-value形式,用于保存當前應用的實例 loadedApplicationMap[this.cacheKey] = cachedInstance; // 省略其他代碼... // 卸載實例 this.instance.$destroy(); // 設置為null后可進行垃圾回收 this.instance = null; } // 子應用在qiankun框架提供的卸載方法中,調用unmountCache export async function unmount() { console.log('[vue] system app unmount'); mainService.unmountCache(); }
3.2 移花接木——將vnode重新掛載到一個新實例上
將vnode緩存到內存中后,再將原有的instance卸載,重新進入子應用時,就可以使用緩存的vnode進行render渲染。
// 創(chuàng)建子應用實例,有緩存的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); } // 實例化子應用實例,根據(jù)是否有緩存vnode確定是否傳入cachedNode this.instance = newVueInstance(cachedNode); this.instance.$mount('#app');
那么,這里不禁就會有些疑問:
- 如果我們每次進入子應用時,都重新創(chuàng)建一個實例,那么為什么還要卸載,直接不卸載就可以了嗎?
- 將緩存vnode使用到一個新的實例上,不會有什么問題嗎?
首先我們回答一下第一個問題,為什么在切換子應用時,要卸載掉原來的子應用實例,有兩個考慮方面:
- 其一,是對內存的考量,我們需要的其實僅僅是vnode,而不是整個實例,緩存整個實例是方案一的實現(xiàn)方案,所以,我們僅需要緩存我們需要的對象即可;
- 其二,卸載子應用實例可以移除不必要的事件監(jiān)聽,比如vue-router對popstate事件就進行了監(jiān)聽,我們在其他子應用操作時,并不希望原來的子應用也對這些事件進行響應,那么在子應用卸載時,就可以移除掉這些監(jiān)聽。
對于第二個問題,情況會更加復雜一點,下面一個部分,就主要來看下主要遇到了哪些問題,又該如何去解決。
3.3 解決應用級緩存方案的問題
3.3.1 vue-router相關問題
- 在實例卸載后對路由變化監(jiān)聽失效;
- 新的vue-router對原有的router params等參數(shù)記錄失效。
首先我們需要明確這兩個問題的原因:
- 第一個是因為在子應用卸載時移除了對popstate事件的監(jiān)聽,那么我們需要做的就是重新注冊對popstate事件的監(jiān)聽,這里可以通過重新實例化一個vue-router解決;
- 第二問題是因為通過重新實例化vue-router解決第一個問題之后,實際上是一個新的vue-router,我們需要做的就是不僅要緩存vnode,還需要緩存router相關的信息。
大致的解決實現(xiàn)如下:
// 實例化子應用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)建子應用實例,有緩存的vnode則使用緩存的vnode function newVueInstance(cachedNode) { const config = { router: this.router, // 在vue init過程中,會重新調用vue-router的init方法,重新啟動對popstate事件監(jiān)聽 store: this.store, render: cachedNode ? () => cachedNode : instance.render, // 優(yōu)先使用緩存vnode }); return new Vue(config); } function render() { if(isCache) { // 場景一、重新進入應用(有緩存) const cachedInstance = loadedApplicationMap[this.cacheKey]; // router使用緩存命中 this.router = cachedInstance.$router; // 讓當前路由在最初的Vue實例上可用 this.router.apps = cachedInstance.catchRoute.apps; // 使用緩存vnode重新實例化子應用 const cachedNode = cachedInstance._vnode; this.instance = this.newVueInstance(cachedNode); } else { // 場景二、首次加載子應用/重新進入應用(無緩存) this.initRouter(); // 正常實例化 this.instance = this.newVueInstance(); } } function unmountCache() { // 省略其他代碼... cachedInstance.$router = this.instance.$router; cachedInstance.$router.app = null; // 省略其他代碼... }
3.3.2 父子組件通信
多頁簽的方式增加了父子組件通信的頻率,qiankun有提供setGlobalState通信方式,但是在單應用模式下,同一時間僅支持和一個子應用進行通行,對于unmount 的子應用來說,無法接收到父應用的通信,因此,對于不同的場景,我們需要更加靈活的通信方式。
子應用——父應用:使用qiankun自帶通信方式;
從子到父的通信場景較為簡單,一般只有路由變化時進行上報,并且僅為激活狀態(tài)的子應用才會上報,可直接使用qiankun自帶通信方式;
父應用——子應用:使用自定義事件通信;
父應用到子應用,不僅需要和active狀態(tài)的子應用通信,還需要和當前處于緩存中子應用通信;
因此,父應用到子應用,通過自定義事件的方式,能夠實現(xiàn)父應用和多個子應用的通信。
// 自定義事件發(fā)布 const evt = new CustomEvent('microServiceEvent', { detail: { action: { name: action, data }, basePath, // 用于子應用唯一標識 }, }); document.dispatchEvent(evt); // 自定義事件監(jiān)聽 document.addEventListener('microServiceEvent', this.listener);
3.3.3 緩存管理,防止內存泄露
- 使用緩存最重要的事項就是對緩存的管理,在不需要的時候及時清理,這在JS中是非常重要但很容易被忽略的事項。
應用級緩存
- 子應用vnode、router等屬性,子應用切換時緩存;
頁面級緩存
- 通過vue-keep-alive緩存組件的vnode;
- 刪除頁簽時,監(jiān)聽remove事件,刪除頁面對應的vnode;
- vue-keep-alive組件中所有緩存均被刪除時,通知刪除整個子應用緩存;
3.4 整體框架
最后,我們從整體的視角來了解下多頁簽緩存的實現(xiàn)方案。
因為不僅僅需要對子應用的緩存進行管理,還需要將vue-keep-alive組件注冊到各個子應用中等事項,我們將這些服務統(tǒng)一在主應用的mainService中進行管理,在registerMicroApps注冊子應用時通過props傳入子應用,這樣就能夠實現(xiàn)同一套代碼,多處復用。
// 子應用main.js let mainService = null; export async function mount(props) { mainService = null; const { MainService } = props; // 注冊主應用服務 mainService = new MainService({ // 傳入對應參數(shù) }); // 實例化vue并渲染 mainService.render(props); } export async function unmount() { mainService.unmountCache(); }
最后對關鍵流程進行梳理:
四、現(xiàn)有問題
4.1 暫時只支持vue框架的實例緩存
該方案也是基于vue現(xiàn)有特性支持實現(xiàn)的,在react社區(qū)中對于多頁簽實現(xiàn)并沒有統(tǒng)一的實現(xiàn)方案,筆者也沒有過多的探索,考慮到現(xiàn)有項目是以vue技術棧為主,后期升級也會只升級到vue3.0,在一段時間內是可以完全支持的。
五、總結
相較于社區(qū)上大部分通過方案一進行實現(xiàn),本文提供了另一種實現(xiàn)多頁簽緩存的一種思路,主要是對子應用緩存處理上有些許的不同,大致的思路及通信的方式都是互通的。
另外本文對qiankun框架的使用沒有做太多的發(fā)散總結,官網(wǎng)和Github上已經(jīng)有很多相關問題的總結和踩坑經(jīng)驗可供參考。
最后,如果文章有什么問題或錯誤,歡迎指出,謝謝。
參考閱讀
[Feature Request] 主應用多頁簽切換不同子應用的頁面狀態(tài)保持 #361
到此這篇關于基于微前端qiankun的多頁簽緩存方案實踐的文章就介紹到這了,更多相關微前端qiankun多頁簽緩存內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
javascript判斷移動端訪問設備并解析對應CSS的方法
這篇文章主要介紹了javascript判斷移動端訪問設備并解析對應CSS的方法,涉及移動端設備的判斷及動態(tài)加載技巧,需要的朋友可以參考下2015-02-02原生javascript實現(xiàn)類似vue的數(shù)據(jù)綁定功能示例【觀察者模式】
這篇文章主要介紹了原生javascript實現(xiàn)類似vue的數(shù)據(jù)綁定功能,結合實例形式分析了JavaScript基于觀察者模式實現(xiàn)類似vue的數(shù)據(jù)綁定相關操作技巧,需要的朋友可以參考下2020-02-02javascript中使用new與不使用實例化對象的區(qū)別
這篇文章主要介紹了javascript中使用new與不使用實例化對象的區(qū)別的相關資料,需要的朋友可以參考下2015-06-06