網易七魚是提供圍繞客戶服務與智能營銷的 SaaS 平臺。在七魚業務中,有在線系統、呼叫系統、機器人、工單系統、數據大屏等業務線,它們分佈在兩個業務端,管理端和客服端。這兩個端的功能框架相似,都是由外層框架(頂部導航、一級菜單)及中間的內容區組成。css
隨着業務體量的增大與功能的增多,主系統做爲一個巨石應用複雜度愈來愈高,全部的業務線耦合在一塊兒,在系統構建、業務分離、開發維護方面帶來了新的挑戰。html
爲解決以上問題,咱們最初採用了 「MPA + iframe」 的技術方案。先按業務維度從巨型單體應用中拆分出多個子應用,並用 React 技術棧對它們進行了重構,經過 iframe 的方式隔離新老技術棧。這些子應用基於 URL 解耦,每一個子應用能夠獨立開發、運行和部署。前端
採用「MPA + iframe」 的技術方案是一把雙刃劍,用它能夠較方便地解決現有的問題,但同時也帶來了一些新的問題。react
用 MPA 方案能夠容許子應用使用不一樣技術棧,父子應用之間自然隔離,可是瀏覽器頁面跳轉時不能保持單頁應用的流暢體驗,父子應用通訊困難。webpack
用 iframe 能夠方便地隔離新老技術棧,可是也帶來了一些問題:git
問題 | 舉例 | 較好的解決方案 |
---|---|---|
父子框架 URL 不一樣步、瀏覽器前進後退按鈕異常 | -- | 定義父子框架路由映射,利用 postMessage 和 history API 解決 |
父子框架 UI 不一樣步 | 遮罩層只能遮蓋 iframe 所在的區域、iframe 內的彈框沒法相對外層頁面居中 | 無 |
子框架的全局上下文與父框架徹底隔離,致使父子框架通訊困難、同步數據冗餘 | -- | 無 |
加載慢,體驗較差 | -- | 無 |
項目最開始時採用的開發框架是 NEJ(Nice Easy Javascript),它的依賴管理系統、控件系統等特性爲早期的項目開發作出了很大的貢獻,如今它完成了本身的歷史使命,項目開始向 React 技術棧過渡。github
下圖展現了應用框架現狀:web
能夠看到,整個系統中使用了 NEJ 和 React 兩套技術棧。npm
React 外層框架內部嵌入的是 React 應用,這些應用分別引用了各自的外層框架,並經過 React 業務組件庫複用。json
NEJ 外層框架內部的狀況則比較複雜,部分場景嵌入的是 NEJ 應用,還有部分場景是經過 iframe 嵌入的 React 應用,這些 React 應用中的部分頁面中也有經過 iframe 再次嵌入 NEJ 應用的場景。
由於 NEJ 老技術棧的組件支持匱乏,並且歷史遺留代碼較多,致使它們的開發和維護成本都很高。
目前前端工程正處於技術棧統一的過渡期,須要維護兩套外層框架,後續將逐漸由 NEJ 轉向 React。對於新增的應用,則直接採用 React 技術棧。
隨着新應用的增多,外層框架被引用的次數愈來愈多,每次更新都須要發佈多個應用,使用新技術棧外層框架的維護成本爲愈來愈高。
微前端是目前比較火的話題,它是微服務在前端領域的擴展。它將前端總體拆分爲多個更小、更易管理的片斷,能夠解決工程複雜度高、多技術棧共存、開發維護困難等問題。微前端的兩大特性,微應用技術棧無關,每一個微應用能夠獨立開發、運行和部署,能夠很好的匹配現有的業務場景。
所以咱們將目光轉到了對現有應用進行微前端改造上。
將現有的應用進行微前端改造能夠帶來如下好處:
社區內的微前端解決方案有許多種,包括:
綜合考慮業務場景、上手難度、文檔友好性、代碼入侵性、可維護性等方面,最終選擇的微前端解決方案是 qiankun。接下來就是基於 qiankun 的微前端改造了。
七魚的微前端改造,從技術層面涉及到 React、NEJ 兩類技術棧,從業務層面涉及到管理端、客服端。
由於最終目的是全部前端工程統一到 React 技術棧,而管理端部分應用的外層框架已經用 React 重構過,因此先從管理端下手。
首先分別重新、老技術棧應用中選取一個應用進行改造,積累相關經驗。應用選擇的標準是無複雜的業務邏輯、流量少,以下降改造風險。新技術棧應用選的是首頁應用,老技術棧應用選的是數據大屏應用。
來看一下七魚微前端改造後的主頁:
這裏說明兩個概念,基座應用(也稱爲主應用、框架應用等)和子應用(也稱爲微應用):
**
能夠看到,上圖用紅框標出了主頁的兩個組成部分,外層框架(頂部導航、一級菜單)和中間內容區。
外層框架就是由基座應用控制的,經過監聽 URL 進行路由分發、子應用調度等。內容區由一個或多個子應用控制,上圖中的內容區就是由一個首頁子應用控制的。
建立管理端基座工程 basic-admin;
const webpackConfig = { //... output: { //... library: `${packageName}-[name]`, // 此處的packageName爲子應用名,如micro-bigscreen libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${packageName}`, } };
新增微應用對應的內部路由,改造網關:
兼容七魚 PC 客戶端(低版本 Chrome 瀏覽器內核):
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', } ] );
微前端改造後,全部管理端相關子應用的 URL 前綴爲「/madmin/」,如主頁的 URL 爲「/madmin/home/」。服務網關須要將全部以「/madmin/」開頭的路由定向到管理端基座應用。
結合網關的微前端架構圖以下:
子應用有獨立的倉庫,部署完以後,將應用的發佈產物註冊到基座應用裏,這些產物能夠是子應用的訪問地址,也能夠是資源配置對象(scripts + styles + html)。
須要注意的是,在子應用與基座應用開發聯調時,子應用讀取的是基座應用的同步數據,Mock 的同步數據須要在基座應用中配置。同理,子應用用到的接口代理也須要在基座應用中配置全。
基座應用啓動後會監聽 URL 變化,當用戶訪問系統時,根據當前訪問的 URL 和註冊的路由信息,可以匹配到當前須要加載的子應用信息,而後去加載子應用的資源並渲染子應用。
當用戶點擊觸發跳轉時,若是路由變化觸發的是一個內部 URL 跳轉,會直接根據應用內部的路由邏輯渲染頁面。若是路由變化觸發的是跨應用的跳轉,則從新回到上面的路由匹配的流程中。
下圖是微前端改造後的應用框架:
按照上述的子應用改造過程,能夠逐步完成管理端的微前端改造。接下來就是對客服端的微前端改造了。
雖然客服端與管理端的框架結構相似,可是它們的 URL 是解耦的,並且它們一級菜單和頂部導航的業務功能差異較大,共用同一個基座應用會致使應用複雜度太高,最好是另外建立一個客服端專用的基座應用,兩個基座應用經過業務組件庫複用組件。
將來總體的應用框架以下:
有了微前端的助力,整個系統能夠更加平滑地進行技術棧升級,最終實現前端技術棧的統一,更高效地賦能業務發展。
babel-polyfill 不支持引用屢次(基座應用和子應用分別引用了一次),直接去除 babel-polyfill 會致使沒法單獨運行子應用,能夠改用 idempodent-babel-polyfill。
資源路徑有問題,須要配置運行時的 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 + "/"; }
將 sandbox 設置爲 strictStyleIsolation,會啓用嚴格的樣式隔離,原理是把子應用內容渲染到基座容器的 shadow dom 中,致使沒法直接獲取基座應用的 dom 元素。
取消 strictStyleIsolation,只設置 jsSandBox 爲 true 就不會有問題。
樣式隔離的最佳實踐是採用約定式隔離:用 CSS 命名空間、CSS Module、css-in-js 等工程化手段,避免寫全局樣式。
開發環境使用 browserSync 進行瀏覽器同步,qiankun 框架經過瀏覽器的 fetch API 獲取子應用的資源,會存在跨域問題,因此須要設置 cors
爲 true。
browserSync({ //... cors: true });
添加條件判斷,非 qiankun 環境下,走以前的運行環境。
修改 'entry.js' 的 render 條件:
if (!window.__POWERED_BY_QIANKUN__) { ReactDOM.render( <Root store={store} history={history} routes={routes}/>, document.getElementById('react-content') ); }
使用 ScriptExtHtmlWebpackPlugin 插件修改 webpack 配置,爲每一個頁面的入口 js 加 entry 屬性。
tplPlugins.push( new ScriptExtHtmlWebpackPlugin({ custom: { test: /(?<!vendors.*)entry\.js$/, attribute: 'entry' } } ));
在子應用與基座應用開發聯調時,子應用讀取的是基座應用的全局配置。本地環境基座應用可能接入不少子應用,其餘子應用用到的接口代理要配全,不然調不到接口。同理,Mock 的同步數據也要在基座應用配置全。
qiankun 框架經過瀏覽器的 fetch API 獲取子應用的資源。Chrome 內核71及以前的版本,即便網址與調用腳本同源,fetch API 也不會自動發送 cookie。
須要在基座應用中啓動應用時,對 fetch 進行顯式的參數配置:
qiankun.start({ //... fetch: (url, init) => { return window.fetch(url, { ...init, credentials: 'same-origin' // 在當前域名內自動發送 cookie }); } });
定義一個與子應用名稱一致的全局變量,生命週期鉤子函數必須返回 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);
PC 客戶端注入了 window.cefQuery 與 window.cefQueryCancel 變量,它們的屬性描述符中 writable 與 configurable 都爲 false,通過 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 框架,建立了管理端基座應用,將管理端首頁和數據大屏應用進行了微前端改造,改造涉及 React 和 NEJ 兩套技術棧,達到了如下目的:
微前端不是一個框架,而是一套架構體系,基座應用的建立和子應用的改造是它的基礎設施,除了基礎設施外還有配置中心和觀察工具。配置中心包括參數配置、版本管理、發佈策略等。觀察工具備必定的運維職能,包括應用狀態的可見、可控性等。
有了上述能力後,能夠經過它們統一管控全部的微應用,爲 SaaS 產品提供自由組合的能力,使技術爲業務帶來更大的價值。
更多技術內容,歡迎關注【網易智企技術+】公衆號。