微前端在网易七鱼的实践
一、前言
網(wǎng)易七魚是提供圍繞客戶服務與智能營銷的 SaaS 平臺。在七魚業(yè)務中,有在線系統(tǒng)、呼叫系統(tǒng)、機器人、工單系統(tǒng)、數(shù)據(jù)大屏等業(yè)務線,它們分布在兩個業(yè)務端,管理端和客服端。這兩個端的功能框架類似,都是由外層框架(頂部導航、一級菜單)及中間的內容區(qū)組成。
二、業(yè)務現(xiàn)狀
隨著業(yè)務體量的增大與功能的增多,主系統(tǒng)作為一個巨石應用復雜度越來越高,所有的業(yè)務線耦合在一起,在系統(tǒng)構建、業(yè)務分離、開發(fā)維護方面帶來了新的挑戰(zhàn)。
為解決以上問題,我們最初采用了 「MPA + iframe」 的技術方案。先按業(yè)務維度從巨型單體應用中拆分出多個子應用,并用 React 技術棧對它們進行了重構,通過 iframe 的方式隔離新老技術棧。這些子應用基于 URL 解耦,每個子應用可以獨立開發(fā)、運行和部署。
采用「MPA + iframe」 的技術方案是一把雙刃劍,用它可以較方便地解決現(xiàn)有的問題,但同時也帶來了一些新的問題。
用 MPA 方案可以允許子應用使用不同技術棧,父子應用之間天然隔離,但是瀏覽器頁面跳轉時不能保持單頁應用的流暢體驗,父子應用通信困難。
用 iframe 可以方便地隔離新老技術棧,但是也帶來了一些問題:
| 父子框架 URL 不同步、瀏覽器前進后退按鈕異常 | -- | 定義父子框架路由映射,利用 postMessage 和 history API 解決 |
| 父子框架 UI 不同步 | 遮罩層只能遮蓋 iframe 所在的區(qū)域、iframe 內的彈框無法相對外層頁面居中 | 無 |
| 子框架的全局上下文與父框架完全隔離,導致父子框架通信困難、同步數(shù)據(jù)冗余 | -- | 無 |
| 加載慢,體驗較差 | -- | 無 |
項目最開始時采用的開發(fā)框架是 NEJ(Nice Easy Javascript),它的依賴管理系統(tǒng)、控件系統(tǒng)等特性為早期的項目開發(fā)做出了很大的貢獻,現(xiàn)在它完成了自己的歷史使命,項目開始向 React 技術棧過渡。
下圖展示了應用框架現(xiàn)狀:
可以看到,整個系統(tǒng)中使用了 NEJ 和 React 兩套技術棧。
React 外層框架內部嵌入的是 React 應用,這些應用分別引用了各自的外層框架,并通過 React 業(yè)務組件庫復用。
NEJ 外層框架內部的情況則比較復雜,部分場景嵌入的是 NEJ 應用,還有部分場景是通過 iframe 嵌入的 React 應用,這些 React 應用中的部分頁面中也有通過 iframe 再次嵌入 NEJ 應用的場景。
因為 NEJ 老技術棧的組件支持匱乏,而且歷史遺留代碼較多,導致它們的開發(fā)和維護成本都很高。
目前前端工程正處于技術棧統(tǒng)一的過渡期,需要維護兩套外層框架,后續(xù)將逐漸由 NEJ 轉向 React。對于新增的應用,則直接采用 React 技術棧。
隨著新應用的增多,外層框架被引用的次數(shù)越來越多,每次更新都需要發(fā)布多個應用,使用新技術棧外層框架的維護成本為越來越高。
微前端是目前比較火的話題,它是微服務在前端領域的擴展。它將前端整體拆分為多個更小、更易管理的片段,可以解決工程復雜度高、多技術棧共存、開發(fā)維護困難等問題。微前端的兩大特性,微應用技術棧無關,每個微應用可以獨立開發(fā)、運行和部署,可以很好的匹配現(xiàn)有的業(yè)務場景。
因此我們將目光轉到了對現(xiàn)有應用進行微前端改造上。
三、微前端改造
改造的好處
將現(xiàn)有的應用進行微前端改造可以帶來以下好處:
-
積累實踐經(jīng)驗,為將來從巨石應用拆分及微前端改造做準備;
-
去除接入二方應用時使用的 iframe,優(yōu)化產(chǎn)品體驗;
-
收斂外層框架,提升研發(fā)效率,降低維護成本;
-
提供前端增量升級能力,后續(xù)可以更好地復用歷史代碼、實施漸進式重構;
社區(qū)內的微前端解決方案有許多種,包括:
-
Single-spa:只解決了應用之間的加載方案,沒有考慮其他的周邊問題;
-
qiankun:基于 single-spa,提供了更加開箱即用的 API,具備 JS 沙箱、樣式隔離、子應用并行等能力;
-
Icestark:約束了框架應用必須基于 React,不利于后續(xù)的技術棧優(yōu)化;
-
Magix:適合做單頁應用的項目,不支持多個實例,不滿足業(yè)務需求;
-
Luigi:是一個基于 iframe 的微前端框架,仍有前文提到的 iframe 帶來的產(chǎn)品體驗問題;
-
Ara Framework:是一個基于 Airbnb's Hypernova 的,由服務端渲染延伸出的微前端框架,接入時對原應用的侵入較多;
-
WidgetJS:是一個輕量級的微前端方案,文檔不夠友好;
綜合考慮業(yè)務場景、上手難度、文檔友好性、代碼入侵性、可維護性等方面,最終選擇的微前端解決方案是 qiankun。接下來就是基于 qiankun 的微前端改造了。
業(yè)務分析與改造效果
七魚的微前端改造,從技術層面涉及到 React、NEJ 兩類技術棧,從業(yè)務層面涉及到管理端、客服端。
因為最終目的是所有前端工程統(tǒng)一到 React 技術棧,而管理端部分應用的外層框架已經(jīng)用 React 重構過,所以先從管理端下手。
首先分別從新、老技術棧應用中選取一個應用進行改造,積累相關經(jīng)驗。應用選擇的標準是無復雜的業(yè)務邏輯、流量少,以降低改造風險。新技術棧應用選的是首頁應用,老技術棧應用選的是數(shù)據(jù)大屏應用。
來看一下七魚微前端改造后的主頁:
這里說明兩個概念,基座應用(也稱為主應用、框架應用等)和子應用(也稱為微應用): **
-
基座應用負責整體布局、子應用的配置和調度,一般包含各個子應用公有的部分,比如外層框架;
-
子應用負責自身業(yè)務邏輯的渲染;
可以看到,上圖用紅框標出了主頁的兩個組成部分,外層框架(頂部導航、一級菜單)和中間內容區(qū)。
外層框架就是由基座應用控制的,通過監(jiān)聽 URL 進行路由分發(fā)、子應用調度等。內容區(qū)由一個或多個子應用控制,上圖中的內容區(qū)就是由一個首頁子應用控制的。
大致的改造步驟
1. 創(chuàng)建管理端基座工程 basic-admin;
- 基座應用只包含各個子應用共有的部分;
2. 創(chuàng)建首頁子工程 micro-index、大屏子工程 micro-bigscreen,以及相應的應用和集群;
3. 在項目的入口文件里,暴露相應的生命周期鉤子,供 qiankun 識別;
4. 修改打包配置,使物料以 umd 的方式輸出,以 webpack 為例:
const webpackConfig = {//...output: {//...library: `${packageName}-[name]`, // 此處的packageName為子應用名,如micro-bigscreenlibraryTarget: 'umd',jsonpFunction: `webpackJsonp_${packageName}`,} };5. 新增微應用對應的內部路由,改造網(wǎng)關:
-
內部路由用于注冊子應用,正常情況下用戶無法直接訪問到;
-
改造后的網(wǎng)關需要將所有匹配到基座 URL 前綴的請求,都定向到基座應用;
6. 兼容七魚 PC 客戶端(低版本 Chrome 瀏覽器內核):
-
qiankun 加載資源時依賴的 fetch API 的兼容性問題;
-
因為 height 繼承等導致的樣式問題;
7. 在基座應用中調用 qiankun 的 API,將子應用注冊到基座應用,如:
registerMicroApps([{name: 'micro-index',entry: '//' + location.hostname + '/_MicroIndex',container: '#subapp-container',activeRule: '/madmin/home',},{name: 'micro-bigscreen',entry: '//' + location.hostname + '/_MicroBigscreen/index',container: '#subapp-container',activeRule: '/madmin/dashboard',}] );四、微前端架構下的業(yè)務變化
服務網(wǎng)關的變化
微前端改造后,所有管理端相關子應用的 URL 前綴為「/madmin/」,如主頁的 URL 為「/madmin/home/」。服務網(wǎng)關需要將所有以「/madmin/」開頭的路由定向到管理端基座應用。
結合網(wǎng)關的微前端架構圖如下:
子應用的開發(fā)模式
子應用有獨立的倉庫,部署完之后,將應用的發(fā)布產(chǎn)物注冊到基座應用里,這些產(chǎn)物可以是子應用的訪問地址,也可以是資源配置對象(scripts + styles + html)。
需要注意的是,在子應用與基座應用開發(fā)聯(lián)調時,子應用讀取的是基座應用的同步數(shù)據(jù),Mock 的同步數(shù)據(jù)需要在基座應用中配置。同理,子應用用到的接口代理也需要在基座應用中配置全。
基座應用的整體流程
基座應用啟動后會監(jiān)聽 URL 變化,當用戶訪問系統(tǒng)時,根據(jù)當前訪問的 URL 和注冊的路由信息,能夠匹配到當前需要加載的子應用信息,然后去加載子應用的資源并渲染子應用。
當用戶點擊觸發(fā)跳轉時,如果路由變化觸發(fā)的是一個內部 URL 跳轉,會直接根據(jù)應用內部的路由邏輯渲染頁面。如果路由變化觸發(fā)的是跨應用的跳轉,則重新回到上面的路由匹配的流程中。
下圖是微前端改造后的應用框架:
按照上述的子應用改造過程,可以逐步完成管理端的微前端改造。接下來就是對客服端的微前端改造了。
雖然客服端與管理端的框架結構類似,但是它們的 URL 是解耦的,而且它們一級菜單和頂部導航的業(yè)務功能差別較大,共用同一個基座應用會導致應用復雜度過高,最好是另外創(chuàng)建一個客服端專用的基座應用,兩個基座應用通過業(yè)務組件庫復用組件。
未來整體的應用框架如下:
有了微前端的助力,整個系統(tǒng)可以更加平滑地進行技術棧升級,最終實現(xiàn)前端技術棧的統(tǒng)一,更高效地賦能業(yè)務發(fā)展。
五、遇到的問題及解決方案
1、子應用接入基座應用后,babel-polyfill 報錯
babel-polyfill 不支持引用多次(基座應用和子應用分別引用了一次),直接去除 babel-polyfill 會導致無法單獨運行子應用,可以改用 idempodent-babel-polyfill。
2、基座應用訪問子應用資源報 404 錯誤
資源路徑有問題,需要配置運行時的 public path。
if (window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__) {__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } else {__webpack_public_path__ = window.location.protocol + "//" + window.location.host + "/"; }3、報錯提示找不到子應用容器
將 sandbox 設置為 strictStyleIsolation,會啟用嚴格的樣式隔離,原理是把子應用內容渲染到基座容器的 shadow dom 中,導致無法直接獲取基座應用的 dom 元素。
取消 strictStyleIsolation,只設置 jsSandBox 為 true 就不會有問題。
樣式隔離的最佳實踐是采用約定式隔離:用 CSS 命名空間、CSS Module、css-in-js 等工程化手段,避免寫全局樣式。
4、本地聯(lián)調時基座應用訪問子應用資源時報跨域錯誤
開發(fā)環(huán)境使用 browserSync 進行瀏覽器同步,qiankun 框架通過瀏覽器的 fetch API 獲取子應用的資源,會存在跨域問題,所以需要設置 cors 為 true。
browserSync({//...cors: true });5、子應用引入 qiankun 生命周期后,無法獨立運行
添加條件判斷,非 qiankun 環(huán)境下,走之前的運行環(huán)境。 修改 'entry.js'?的 render 條件:
if (!window.__POWERED_BY_QIANKUN__) {ReactDOM.render(<Root store={store} history={history} routes={routes}/>, ? ?document.getElementById('react-content')); }6、本地聯(lián)調時子應用因為有熱加載導致報錯
使用 ScriptExtHtmlWebpackPlugin 插件修改 webpack 配置,為每個頁面的入口 js 加 entry 屬性。
tplPlugins.push(new ScriptExtHtmlWebpackPlugin({custom: {test: /(?<!vendors.*)entry\.js$/,attribute: 'entry'}} ));7、本地聯(lián)調時子應用調用 Mock 接口或同步數(shù)據(jù)報錯
在子應用與基座應用開發(fā)聯(lián)調時,子應用讀取的是基座應用的全局配置。本地環(huán)境基座應用可能接入很多子應用,其他子應用用到的接口代理要配全,否則調不到接口。同理,Mock 的同步數(shù)據(jù)也要在基座應用配置全。
8、低版本瀏覽器加載資源時 cookie 丟失
qiankun 框架通過瀏覽器的 fetch API 獲取子應用的資源。Chrome 內核71及之前的版本,即使網(wǎng)址與調用腳本同源,fetch API 也不會自動發(fā)送 cookie。
需要在基座應用中啟動應用時,對 fetch 進行顯式的參數(shù)配置:
qiankun.start({//...fetch: (url, init) => {return window.fetch(url, {...init,credentials: 'same-origin' ?// 在當前域名內自動發(fā)送 cookie});} });9、非 React 環(huán)境引入 qiankun 生命周期的方式
定義一個與子應用名稱一致的全局變量,生命周期鉤子函數(shù)必須返回 promise,如果不支持 promise 需要引入 promise-polyfill。入口文件可以這樣寫:
(function(win) {// 此處的'micro-bigscreen'與注冊到基座應用的子應用名稱一致win['micro-bigscreen'] = {bootstrap: function() {// 必須返回promise,否則子應用無法正常啟動return Promise.resolve();},mount: function() {return Promise.resolve();},unmount: function() {return Promise.resolve();}}; })(window);10、PC 客戶端子應用變量訪問報錯:Uncaught TypeError: 'get' on proxy
PC 客戶端注入了 window.cefQuery 與 window.cefQueryCancel 變量,它們的屬性描述符中 writable 與 configurable 都為 false,經(jīng)過 JS 沙箱 Proxy 后直接訪問它們會報錯:Uncaught TypeError: 'get' on proxy。
因為只有子應用用到了沙箱,此報錯只會影響子應用,基座應用不受影響。
解決方法是:分別從 window.cefQuery 與 window.cefQueryCancel 復制出新的變量 window.cefQuery2 與 window.cefQueryCancel2,修改它們的屬性描述符 writable 與 configurable 為 true。然后將微前端子應用中引用 window.cefQuery 與 window.cefQueryCancel 的地方分別修改為 window.cefQuery2 與 window.cefQueryCancel2。
基座應用中的相關代碼:
const polyfillPcPlatform = () => {if (window.cefQuery) {Object.defineProperty(window, 'cefQuery2', {value: window.cefQuery,writable: true,configurable: true});}if (window.cefQueryCancel) {Object.defineProperty(window, 'cefQueryCancel2', {value: window.cefQueryCancel,writable: true,configurable: true});} }; ? //注冊子應用 registerMicroApps([//...],{beforeLoad: [app => {// 兼容PC客戶端polyfillPcPlatform();}],//...} );六、總結
本次微前端實踐基于 qiankun 框架,創(chuàng)建了管理端基座應用,將管理端首頁和數(shù)據(jù)大屏應用進行了微前端改造,改造涉及 React 和 NEJ 兩套技術棧,達到了以下目的:
積累了微前端實踐經(jīng)驗,為將來從巨石應用拆分及微前端改造做準備;
使管理端不同技術棧的二方應用接入不再需要使用 iframe,優(yōu)化了產(chǎn)品體驗;
收斂了管理端外層框架,使新應用的接入不再需要理會頂部導航和一級菜單;
提供了前端增量升級能力,后續(xù)可以更好地復用歷史代碼、實施漸進式重構;
微前端不是一個框架,而是一套架構體系,基座應用的創(chuàng)建和子應用的改造是它的基礎設施,除了基礎設施外還有配置中心和觀察工具。配置中心包括參數(shù)配置、版本管理、發(fā)布策略等。觀察工具有一定的運維職能,包括應用狀態(tài)的可見、可控性等。
有了上述能力后,可以通過它們統(tǒng)一管控所有的微應用,為 SaaS 產(chǎn)品提供自由組合的能力,使技術為業(yè)務帶來更大的價值。
總結
以上是生活随笔為你收集整理的微前端在网易七鱼的实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WebRTC 系列之视频辅流
- 下一篇: 2020 年值得再读一遍的网易云信技术干