1. 程式人生 > 實用技巧 >微前端在網易七魚的實踐

微前端在網易七魚的實踐

一、前言

網易七魚是提供圍繞客戶服務與智慧營銷的 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 產品提供自由組合的能力,使技術為業務帶來更大的價值。