微前端框架 qiankun 技術分析


我們在single-spa 技術分析 基本實現了一個微前端框架需要具備的各種功能,但是又實現的不夠徹底,遺留了很多問題需要解決。雖然官方提供了很多樣例和最佳實踐,但是總顯得過於單薄,總給人一種“問題解決了,但是又沒有完全解決”的感覺。

qiankun 在 single-spa 的基礎上做了二次開發,完善了很多功能,算是一個比較完備的微前端框架了。今天我們來聊一聊 qiankun 的技術原理。

在本系列的開頭,我們提到微前端的核心問題其實就是解決如何加載子應用以及如果做好子應用間的隔離問題。所以,我們從這兩點來看 qiankun 的實現。

如何加載子應用

single-spa 通過 js entry 的形式來加載子應用。而 qiankun 采用了 html entry 的形式。這兩種方式的優缺點我們在理解微前端技術原理中已經做過分析,這里不再贅述,我們看看 qiankun 是如何實現 html entry 的。

qiankun 提供了一個 API registerMicroApps 來注冊子應用,其內部調用 single-spa 提供的 registerApplication 方法。在調用 registerApplication 之前,會調用內部的 loadApp 方法來加載子應用的資源,初始化子應用的配置。

通過閱讀 loadApp 的代碼,我們發現,qiankun 通過 import-html-entry 這個包來加載子應用。import-html-entry 的作用就是通過解析子應用的入口 html 文件,來獲取子應用的 html 模板、css 樣式和入口 JS 導出的生命周期函數。

import-html-entry

import-html-entry 是這樣工作的,假設我們有如下 html entry 文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>

<!-- mark the entry script with entry attribute -->
<script src="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js" entry></script>
<script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
</body>
</html>

我們使用 import-html-entry 來解析這個 html 文件:

import importHTML from 'import-html-entry';

importHTML('./subApp/index.html')
    .then(res => {
        console.log(res.template);

        res.execScripts().then(exports => {
            const mobx = exports;
            const { observable } = mobx;
            observable({
                name: 'kuitos'
            })
        })
});

importHTML 的返回值有如下幾個屬性:

  • template 處理后的 HTML 模板
  • assetPublicPath 靜態資源的公共路徑
  • getExternalScripts 獲取所有外部腳本的函數,返回腳本路徑
  • getExternalStyleSheets 獲取所有外部樣式的函數,返回樣式文件的路徑
  • execScripts 執行腳本的函數

importHTML 的返回值中,除了幾個工具類的方法,最重要的就是 templateexecScripts 了。

importHTML('./subApp/index.html') 的整個執行過程代碼比較長,我們只講一下大概的執行原理,感興趣的同學可以自行查看importHTML 的源碼

importHTML 首先會通過 fetch 函數請求具體的 html 內容,然后在 processTpl 函數 中通過一系列復雜的正則匹配,解析出 html 中的樣式文件和 js 文件。

importHTML 函數返回值為 { template, scripts, entry, styles },分別是 html 模板,html 中的 js 文件(包含內嵌的代碼和通過鏈接加載的代碼),子應用的入口文件,html 中的樣式文件(同樣是包含內嵌的代碼和通過鏈接加載的代碼)。

之后通過 getEmbedHTML 函數 將所有使用外部鏈接加載的樣式全部轉化成內嵌到 html 中的樣式。getEmbedHTML 返回的 html 就是 importHTML 函數最終返回的 template 內容。

現在,我們看看 execScripts 是怎么實現的。

execScripts 內部會調用 getExternalScripts 加載所有 js 代碼的文本內容,然后通過 eval("code") 的形式執行加載的代碼。

注意,execScripts 的函數簽名是這樣的 (sandbox?: object, strictGlobal?: boolean, execScriptsHooks?: ExecScriptsHooks): Promise<unknown>。允許我們傳入一個沙箱對象,如果子應用按照微前端的規范打包,那么會在全局對象上設置 mountunmount 這幾個生命周期函數屬性。execScripts 在執行 eval("code") 的時候,會巧妙的把我們指定的沙箱最為全局對象包裝到 "code" 中,子應用能夠運行在沙盒環境中。

在執行完 eval("code") 以后,就可以從沙盒對象上獲取子應用導出的生命周期函數了。

loadApp

現在我們把視線拉回 loadApp 中,loadApp 在獲取到 templateexecScripts 這些信息以后,會基於 template 生成 render 函數用於渲染子應用的頁面。之后會根據需要生成沙盒,並將沙盒對象傳給 execScripts 來獲取子應用導出的聲明周期函數。

之后,在子應用生命周期函數的基礎上,構建新的生命周期函數,再調用 single-spa 的 API 啟動子應用。

在這些新的生命周期函數中,會在不同時機負責啟動沙盒、渲染子應用、清理沙盒等事務。

隔離

在完成子應用的加載以后,作為一個微前端框架,要解決好子應用的隔離問題,主要要解決 JS 隔離和樣式隔離這兩方面的問題。

JS 隔離

qiankun 為根據瀏覽器的能力創建兩種沙箱,在老舊瀏覽器中會創建快照模式 的瀏覽器中創建 VM 模式的沙箱 ProxySandbox

篇幅限制,我們只看 ProxySandbox 的實現,在其構造函數中,我們可以看到具體的邏輯:首先會根據用戶指定的全局對象(默認是 window)創建一個 fakeWindow,之后在這個 fakeWindow 上創建一個 proxy 對象,在子應用中,這個 proxy 對象就是全局變量 window

constructor(name: string, globalContext = window) {
  const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext);
  const proxy = new Proxy(fakeWindow, {
      set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {},
      get: (target: FakeWindow, p: PropertyKey): any => {},
      has(target: FakeWindow, p: string | number | symbol): boolean {},

      getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {},

      ownKeys(target: FakeWindow): ArrayLike<string | symbol> {},

      defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {},

      deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {},

      getPrototypeOf() {
        return Reflect.getPrototypeOf(globalContext);
      },
    });
  this.proxy = proxy;
}

其實 qiankun 中的沙箱分兩個類型:

  • app 環境沙箱
    app 環境沙箱是指應用初始化過之后,應用會在什么樣的上下文環境運行。每個應用的環境沙箱只會初始化一次,因為子應用只會觸發一次 bootstrap 。子應用在切換時,實際上切換的是 app 環境沙箱。
  • render 沙箱
    子應用在 app mount 開始前生成好的的沙箱。每次子應用切換過后,render 沙箱都會重現初始化。

上面說的 ProxySandbox 其實是 render 沙箱。至於 app 環境沙箱,qiankun 目前只針對在應用 bootstrap 時動態創建樣式鏈接、腳本鏈接等副作用打了補丁,保證子應用切換時這些副作用互不干擾。

之所以設計兩層沙箱,是為了保證每個子應用切換回來之后,還能運行在應用 bootstrap 之后的環境下。

樣式隔離

qiankun 提供了多種樣式隔離方式,隔離效果最好的是 shadow dom,但是由於其存在諸多限制,qiankun 官方在將來的版本中將會棄用,轉而推行 experimentalStyleIsolation 方案。

我們可以通過下面這段代碼看到 experimentalStyleIsolation 方案的基本原理。

const styleNodes = appElement.querySelectorAll('style') || [];
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
  css.process(appElement!, stylesheetElement, appInstanceId);
});

css.process 的核心邏輯,就是給讀取到的子應用的樣式添加帶有子應用信息的前綴。效果如下:

/* 假設應用名是 react16 */
.app-main {
  font-size: 14px;
}

div[data-qiankun-react16] .app-main {
  font-size: 14px;
}

通過上面的隔離方法,基本可以保證子應用間的樣式互不影響。

小結

qiankun 在 single-spa 的基礎上根據實際的生產實踐開發了很多有用的功能,大大降低了微前端的使用成本。

本文僅僅針對如何加載子應用和如何做好子應用間的隔離這兩個問題,介紹了 qiankun 的實現。其實,在隔離這個問題上,qiankun 也僅僅是根據實際中會遇到的情況做了必要的隔離措施,並沒有像 iframe 那樣實現完全的隔離。我們可以說 qiankun 實現的隔離有缺陷,也可以說是 qiankun 在實際的業務需求和完全隔離的實現成本之間做的取舍。

常見面試知識點、技術方案分析、教程,都可以掃碼關注公眾號“眾里千尋”獲取,或者來這里 https://everfind.github.io/posts/
眾里千尋


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM