如何分三步來探索微前端架構(gòu)落地
正文
本文是前端早早聊的第 38 位講師,也是第七屆 - 前端微前端專場,來自宋小菜 團(tuán)隊偉林 的分享 - 講稿簡要整理版(完整版含演示請看錄播視頻和 PPT)
主持人介紹
這一場分享是來自宋小菜前端總架構(gòu)組的偉林,那么偉林是宋小菜前端團(tuán)隊基礎(chǔ)架構(gòu)組得核心成員,長期從事與前端基建方面的工作,特別是中后臺的腳手架,配套研發(fā)設(shè)施,以及微前端方面的工程體系架構(gòu)升級等工作。
主要內(nèi)容

分享大綱
微前端(概念)認(rèn)知史 為什么需要微前端 調(diào)研到落地實踐

主要內(nèi)容
多應(yīng)用集成 - Qiankun(乾坤) 單體拆分 - Federation 的探索
微前端(概念)認(rèn)知史

17 年我才聽到微前端這個概念。當(dāng)時是一位 Java 同事分享了一篇關(guān)于微前端的 ThoughtWorks 文章。后來發(fā)現(xiàn) 16 年便開始流傳這個概念了。
18 年 microfrontend.org 的站長在一場 JS 大會上做了演講,實際上那時我沒怎么細(xì)看,沒有案例空講實在太虛了。那段時間 Dan Ambramov 還發(fā)推表示對微前端不解,覺得它的價值沒有被捧得那么的大。甚至有人吐槽單頁應(yīng)用本身都寫不好,還想著微前端這個花里胡哨的東西。再后來便出現(xiàn)了 single-spa 這個庫,慢慢地它的周邊生態(tài)開始涌現(xiàn)。
19 年在知乎第一次了解到乾坤。它是基于 single-spa 封裝,加上獨辟蹊徑的隔離方案和沙箱機制解決了 single-spa 的不足,讓人眼前一亮。
20 年在自己公司遇到了一部分需求和痛點,在調(diào)研多個庫和框架(分別是 Luigi、feature-hub、Single-spa、qiankun 還有 Webpack5 的 federation)后便開始正式實施。就是在五一之后開始做的,如果再晚點就沒這場分享了。最近我看到 Dan 的一條 Twitter 表示明白了微前端能帶來什么。
開始前也先總結(jié)一句“微前端不是銀彈,它并沒有多么高深莫測”。
為什么需要微前端

不服務(wù)于業(yè)務(wù)的技術(shù)是沒有價值的,我們需要以終為始。微前端給我們公司帶來了一些特殊的價值,抽象看就是兩類價值。
業(yè)務(wù)價值
在用戶這一側(cè),偏向產(chǎn)品和業(yè)務(wù)價值,尤其體現(xiàn)在多應(yīng)用集成時帶來的好處。
我們遇到了以下痛點:
新的運營同學(xué)覺得中后臺應(yīng)用過多,記不住。 UED 覺得很多的應(yīng)用交互體驗不一致。常見的就是菜單和頭部寬高不一致。雖然都是用的 Ant-design,但各個系統(tǒng)的全局的 UI 都是不一致的。 運營在多個應(yīng)用中操作時有時存在斷層,他們期望有工作臺的感覺(我們也期望給他們這種感覺)。同時在未來我們需要接入統(tǒng)一的工單通知,我們想把這塊維護(hù)一個應(yīng)用中,不用每個應(yīng)用都接入一次。
通過微前端能力整合多個應(yīng)用就可以解決上述的痛點。
工程價值
在開發(fā)者這一側(cè),則偏向工程價值。在多應(yīng)用集成場景里,我們可以做到統(tǒng)一管理應(yīng)用,使得應(yīng)用的申請入駐、獲得應(yīng)用配置、發(fā)布平臺對接等等的生命周期都可以被維護(hù)起來。在之前較為混亂,存在口頭索要 appKey、SysID 這種情況。如果能有一個統(tǒng)一的入駐平臺,并且用工作臺的方式將他們管理起來,這些問題都可以被解決。
利用微前端拆分單體巨大應(yīng)用,則會帶來更大的工程價值,我個人其實覺得工程價值更大。將一個巨大的應(yīng)用按模塊拆分,可以使得團(tuán)隊分開維護(hù),使得工程師能踏出部分泥潭模塊。在拆解后,按需要分開獨立發(fā)布模塊也使得發(fā)布速度也得到提升,產(chǎn)品和業(yè)務(wù)方會減少焦慮感,用戶自然也是受益的。(在我們內(nèi)部有一個應(yīng)用就因為某個模塊太過巨大,經(jīng)常發(fā)布失敗。)
調(diào)研到落地實踐

開發(fā)前我還沒有具體實踐過 single-spa 和 qiankun 以及 liuji 等等。但我們可以幻想一下,多個應(yīng)用或者模塊集成在一起會遇到什么問題。

可能遇到的問題
第一,全局樣式的沖突。我們最先想到的是 Shadow DOM,但是它是有兼容性問題的,而且在 React 上也有事件代理的坑,對于子應(yīng)用來說會有巨大的改造成本。另外我們還能想到利用 CSS 前綴的方式,給樣式加上 Scope 或者利用 css-in-js 的方案去處理,不過有一定的改造成本,但也是可以接受的。
第二,JS 可能存在全局污染的問題。tc39 的 realms 沙盒提案瀏覽器還沒支持,可能需要自己實現(xiàn)。其實,我覺得這點毛病不是特別特別得強烈,畢竟現(xiàn)在大多數(shù)包都是 bundle 過得。
第三,某些庫多版本。例如 React 存在多版本,過去某些系統(tǒng)可能想打包提速,會利用 Webpack externals 的能力,但我們不能一下統(tǒng)一所有應(yīng)用的 React 版本,所以利用需要利用 DLL,同時達(dá)到復(fù)用目的。

利用 iframe 去解決?
針對前兩個問題,我們可能會想到利用 iframe 去解決。它似乎可以解決以上污染和沖突問題。但我們可以可以想象一種情景:在 iframe 內(nèi)打開某個應(yīng)用,并導(dǎo)航了幾次,此時刷新一下頁面,iframe 的狀態(tài)就沒了。有人會說持久化記錄一下不行嗎?可以,但是還有其他的問題,比如它加載速度的缺陷、父子通信的問題,總之使用成本很大。對此方案沒抱多大希望。

在想了一些比較細(xì)節(jié)的問題后,在思考一下拆分集成的粒度問題,說到底微前端就是一種拆解類的問題。

這是我做拆解類問題喜歡用的一張圖。在我們公司的場景里,我們的第一訴求是應(yīng)用的集成,少部分較大應(yīng)用才會涉及單應(yīng)用的拆解。多應(yīng)用的集成就是將從前 App 這一層的應(yīng)用降到 Module 這一側(cè),再利用 Host 應(yīng)用(主應(yīng)用)加載進(jìn)來,而單體的拆分則是將單個 APP 按照上面不同的維度,拆解出不同的 Module 或是 Class 級別的包。
應(yīng)用集成

前面說到技術(shù)調(diào)研,SAP 開源的 Luigi 是基于 Iframe 的,把玩了下就直接排除了。Feature-hub 是模塊級別的,更符合單體拆分的場景,但是改造成本很大,有學(xué)習(xí)成本。那么只剩下 single-spa 和 qiankun 的對比了。
single-spa 是較為簡陋的,只是劫持了單頁應(yīng)用的路由變換(感興趣的同學(xué)可以去看它的測試用例),沒有考慮到樣式的隔離和 JS 執(zhí)行的沙盒,這兩點需要我們自己實現(xiàn)集成。同時,模塊加載的能力,它也不具備,一般輔以 SystemJS,當(dāng)然還可以結(jié)合我們后面會提到的 federation 。
相比之下,qiankun 幫我們實現(xiàn)了沙盒機制和一套另辟蹊徑的樣式隔離方案,在多應(yīng)用集成場景下非常適合。為什么說是另辟蹊徑呢?正常的思路,也就是在我幻想階段時,我想可能要利用 Webpack 打包出的 manifest 加載,但是這樣 HTML 的部分是照顧不到的。
當(dāng)然 qiankun 也不是完美的,因為獨辟蹊徑的方式就是利用 import-html-entry 加載解析 HTML。這是有一定損耗的,但在權(quán)衡下,乾坤最為簡單直接,還有 preload 機制預(yù)加載,對我們而言,其實完全是可以接受的。

它的大致用法也是非常簡單的,主應(yīng)用只需要注冊一下子應(yīng)用的信息,比如掛載節(jié)點、應(yīng)用名(注意應(yīng)用名稱唯一)、路由規(guī)則,然后設(shè)置一下默認(rèn)加載的應(yīng)用,最后 start 一下就可以了。對于子應(yīng)用而言就更簡單了,只需要更改自己 Webpack 打包配置,將自身打包成 umd 的模塊,再暴露出主應(yīng)用需要的生命周期即可。

遇到的實際問題
在正式實施開發(fā)時,我們遇到了一些問題。
第一,重復(fù)配置。我們將自應(yīng)用重復(fù)的配置抽成了一個解決方案插件。
第二,dll 配置。除了要將加載應(yīng)用的 libraryTarget 設(shè)置為 umd 還需要設(shè)置 dll 的,不然會加載失敗??梢钥吹?qiankun 沙盒內(nèi)部是靠 eval 去執(zhí)行的,它沒有義務(wù)幫你解析這個 var (dll 默認(rèn)是 var 類型的導(dǎo)出方式),同時嚴(yán)格模式下也不允許。
第三,Ant Design modal 銷毀問題??梢岳?getContainer 指定局部渲染的節(jié)點。新版 Ant Design 中簡單 false 一下即可,舊版本的則需要你指定特定的 DOM 節(jié)點,可以自己包裝一個 Modal 出來達(dá)到復(fù)用目的。
第四,父子通訊。一開始我以為需要自己想辦法,例如利用原生事件或者約定在 window 上的某個模塊進(jìn)行通信。這里 qiankun 和 single-spa 是一樣的,可以利用 props 通過生命周期參數(shù)注入。
第五,我們將部分公用的 Redux model 提升到主應(yīng)用,子應(yīng)用就不要去再重復(fù)加載。此時注入給子應(yīng)用就會存在多個 store ??梢岳?react-redux connect 的高級用法,在新版中是利用 context,我們有些還是 5.x,可以用 storeKey 的方式解決。

集成也分簡單模式和精細(xì)模式。
簡單模式就是改造完子應(yīng)用,主應(yīng)用提供空白的整頁給其渲染,導(dǎo)航則利用一個浮層導(dǎo)航器(fixed),但這非常簡單粗暴。
精細(xì)模式則是指主應(yīng)用僅僅提供 Content 區(qū)域給子應(yīng)用。同時再做一些額外的改造,協(xié)定菜單的數(shù)據(jù)結(jié)構(gòu),將定制菜單的管理類傳給子應(yīng)用,在子應(yīng)用加載完畢時進(jìn)行增刪,這份數(shù)據(jù)是可以緩存的,所以下次就可以更快的展現(xiàn)。對于那些不標(biāo)準(zhǔn)的 Header 和 Footer,我們也提供定制的接口給它們,在 unmount 時復(fù)原即可。對于接口加載上,我們統(tǒng)一請求庫,再利用 LRU 的能力去緩存接口,以達(dá)到多應(yīng)用間復(fù)用接口的目的。
以上就是多應(yīng)用集成時我們遇到的實際問題和解決辦法,下面我們講講單體拆分。

單體拆分
單體拆分簡單說就是要把一個大應(yīng)用拆出一部分出來,然后遠(yuǎn)程加載它們。一般來說,比起多應(yīng)用集成,單個應(yīng)用的拆分更適合大模塊分活干和分開管理維護(hù)的情況。這樣的應(yīng)用本身就有一定約束。比如,統(tǒng)一的技術(shù)棧(React),使用同一個 UI 庫 (Ant Design)一般也有統(tǒng)一的交互標(biāo)準(zhǔn)。從 single-spa 文檔上取了一個拆解方式的對比圖,簡單對比一下,只有動態(tài)加載模塊的方式才能滿足分開構(gòu)建、分開部署且是代碼倉庫獨立的需求。

單體拆分-動態(tài)加載
因為我們其中一個系統(tǒng)較大(用戶模塊、微信管理、報表分析模塊、內(nèi)容管理等等),發(fā)布非常慢,偶爾還會發(fā)布失敗。因此需要將一部分劃分出去,單獨管理。
一開始我是使用的 [email protected],它無法滿足我們這邊的需求。因為當(dāng)我們按照領(lǐng)域劃分出業(yè)務(wù)模塊后,有的業(yè)務(wù)模塊目前頁面數(shù)量很少,沒有特殊的理由去說服用戶去改變菜單的使用習(xí)慣。但如果是那種塊很多,菜單也很臃腫的應(yīng)用,那么多抽離一級業(yè)務(wù)域的菜單或是切換器,qiankun 完全是可以用。那么采用 single-spa 的 parcel 和 SystemJS 嗎?或者 [email protected](2.0支持了 parcel) 嗎?其實它們都是可行的,但還不夠完美。

這是 Webpack 作者最近的一條 Twitter。關(guān)注 Webpack 動態(tài)的同學(xué),可能知道 federation 已經(jīng)支持好一段時間了,但也就是在那天 federation 才有了一個 concept 的文檔,API 文檔目前還沒有。那我們看下 federation 相比過去的拆解方案強在哪里。
federation

externals 簡單粗暴,無法處理多版本共存時的問題。dll 將所有包都打入,模塊間無法共享代碼。而 federation 既可以做到版本控制,還可以動態(tài)判斷是否存在缺失的 vendor 包并加載。同時,它還克服了用 npm 包開發(fā)公共模塊時的不便利。

這兩張圖是從 federation 作者文章里盜來的,哈哈??梢哉f他們非常形象地描述了 federation 的作用。例如,landing site 的摸個模塊是可以被 media site 拿到的,同理,landing site 也可以共享模塊給 media site 。對于那些公共的模塊,則可以直接抽離到單獨的倉庫,按需引入,并且在某個模塊加載后,別的模塊如何也引入了此公共模塊,這時不會發(fā)生資源請求。具體大家可以看左下角這個倉庫中的例子。

我們看下例子中是如何使用的。
第一步,在 Webpack5 中加入 federation 的插件。我們先從插件參數(shù)看,第一個參數(shù) name 是聲明這個 federation 的名稱,第二個參數(shù) libarary 是聲明暴露成 libarary 時的類型和名字,第三個參數(shù) filename 是申明打包出的運行時配置文件的名稱。第四個參數(shù) exposes 是指出你要暴露出的具體文件(模塊)有哪些。第五個參數(shù) remotes 是指定從遠(yuǎn)程加載的應(yīng)用名稱以及它在引入時的名字。第六個參數(shù) shared 是指定需要共享的模塊(包)。
第二步,在應(yīng)用的 HTML 中引入 運行時配置,這點和 SystemJS 很像。
第三步,在應(yīng)用中,我們按照 remotes 中配置的遠(yuǎn)程模塊,進(jìn)行加載即可,和正常的代碼無異。

對我們而言就是將公共模塊和業(yè)務(wù)域模塊進(jìn)行拆分 exposed ,接著 shared 出來。

還有一點建議提給大家。這個巨大的單體應(yīng)用經(jīng)過多個人開發(fā),所以二級菜單和二級頁面放置比較混亂,在拆分不同模塊時花費了不少時間。平時開發(fā)時就要注意按領(lǐng)域去劃分二級子頁面,做到未雨綢繆。

實施的最后一塊是發(fā)布問題。簡答模式就是什么都不用考慮,正常發(fā)布即可。精細(xì)模式則要考慮平滑上線的問題。

讓我們仔細(xì)想象一種場景:子模塊先發(fā)布成功,但是其依賴了還未發(fā)布的主應(yīng)用的 api,那么應(yīng)用就會崩潰,反之亦然。這點在可用性上的要求和后端是一樣的。
那如何解決呢?首先,我們要 hash 化子應(yīng)用的 HTML。但 HTML 的版本機制意味著配置動態(tài)化。大家想想一想前面乾坤那邊的配置,我們要改動主應(yīng)用中注冊的子應(yīng)用的地址。這份配置動態(tài)就需要從服務(wù)端獲取,因此還需要開發(fā)一個配置中心。同時子應(yīng)用發(fā)布時需要通知配置中心發(fā)布成功了,并將 index_[hash].html 或是 federation 的runtime 配置地址傳給配置中心。最后,發(fā)布平臺上還要保證發(fā)布的順序。主應(yīng)用如果有更改,必須編排在最后一個發(fā)布。
總結(jié)和規(guī)劃

簡單事簡單做,讓你的架構(gòu)隨著你的業(yè)務(wù)規(guī)模而改動,適合的才是最好的。也許不一定每個人都需要多應(yīng)用集成這種場景,但是單體拆分確實是微前端的帶來的巨大的工程價值點。

最后,說下我們整體的規(guī)劃吧。我們想做一個入駐平臺,銜接和管理應(yīng)用的生命周期。簡單說就是將應(yīng)用從申請入駐 => [配置中心注冊、配置中心分發(fā)配置、監(jiān)控平臺對接、服務(wù)端配置對接] => 基礎(chǔ)框架獲取配置初始化項目 => 運行時獲取配置 => 發(fā)布平臺對接 => [用戶側(cè)配置獲取、通知機制、增量發(fā)布] 的完整流程都管理到。

說了很多,五月份開發(fā)的時間不長,部分還在規(guī)劃實施中。目前現(xiàn)在發(fā)布方案還是簡單模式,灰度方案和通知機制也沒有集成。在多應(yīng)用集成時,對監(jiān)控平臺也造成了開發(fā)量,因為域名變成了子 path 。
