微前端在網易七魚的實踐

一、前言

網易七魚是提供圍繞客戶服務與智能營銷的 SaaS 平臺。在七魚業務中,有在線系統、呼叫系統、機器人、工單系統、數據大屏等業務線,它們分佈在兩個業務端,管理端和客服端。這兩個端的功能框架類似,都是由外層框架(頂部導航、一級菜單)及中間的內容區組成。

二、業務現狀

隨着業務體量的增大與功能的增多,主系統作爲一個巨石應用複雜度越來越高,所有的業務線耦合在一起,在系統構建、業務分離、開發維護方面帶來了新的挑戰。

爲解決以上問題,我們最初採用了 「MPA + iframe」 的技術方案。先按業務維度從巨型單體應用中拆分出多個子應用,並用 React 技術棧對它們進行了重構,通過 iframe 的方式隔離新老技術棧。這些子應用基於 URL 解耦,每個子應用可以獨立開發、運行和部署。

採用「MPA + iframe」 的技術方案是一把雙刃劍,用它可以較方便地解決現有的問題,但同時也帶來了一些新的問題。

MPA 方案可以允許子應用使用不同技術棧,父子應用之間天然隔離,但是瀏覽器頁面跳轉時不能保持單頁應用的流暢體驗,父子應用通信困難。

iframe 可以方便地隔離新老技術棧,但是也帶來了一些問題:

問題 舉例 較好的解決方案
父子框架 URL 不同步、瀏覽器前進後退按鈕異常 -- 定義父子框架路由映射,利用 postMessage 和 history API 解決
父子框架 UI 不同步 遮罩層只能遮蓋 iframe 所在的區域、iframe 內的彈框無法相對外層頁面居中
子框架的全局上下文與父框架完全隔離,導致父子框架通信困難、同步數據冗餘 --
加載慢,體驗較差 --

項目最開始時採用的開發框架是 NEJ(Nice Easy Javascript),它的依賴管理系統、控件系統等特性爲早期的項目開發做出了很大的貢獻,現在它完成了自己的歷史使命,項目開始向 React 技術棧過渡。

下圖展示了應用框架現狀:

應用框架現狀

可以看到,整個系統中使用了 NEJReact 兩套技術棧。

React 外層框架內部嵌入的是 React 應用,這些應用分別引用了各自的外層框架,並通過 React 業務組件庫複用。

NEJ 外層框架內部的情況則比較複雜,部分場景嵌入的是 NEJ 應用,還有部分場景是通過 iframe 嵌入的 React 應用,這些 React 應用中的部分頁面中也有通過 iframe 再次嵌入 NEJ 應用的場景。

因爲 NEJ 老技術棧的組件支持匱乏,而且歷史遺留代碼較多,導致它們的開發和維護成本都很高。

目前前端工程正處於技術棧統一的過渡期,需要維護兩套外層框架,後續將逐漸由 NEJ 轉向 React。對於新增的應用,則直接採用 React 技術棧。

隨着新應用的增多,外層框架被引用的次數越來越多,每次更新都需要發佈多個應用,使用新技術棧外層框架的維護成本爲越來越高。

微前端是目前比較火的話題,它是微服務在前端領域的擴展。它將前端整體拆分爲多個更小、更易管理的片段,可以解決工程複雜度高、多技術棧共存、開發維護困難等問題。微前端的兩大特性微應用技術棧無關,每個微應用可以獨立開發、運行和部署,可以很好的匹配現有的業務場景。

因此我們將目光轉到了對現有應用進行微前端改造上。

三、微前端改造

改造的好處

將現有的應用進行微前端改造可以帶來以下好處:

  • 積累實踐經驗,爲將來從巨石應用拆分及微前端改造做準備;
  • 去除接入二方應用時使用的 iframe,優化產品體驗;
  • 收斂外層框架,提升研發效率,降低維護成本;
  • 提供前端增量升級能力,後續可以更好地複用歷史代碼、實施漸進式重構;

社區內的微前端解決方案有許多種,包括:

  • Single-spa:只解決了應用之間的加載方案,沒有考慮其他的周邊問題;
  • qiankun:基於 single-spa,提供了更加開箱即用的 API,具備 JS 沙箱、樣式隔離、子應用並行等能力;
  • Icestark:約束了框架應用必須基於 React,不利於後續的技術棧優化;
  • Magix:適合做單頁應用的項目,不支持多個實例,不滿足業務需求;
  • Luigi:是一個基於 iframe 的微前端框架,仍有前文提到的 iframe 帶來的產品體驗問題;
  • Ara Framework:是一個基於 Airbnb's Hypernova 的,由服務端渲染延伸出的微前端框架,接入時對原應用的侵入較多;
  • WidgetJS:是一個輕量級的微前端方案,文檔不夠友好;

綜合考慮業務場景、上手難度、文檔友好性、代碼入侵性、可維護性等方面,最終選擇的微前端解決方案是 qiankun。接下來就是基於 qiankun 的微前端改造了。

業務分析與改造效果

七魚的微前端改造,從技術層面涉及到 React、NEJ 兩類技術棧,從業務層面涉及到管理端、客服端。

因爲最終目的是所有前端工程統一到 React 技術棧,而管理端部分應用的外層框架已經用 React 重構過,所以先從管理端下手。

首先分別從新、老技術棧應用中選取一個應用進行改造,積累相關經驗。應用選擇的標準是無複雜的業務邏輯、流量少,以降低改造風險。新技術棧應用選的是首頁應用,老技術棧應用選的是數據大屏應用。

來看一下七魚微前端改造後的主頁:

七魚微前端改造後主頁

這裏說明兩個概念,基座應用(也稱爲主應用、框架應用等)和子應用(也稱爲微應用): **

  • **基座應用負責整體佈局、子應用的配置和調度,**一般包含各個子應用公有的部分,比如外層框架;
  • 子應用負責自身業務邏輯的渲染;

可以看到,上圖用紅框標出了主頁的兩個組成部分,外層框架(頂部導航、一級菜單)和中間內容區。

外層框架就是由基座應用控制的,通過監聽 URL 進行路由分發、子應用調度等。內容區由一個或多個子應用控制,上圖中的內容區就是由一個首頁子應用控制的。

大致的改造步驟

  1. 創建管理端基座工程 basic-admin;
    1. 基座應用只包含各個子應用共有的部分;
  2. 創建首頁子工程 micro-index、大屏子工程 micro-bigscreen,以及相應的應用和集羣;
  3. 在項目的入口文件裏,暴露相應的生命週期鉤子,供 qiankun 識別;
  4. 修改打包配置,使物料以 umd 的方式輸出,以 webpack 爲例:
const webpackConfig = {
    //...
    output: {
        //...
        library: `${packageName}-[name]`, // 此處的packageName爲子應用名,如micro-bigscreen
        libraryTarget: 'umd',
        jsonpFunction: `webpackJsonp_${packageName}`,
    }
};
  1. 新增微應用對應的內部路由,改造網關:
    1. 內部路由用於註冊子應用,正常情況下用戶無法直接訪問到;
    2. 改造後的網關需要將所有匹配到基座 URL 前綴的請求,都定向到基座應用;
  2. 兼容七魚 PC 客戶端(低版本 Chrome 瀏覽器內核):
    1. qiankun 加載資源時依賴的 fetch API 的兼容性問題;
    2. 因爲 height 繼承等導致的樣式問題;
  3. 在基座應用中調用 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',
    }
  ]
);

四、微前端架構下的業務變化

服務網關的變化

微前端改造後,所有管理端相關子應用的 URL 前綴爲「/madmin/」,如主頁的 URL 爲「/madmin/home/」。服務網關需要將所有以「/madmin/」開頭的路由定向到管理端基座應用。

結合網關的微前端架構圖如下:

微前端架構圖

子應用的開發模式

子應用有獨立的倉庫,部署完之後,將應用的發佈產物註冊到基座應用裏,這些產物可以是子應用的訪問地址,也可以是資源配置對象(scripts + styles + html)。

需要注意的是,在子應用與基座應用開發聯調時,子應用讀取的是基座應用的同步數據,Mock 的同步數據需要在基座應用中配置。同理,子應用用到的接口代理也需要在基座應用中配置全。

基座應用的整體流程

基座應用啓動後會監聽 URL 變化,當用戶訪問系統時,根據當前訪問的 URL 和註冊的路由信息,能夠匹配到當前需要加載的子應用信息,然後去加載子應用的資源並渲染子應用

當用戶點擊觸發跳轉時,如果路由變化觸發的是一個內部 URL 跳轉,會直接根據應用內部的路由邏輯渲染頁面。如果路由變化觸發的是跨應用的跳轉,則重新回到上面的路由匹配的流程中。

下圖是微前端改造後的應用框架:

微前端改造後的應用框架

按照上述的子應用改造過程,可以逐步完成管理端的微前端改造。接下來就是對客服端的微前端改造了。

雖然客服端與管理端的框架結構類似,但是它們的 URL 是解耦的,而且它們一級菜單和頂部導航的業務功能差別較大,共用同一個基座應用會導致應用複雜度過高,最好是另外創建一個客服端專用的基座應用,兩個基座應用通過業務組件庫複用組件。

未來整體的應用框架如下:

未來整體的應用框架

有了微前端的助力,整個系統可以更加平滑地進行技術棧升級,最終實現前端技術棧的統一,更高效地賦能業務發展。

五、遇到的問題及解決方案

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、本地聯調時基座應用訪問子應用資源時報跨域錯誤

開發環境使用 browserSync 進行瀏覽器同步,qiankun 框架通過瀏覽器的 fetch API 獲取子應用的資源,會存在跨域問題,所以需要設置 cors 爲 true。

browserSync({
  //...
  cors: true
});

5、子應用引入 qiankun 生命週期後,無法獨立運行

添加條件判斷,非 qiankun 環境下,走之前的運行環境。 修改 'entry.js' 的 render 條件:

if (!window.__POWERED_BY_QIANKUN__) {
  ReactDOM.render(
    <Root store={store} history={history} routes={routes}/>,    document.getElementById('react-content')
  );
}

6、本地聯調時子應用因爲有熱加載導致報錯

使用 ScriptExtHtmlWebpackPlugin 插件修改 webpack 配置,爲每個頁面的入口 js 加 entry 屬性。

tplPlugins.push(
  new ScriptExtHtmlWebpackPlugin({
    custom: {
      test: /(?<!vendors.*)entry\.js$/,
      attribute: 'entry'
    }
  }
));

7、本地聯調時子應用調用 Mock 接口或同步數據報錯

在子應用與基座應用開發聯調時,子應用讀取的是基座應用的全局配置。本地環境基座應用可能接入很多子應用,其他子應用用到的接口代理要配全,否則調不到接口。同理,Mock 的同步數據也要在基座應用配置全。

8、低版本瀏覽器加載資源時 cookie 丟失

qiankun 框架通過瀏覽器的 fetch API 獲取子應用的資源。Chrome 內核71及之前的版本,即使網址與調用腳本同源,fetch API 也不會自動發送 cookie。

需要在基座應用中啓動應用時,對 fetch 進行顯式的參數配置:

qiankun.start({
  //...
  fetch: (url, init) => {
    return window.fetch(url, {
      ...init,
      credentials: 'same-origin'  // 在當前域名內自動發送 cookie
    });
  }
});

9、非 React 環境引入 qiankun 生命週期的方式

定義一個與子應用名稱一致的全局變量,生命週期鉤子函數必須返回 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,經過 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 兩套技術棧,達到了以下目的:

  1. 積累了微前端實踐經驗,爲將來從巨石應用拆分及微前端改造做準備;
  2. 使管理端不同技術棧的二方應用接入不再需要使用 iframe,優化了產品體驗;
  3. 收斂了管理端外層框架,使新應用的接入不再需要理會頂部導航和一級菜單;
  4. 提供了前端增量升級能力,後續可以更好地複用歷史代碼、實施漸進式重構;

微前端不是一個框架,而是一套架構體系,基座應用的創建和子應用的改造是它的基礎設施,除了基礎設施外還有配置中心和觀察工具。配置中心包括參數配置、版本管理、發佈策略等。觀察工具有一定的運維職能,包括應用狀態的可見、可控性等。

有了上述能力後,可以通過它們統一管控所有的微應用,爲 SaaS 產品提供自由組合的能力,使技術爲業務帶來更大的價值。

更多技術乾貨,歡迎關注【網易智企技術+】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章