基於微前端qiankun的多頁籤快取方案實踐
作者:vivo 網際網路前端團隊- Tang Xiao
本文梳理了基於阿里開源微前端框架qiankun,實現多頁籤及子應用快取的方案,同時還類比了多個不同方案之間的區別及優劣勢,為使用微前端進行多頁籤開發的同學,提供一些參考。
一、多頁籤是什麼?
我們常見的瀏覽器多頁籤、編輯器多頁籤,從產品角度來說,就是為了能夠實現使用者訪問可記錄,快速定位工作區等作用;那對於單頁應用,可以透過實現多頁籤,對使用者的訪問記錄進行快取,從而提供更好的使用者體驗。
前端可以透過多種方式實現多頁籤,常見的方案有兩種:
-
透過CSS樣式display:none來控制頁面的顯示隱藏模組的內容;
-
將模組序列化快取,透過快取的內容進行渲染(與vue的keep-alive原理類似,在單頁面應用中應用廣泛)。
相對於第一種方式,第二種方式將DOM格式儲存在序列化的JS物件當中,只渲染需要展示的DOM元素,減少了DOM節點數,提升了渲染的效能,是當前主流的實現多頁籤的方式。
那麼相對於傳統的單頁面應用,透過微前端qiankun進行改造後的前端應用,在多頁簽上實現會有什麼不同呢?
1.1 單頁面應用實現多頁籤
改造前的單頁面應用技術棧是Vue全家桶(vue2.6.10 + element2.15.1 + webpack4.0.0+vue-cli4.2.0)。
vue框架提供了keep-alive來支援快取相關的需求,使用keep-alive即可實現多頁籤的基本功能,但是為了支援更多的功能,我們在其基礎上重新封裝了vue-keep-alive元件。
相對較於keep-alive透過include、exclude對快取進行控制,vue-keep-alive使用更原生的釋出訂閱方式來刪除快取,可以實現更完整的多頁籤功能,例如同個路由可以根據引數的不同派生出多個路由例項(如開啟多個詳情頁頁籤)以及動態刪除快取例項等功能。
下面是vue-keep-alive自定義的擴充實現:
created() { // 動態刪除快取例項監聽 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實現的前端微服務框架,本質上還是路由分發式的服務框架,不同於原本 Single-Spa採用JS Entry用的方案,qiankun採用HTML Entry 方式進行了替代最佳化。
使用qiankun進行微前端改造後,頁面被拆分為一個基座應用和多個子應用,每個子應用都執行在獨立的沙箱環境中。
相對於單頁面應用中透過keep-alive管控元件例項的方式,拆分後的各個子應用的keep-alive並不能管控到其他子應用的例項,我們需要快取對所有的應用生效,那麼只能將快取放到基座應用中。
這個就存在幾個問題:
-
載入:主應用需要在什麼時候,用什麼方式來載入子應用例項?
-
渲染:透過快取例項來渲染子應用時,是透過DOM顯隱方式渲染子應用還是有其他方式?
-
通訊:關閉頁籤時,如何判斷是否完全解除安裝子應用,主應用應該使用什麼通訊方式告訴子應用?
二、方案選擇
透過在Github issues及掘金等平臺的一系列資料查詢和對比後,關於如何在qiankun框架下實現多頁籤,在不修改qiankun原始碼的前提下,主要有兩種實現的思路。
2.1 方案一:多個子應用同時存在
實現思路:
在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: ' container: '#appContainer1', props: { data : { store, router } } }, { name: 'app-vue-history', entry: ' container: '#appContainer2', props: { data : store } } ] export default { mounted() { // 優先載入當前的子專案 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 的生命週期函式陣列 const loadApps = apps.map(item => loadMicroApp(item)) // 當 tab 頁關閉時,呼叫 loadApps 中 app 的 unmount 函式即可 }, } </script>[object Object]
具體的DOM展示(透過display:none;控制不同子應用DOM的顯隱):
方案優勢:
-
loadMicroApp是qiankun提供的API,可以方便快速接入;
-
該方式不解除安裝子應用,頁籤切換速度比較快。
方案不足:
-
子應用切換時不銷燬DOM,會導致DOM節點和事件監聽過多,嚴重時會造成頁面卡頓;
-
子應用切換時未解除安裝,路由事件監聽也未解除安裝,需要對路由變化的監聽做特殊的處理。
2.2 方案二:同一時間僅載入一個子應用,同時儲存其他應用的狀態
實現思路:
-
透過registerMicroApps註冊子應用,qiankun會透過自動載入匹配的子應用;
-
參考keep-alive實現方式,每個子應用都快取自己例項的vnode,下次進入子應用時可以直接使用快取的vnode直接渲染為真實DOM。
方案優勢:
-
同一時間,只是展示一個子應用的active頁面,可減少DOM節點數;
-
非active子應用解除安裝時同時會解除安裝DOM及不需要的事件監聽,可釋放一定記憶體。
方案不足:
-
沒有現有的API可以快速實現,需要自己管理子應用快取,實現較為複雜;
-
DOM渲染多了一個從虛擬DOM轉化為真實DOM的一個過程,渲染時間會比第一種方案稍多。
vue元件例項化過程簡介
這裡簡單的回顧下vue的幾個關鍵的渲染節點:
vue關鍵渲染節點(來源:掘金社群)
compile:對template進行編譯,將AST轉化後生成render function;
render:生成VNODE虛擬DOM;
patch :將虛擬DOM轉換為真實DOM;
因此,方案二相對於方案一,就是多了最後patch的過程。
2.3 最終選擇
根據兩種方案優勢與不足的評估,同時根據我們專案的具體情況,最終選擇了方案二進行實現,具體原因如下:
過多的DOM及事件監聽,會造成不必要的記憶體浪費,同時我們的專案主要以編輯器展示和資料展示為主,單個頁籤內內容較多,會更傾向於關注記憶體使用情況;
方案二在子應用二次渲染時多了一個patch過程,渲染速度不會慢多少,在可接受範圍內。
三、具體實現
在上面一部分我們簡單的描述了方案二的一個實現思路,其核心思想就是是透過快取子應用例項的vnode,那麼這一部分,就來看下它的一個具體的實現的過程。
3.1 從元件級別的快取到應用級別的快取
在vue中,keep-alive元件透過快取vnode的方式,實現了元件級別的快取,對於透過vue框架實現的子應用來說,它其實也是一個vue例項,那麼我們同樣也可以做到透過快取vnode的方式,實現應用級別的快取。
透過分析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,防止再次觸發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實現 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渲染。
// 建立子應用例項,有快取的vnode則使用快取的vnode function newVueInstance(cachedNode) { const config = { router: this.router, store: this.store, render: cachedNode ? () => cachedNode : instance.render, // 優先使用快取vnode }); return new Vue(config); } // 例項化子應用例項,根據是否有快取vnode確定是否傳入cachedNode this.instance = newVueInstance(cachedNode); this.instance.$mount('#app');
那麼,這裡不禁就會有些疑問:
-
如果我們每次進入子應用時,都重新建立一個例項,那麼為什麼還要解除安裝,直接不解除安裝就可以了嗎?
-
將快取vnode使用到一個新的例項上,不會有什麼問題嗎?
首先我們回答一下第一個問題,為什麼在切換子應用時,要解除安裝掉原來的子應用例項,有兩個考慮方面:
-
其一,是對記憶體的考量,我們需要的其實僅僅是vnode,而不是整個例項,快取整個例項是方案一的實現方案,所以,我們僅需要快取我們需要的物件即可;
-
其二,解除安裝子應用例項可以移除不必要的事件監聽,比如vue-router對popstate事件就進行了監聽,我們在其他子應用操作時,並不希望原來的子應用也對這些事件進行響應,那麼在子應用解除安裝時,就可以移除掉這些監聽。
對於第二個問題,情況會更加複雜一點,下面一個部分,就主要來看下主要遇到了哪些問題,又該如何去解決。
3.3 解決應用級快取方案的問題
3.3.1 vue-router相關問題
-
在例項解除安裝後對路由變化監聽失效;
-
新的vue-router對原有的router params等引數記錄失效。
首先我們需要明確這兩個問題的原因:
-
第一個是因為在子應用解除安裝時移除了對popstate事件的監聽,那麼我們需要做的就是重新註冊對popstate事件的監聽,這裡可以透過重新例項化一個vue-router解決;
-
第二問題是因為透過重新例項化vue-router解決第一個問題之後,實際上是一個新的vue-router,我們需要做的就是不僅要快取vnode,還需要快取router相關的資訊。
大致的解決實現如下:
// 例項化子應用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); } // 建立子應用例項,有快取的vnode則使用快取的vnode function newVueInstance(cachedNode) { const config = { router: this.router, // 在vue init過程中,會重新呼叫vue-router的init方法,重新啟動對popstate事件監聽 store: this.store, render: cachedNode ? () => cachedNode : instance.render, // 優先使用快取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自帶通訊方式;
從子到父的通訊場景較為簡單,一般只有路由變化時進行上報,並且僅為啟用狀態的子應用才會上報,可直接使用qiankun自帶通訊方式;
父應用——子應用:使用自定義事件通訊;
父應用到子應用,不僅需要和active狀態的子應用通訊,還需要和當前處於快取中子應用通訊;
因此,父應用到子應用,透過自定義事件的方式,能夠實現父應用和多個子應用的通訊。
// 自定義事件釋出 const evt = new CustomEvent('microServiceEvent', { detail: { action: { name: action, data }, basePath, // 用於子應用唯一標識 }, }); document.dispatchEvent(evt); // 自定義事件監聽 document.addEventListener('microServiceEvent', this.listener);
3.3.3 快取管理,防止記憶體洩露
使用快取最重要的事項就是對快取的管理,在不需要的時候及時清理,這在JS中是非常重要但很容易被忽略的事項。
應用級快取
子應用vnode、router等屬性,子應用切換時快取;
頁面級快取
透過vue-keep-alive快取元件的vnode;
刪除頁籤時,監聽remove事件,刪除頁面對應的vnode;
vue-keep-alive元件中所有快取均被刪除時,通知刪除整個子應用快取;
3.4 整體框架
最後,我們從整體的視角來了解下多頁籤快取的實現方案。
因為不僅僅需要對子應用的快取進行管理,還需要將vue-keep-alive元件註冊到各個子應用中等事項,我們將這些服務統一在主應用的mainService中進行管理,在registerMicroApps註冊子應用時透過props傳入子應用,這樣就能夠實現同一套程式碼,多處複用。
// 子應用main.js let mainService = null; export async function mount(props) { mainService = null; const { MainService } = props; // 註冊主應用服務 mainService = new MainService({ // 傳入對應引數 }); // 例項化vue並渲染 mainService.render(props); } export async function unmount() { mainService.unmountCache(); }
最後對關鍵流程進行梳理:
四、現有問題
4.1 暫時只支援vue框架的例項快取
該方案也是基於vue現有特性支援實現的,在react社群中對於多頁籤實現並沒有統一的實現方案,筆者也沒有過多的探索,考慮到現有專案是以vue技術棧為主,後期升級也會只升級到vue3.0,在一段時間內是可以完全支援的。
五、總結
相較於社群上大部分透過方案一進行實現,本文提供了另一種實現多頁籤快取的一種思路,主要是對子應用快取處理上有些許的不同,大致的思路及通訊的方式都是互通的。
另外本文對qiankun框架的使用沒有做太多的發散總結,官網和Github上已經有很多相關問題的總結和踩坑經驗可供參考。
最後,如果文章有什麼問題或錯誤,歡迎指出,謝謝。
參考閱讀
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912579/viewspace-2908394/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- qiankun微前端實踐前端
- 基於 qiankun 的微前端最佳實踐(萬字長文) - 從 0 到 1 篇前端
- 「微前端實踐」使用Vue+qiankun微前端方案重構老專案的本地驗證前端Vue
- Vue微前端架構與Qiankun實踐理論指南Vue前端架構
- 微前端在得物客服域的技術實踐/ 那麼多微前端框架,為啥我們選Qiankun + MF前端框架
- 淺談qiankun微前端前端
- 前端快取最佳實踐前端快取
- Facebook團隊關於網頁快取的再實踐網頁快取
- 微前端實踐前端
- 投稿007期|記一次基於vue的spa多頁籤實踐經驗Vue
- 網頁上的微服務—微前端架構實踐網頁微服務前端架構
- 微前端(qiankun)主應用共享React元件前端React元件
- 使用Jenkins部署微前端方案實踐總結Jenkins前端
- 聊聊微前端的原理和實踐前端
- 基於 iframe 的微前端框架 —— 擎天前端框架
- 對於前端快取的理解(快取機制和快取型別)前端快取型別
- 前端微架構實踐(Vue)前端架構Vue
- Vue 前端配置多級目錄實踐(基於Nginx配置方式)Vue前端Nginx
- 快取&PWA實踐快取
- nuxt快取實踐UX快取
- okhttp 快取實踐HTTP快取
- 初步認識微前端(single-spa 和 qiankun)前端
- 基於多Engine、Navigator2.0實現混合棧管理方案實踐
- 萬字長文+圖文並茂+全面解析微前端框架 qiankun 原始碼 - qiankun 篇前端框架原始碼
- 基於Vue的多專案整合實踐Vue
- 配運基礎資料快取瘦身實踐快取
- 微前端的設計理念與實踐初探前端
- 微前端方案前端
- 基於 RxJs 的前端資料層實踐JS前端
- 微前端框架 之 qiankun 從入門到原始碼分析前端框架原始碼
- 基於快取或zookeeper的分散式鎖實現快取分散式
- 基於ObjectMapper的本地快取ObjectAPP快取
- 專案中多級快取設計實踐總結快取
- 基於Canal+Kafka實現快取實時更新Kafka快取
- 基於vue2.0的weex實踐(前端視角)Vue前端
- 基於 GitLab CI 的前端工程CI/CD實踐Gitlab前端
- 前端開發:基於cypress的自動化實踐前端
- 記錄--微前端qiankun接入vue2&vue3專案前端Vue