本文首發於 vivo互聯網技術 微信公眾號
鏈接:https://mp.weixin.qq.com/s/2qH9qMNpU_LuLEBTsDUKzA
作者:Tan Xin
本文對微前端的概念和場景進行科普,介紹一些主流的微前端的實現庫及其用法,並講解部分這些庫的原理和實踐知識。
一、微前端
在項目迭代中,隨着業務的發展壯大,項目的功能模塊通常也會越來越多。可能原來所有的代碼模塊都在一個倉庫里,由一個團隊負責。但隨着功能模塊越來越多,一個團隊可能負責不過來,需要多個團隊來專門維護不同的模塊。相應的代碼也會被拆到多個倉庫里,並且各模塊能獨立開發、部署更新。通常雖然項目被拆成了多個模塊,但為了維持整體統一性以及用戶體驗,各模塊依然都會掛在統一的入口下。
上面所述場景就是典型的微前端場景,類似於后端的微服務架構,它將web應用由單一的單體應用轉變為多個小型前端應用聚合為一的應用。
通常,要實現上面類似的需求,我們很容易會想到使用iframe的方式來實現。在入口框架中用iframe來顯示子模塊的頁面,切換子模塊時,iframe也跟着切換成對應子模塊頁面的url。
雖然iframe是比較容易實現的,但通常也會有一些問題:
- 顯示區域受限制,比如子項目中顯示彈窗蒙層時,蒙層只會覆蓋iframe區域,無法覆蓋整個頁面,內容也無法真正居中。
- 頁面瀏覽記錄無法自動被記錄,刷新頁面后iframe又自動回到首頁。
- 全局上下文完全隔離,變量不共享,頁面間通信比較麻煩,比如子項目與主題框架、子項目之間通信等,只能采用postMessage方式。
- 速度較慢,每次進入子應用時都要重建整個上下文。
上面所列問題,有些可以解決,有些甚至都沒法或者很難解決。總的來說,iframe是一個比較快捷的方案,但不是最好的方案,會對體驗有很多限制。如果強行打各種patch,復雜度又上來了,最后可能得不償失。
二、single-spa
剛才我們講了iframe實現微前端的一些弊端,主要原因就是這些應用還是在各自獨立的頁面內,這就導致了一些天然的限制。而single-spa微前端方案結合了MPA和SPA的優勢,可以在單個頁面內集成多個應用,並且是技術棧無關的。
如上圖就是采用single-spa實現微前端的整體流程:
資源模塊加載器:用來加載子項目初始化資源。我們將子項目的入口js構建成umd格式,然后使用模塊加載器遠程加載,通常會使用SystemJs(不是必須)通用模塊加載器來進行加載。
子應用資源配置表:用來記錄各個子應用的入口資源url信息,以便在切換不同子應用時使用模塊加載器去遠程加載。因為每次子應用更新后入口資源的hash通常會變化,所以需要服務端定時去更新該配置表,以便框架能及時加載子應用最新的資源。
注意:single-spa本身是不支持子應用資源列表的,每個子應用只能將自己所有初始化資源打包到一個入口js中。如果子應用初始化資源有多個文件(可以通過webpack-manifest-plugin生成應用初始化資源清單),就需要按照上述方式來添加額外處理。
1、框架入口
<!DOCTYPE html> <html> <head> <!-- 在systemjs中注冊模塊 --> <script type="systemjs-importmap"> { "imports": { "app1": "http://localhost:8081/js/app.js", "app2": "http://localhost:8082/js/app.js", "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js", "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js", "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js", "vuex": "https://cdnjs.cloudflare.com/ajax/libs/vuex/3.1.2/vuex.min.js" } } </script> </head> <body> <div></div> <!-- 加載systemjs --> <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-exports.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-register.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script> <script> (function () { // 加載公共js庫 Promise.all([System.import('single-spa'), System.import('vue'), System.import('vue-router'), System.import('vuex')]).then(function (modules) { var singleSpa = modules[0]; var Vue = modules[1]; var VueRouter = modules[2]; var Vuex = modules[3]; Vue.use(VueRouter) Vue.use(Vuex) // single-spa注冊子應用 singleSpa.registerApplication( 'app1', () => System.import('app1'), location => location.pathname.startsWith('/app1') ) singleSpa.registerApplication( 'app2', () => System.import('app2'), location => location.pathname.startsWith('/app2') ) // 啟動 singleSpa.start(); }) })() </script> </body> </html>
為了簡單展示,上述只是框架入口html的一個簡單demo,並沒有解析子應用資源配置表來加載相應資源。在入口中我們注冊了子應用,並確定了子應用的激活時機。
子應用資源配置表是完全自定義的,只要入口加載器這邊按照約定的規范來解析加載資源,並按照single-spa的生命周期鈎子來處理好這些資源的掛載。
我們還可以將一些公共的資源庫資源庫(如上vue、vue-router等)抽取到入口中,這樣各個子應用不需要再包含這些庫文件了,可以減小資源文件大小,提升加載速度。子應用中構建時要外置這些庫,比如用webpack構建時如下:
2、子應用入口
set-public-path.js
細心的同學就會注意到,子應用代碼中運行了set-public-path.js。那么這個文件是干嘛用的呢?先來看下:
import { setPublicPath } from 'systemjs-webpack-interop' setPublicPath('app1', 2)
從名字也能看出,systemjs-webpack-interop是針對在systemjs中使用webpack構建的bundle的場景的。眾所周知,webpack構建代碼時,可以通過output.publicPath選項指定要加載資源的url前綴,這在傳統的spa中不會有問題,但在single-spa的頁面中可能會有問題。比如output.publicPath: '/xx'的情況,webpack會認為異步資源加載的url域名為當前頁面的域名,這在傳統spa中不會有問題,但在single-spa的場景下異步資源就會加載失敗,因為子應用的異步資源與框架頁面的url域名並不是一樣的。所以需要各個子應用自行在入口中執行上述代碼,這會設置子應用的異步資源url前綴與子應用的入口js一致,這樣加載的路徑就不會錯誤了。
setPublicPath代碼如下:
export function setPublicPath(systemjsModuleName, rootDirectoryLevel) { if (!rootDirectoryLevel) { rootDirectoryLevel = 1; } if ( typeof systemjsModuleName !== "string" || systemjsModuleName.trim().length === 0 ) { throw Error( "systemjs-webpack-interop: setPublicPath(systemjsModuleName) must be called with a non-empty string 'systemjsModuleName'" ); } if ( typeof rootDirectoryLevel !== "number" || rootDirectoryLevel <= 0 || !Number.isInteger(rootDirectoryLevel) ) { throw Error( "systemjs-webpack-interop: setPublicPath(systemjsModuleName, rootDirectoryLevel) must be called with a positive integer 'rootDirectoryLevel'" ); } let moduleUrl; try { moduleUrl = window.System.resolve(systemjsModuleName); if (!moduleUrl) { throw Error() } } catch (err) { throw Error( "systemjs-webpack-interop: There is no such module '" + systemjsModuleName + "' in the SystemJS registry. Did you misspell the name of your module?" ); } __webpack_public_path__ = resolveDirectory(moduleUrl, rootDirectoryLevel); } function resolveDirectory(urlString, rootDirectoryLevel) { const url = new URL(urlString); const pathname = new URL(urlString).pathname; let numDirsProcessed = 0, index = pathname.length; while (numDirsProcessed !== rootDirectoryLevel && index >= 0) { const char = pathname[--index]; if (char === "/") { numDirsProcessed++; } } if (numDirsProcessed !== rootDirectoryLevel) { throw Error( "systemjs-webpack-interop: rootDirectoryLevel (" + rootDirectoryLevel + ") is greater than the number of directories (" + numDirsProcessed + ") in the URL path " + fullUrl ); } url.pathname = url.pathname.slice(0, index + 1); return url.href; }
三、single-spa的不足
-
如上面提到過,如果子應用初始化資源有多個文件(比如通常我們會將css、npm模塊抽離成一個單獨的文件),那么我們就要自行維護一個子應用資源列表並做一些額外處理,這個工作往往也是比較繁瑣的;
-
將多個子應用都集成在一個頁面中,css和js都是很有可能產生沖突的。雖然我們可以制定規范,比如各子項目使用唯一地命名前綴等,但這種人為約定往往又是不那么靠譜。對於css,我們還可以在構建時使用一些工具自動添加前綴,這樣可以比較靠譜的避免沖突;對於js來說,比較靠譜的方式可能就是人為制造沙箱,讓子應用的js都運行在各自的沙箱中,但這實現起來就比較復雜了。
四、qiankun
其實,已經有個基於single-spa的開源庫qiankun已經幫我們解決了上面提到的問題,其有如下特征:
-
解析子應用入口時,不是解析的js文件,二是直接解析子應用的html文件。就算子應用更新了,其入口html文件的url始終不會變,並且完整的包含了所有的初始化資源url,所以不用再自行維護子應用的資源列表了。
-
子應用掛載時,會自動進行一些特殊處理,可以確保子應用所有的資源dom(包括js添加的style標簽等)都集中在子應用根節點dom下。子應用卸載時,對應的整個dom都移除了,這樣也就避免了樣式沖突。
-
提供了js沙箱,子應用掛載時,會對全局window對象代理、對全局事件監聽進行劫持等,確保各子應用都運行在自己的沙箱內,這樣也就避免了js沖突。
包含多個spa應用的demo
子應用 dom 結構如下
當然,在前端越來越龐大復雜的場景中,微前端方案也不是銀彈,但確是值得探索實踐的方向。
五、參考文獻
更多內容敬請關注 vivo 互聯網技術 微信公眾號
注:轉載文章請先與微信號:Labs2020 聯系。