首頁 > 軟體

qiankun 找不到入口問題徹底解決

2022-08-30 18:03:04

前言

嗨害嗨,好久不見,我是海怪。

有一陣子沒寫文章了,今天來更一期關於 qiankun 找不到生命週期的問題。

剛開始給專案接入 qiankun 的時候,時不時就會報

Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry:

開發的時候一切正常,只有在打包釋出後才會報這個 Bug,讓人非常惱火。相信有不少同學也遇到過這個問題,今天就來分享一下這個問題的思考和解決方案吧。

為什麼要找生命週期

首先,我們要知道為什麼 qiankun 載入微應用時要找生命週期勾點。

早在 qiankun 出來前,已經有一個微前端框架 single-spa 了。

它的思想是:無論 React、Vue 還是 Angular,專案打包最終的產物都是 JS。如果在 合適的時機 以 某種執行方式 去執行微應用的 JS 程式碼,大概就能實現 主-微 結構的微前端開發了。

這裡有兩個關鍵詞:合適的時機 和 執行方式。對於前者,single-spa 參考了單頁應用(Single Page Application)的思路,也希望用生命週期來管理微應用的 bootstrap, mount, update, unmount。而對於後者,則需要開發者自己實現執行微應用 JS 的方式。

總的來說,開發者需要在微應用的入口檔案 main.js 裡寫好生命週期實現:

export async function bootstrap() {
  // 啟動微應用
}
export async function mount() {
  // 載入微應用
}
export async function unmount() {
  // 解除安裝微應用
}
export async function update() {
  // 更新微應用
}

single-spa 會自動劫持和監聽網頁地址 URL 的變化,在命中路由規則後,執行這些生命週期勾點,從而實現微應用的載入、解除安裝和更新。

但這就有一個嚴重的問題了:一般我們專案的入口檔案就只有:

React.render(<App/>, document.querySelector('#root'))

這要如何和主應用互動呢?而且裡面的樣式、全域性變數隔離又要怎麼實現呢?Webpack 又該如何改造呢?然而,single-spa 只提供了生命週期的排程,並沒有解決這一系列問題。

既然前人解決不了,後人則可以基於原有框架繼續優化,這就是 qiankun

qiankun 和 single-spa 最大的不同是:qiankun 是 HTML 入口。它的原理如圖所示:

可以看到 qiankun 自己實現了一套通過 HTML 地址載入微應用的機制,但對於 “要在什麼時候執行 JS” 依然用了 single-spa 的生命週期排程能力。

這就是為什麼微應用的入口檔案 main.js 依然需要提供 single-spa 的生命週期回撥。

如何找入口

現在我們來聊聊如何找入口的問題。

對於一個簡單的 SPA 專案來說,一個 <div id="app"></div> + 一個 main.js 就夠了,入口很好找。

但真實專案往往會做分包拆包、自動注入 <script> 指令碼等操作,使得最終存取的 HTML 會有多個 <script> 標籤:

<script>
  // 初始化 XX SDK
</script>
<body>
  ...
</body>
<script src="你真實的入口 main.js"></script>
<script src="ant-design.js"></script>
<script>
  // 打包後自動注入的靜態資源 retry 邏輯
</script>
<script>
  // 公司程式碼閘道器自動注入的 JS 邏輯
</script>

對於這樣複雜的情況,qiankun 提供了 2 種定位入口的方式:

  • 找 帶有 entry 屬性的 <script entry src="main.js"></script>
  • 如果找不到,那麼把 最後一個 <script> 作為入口

第一種方法是最穩妥的,可以使用 html-webpack-inject-attributes-plugin 這個 Webpack 外掛,在打包的時候就給入口 main.js 新增 entry 屬性:

plugins = [
    new HtmlWebpackPlugin(),
    new htmlWebpackInjectAttributesPlugin({
        entry: "true",
    })
]

不推薦大家使用最後一種方法來確定入口,這種方式很不可靠。 因為微應用 HTML 有可能在一些公司代理、閘道器層中被攔截,自動注入一些指令碼。

這樣最終拿到 HTML 裡最後的一個 <script> 就不是原先的入口 main.js 檔案了:

<script src="你真實的入口 main.js"></script>
<script>
  // 自動注入的閘道器層的代理邏輯
</script>

兜底找入口

上面兩種找入口方式並不能 100% 覆蓋所有情況,比如我就遇到過這樣的場景:

  • 腳手架封裝得太黑盒,導致新增外掛不生效,無法在打包時注入 entry 屬性
  • 測試環境中,代理工具會自動往 HTML 插入 <script>,無法將最後一個 JS 作為入口

這下 qiankun 徹底找不到我的入口了。你總不能說:手寫一個 JS 指令碼,然後每次打包後用正則去 replace HTML,以此來新增 entry 屬性吧???

當然不行!

曾經我在 qiankun 的檔案裡看到過這段設定:

module.exports = {
  webpack: (config) => {
    config.output.library = `microApp`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';
    return config;
  },
  ...
};

檔案裡說這是一個兜底找入口的邏輯:

但檔案沒有說這裡的細節,下面就來一起研究一下。

微應用的 Webpack 設定

libraryTarget 指定打包成 umd 格式,也即最終模組會相容 CommonJS 和 AMD 等多種格式來進行匯出,最終 main.js 會是這樣:

(function webpackUniversalModuleDefinition(root, factory) {
  // CommonJS 匯出
  if (typeof exports === 'object' && typeof module === 'object')
    module.exports = factory(require('lodash'));
  // AMD 匯出
  else if (typeof define === 'function' && define.amd)
    define(['lodash'], factory);
  // 另一種匯出
  else if (typeof exports === 'object')
    exports['microApp'] = factory(require('lodash'));
  // 關鍵點
  else root['microApp'] = factory(root['_']);
})(this, function (__WEBPACK_EXTERNAL_MODULE_1__) {
  // 入口檔案的內容
  // ...
  return {
    bootstrap() {},
    mount() {},
    // ...
  }
});

直接看最後一種匯出方式 root['microApp'] = factory(root['_'])。Webpack 設定的 globalObjectlibrary 正好對應了裡面的 root 以及 'microApp'

而且上面的函數 factory 則是入口檔案的執行函數,理論上當執行 factory() 後會返回模組的輸出。

最終的效果是:Webpack 會把入口檔案的輸出內容掛在到 globalObject[library]/window['microApp'] 上:

window['microApp'] = {
  // main.js 所 export 的內容
  bootstrap() {},
  mount() {},
  unmount() {},
  update() {},
  // ...
}

主應用的兜底邏輯

把入口的內容掛載到 window 上有什麼好處呢?我們來稍微看點原始碼:

// 發 Http 請求獲取 HTML, JS 執行器
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
// 執行微應用的 JS,但這裡不一定有入口
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox);
// 獲取入口匯出的生命週期
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
  scriptExports,
  appName,
  global,
  sandboxContainer?.instance?.latestSetProp,
);

上面的程式碼很簡單,就是獲取微應用 HTML 和 JS,試圖從裡面獲取生命週期,所以下面我們來看看 getLifecyclesFromExports 做了什麼:

function getLifecyclesFromExports(
  scriptExports: LifeCycles<any>,
  appName: string,
  global: WindowProxy,
  globalLatestSetProp?: PropertyKey | null,
) {
  // 如果在獲取微應用的 JS 時可以鎖定入口檔案,那麼直接返回
  if (validateExportLifecycle(scriptExports)) {
    return scriptExports;
  }
  // 不用看
  if (globalLatestSetProp) {
    const lifecycles = (<any>global)[globalLatestSetProp];
    if (validateExportLifecycle(lifecycles)) {
      return lifecycles;
    }
  }
  // 獲取 globalObject[library] 裡的內容
  const globalVariableExports = (global as any)[appName];
  // 判斷 globalObject[library] 裡的內容是否為生命週期
  // 如果是合法生命週期,那麼直接返回
  if (validateExportLifecycle(globalVariableExports)) {
    return globalVariableExports;
  }
  throw new QiankunError(`You need to export lifecycle functions in ${appName} entry`);
}

從上面可以看到,在 getLifecyclesFromExports 最後會試圖從 windowProxy[微應用名] 中拿匯出的生命週期。

這也是為什麼兜底找入口操作需要微應用設定 Webpack,同時主應用指定的微應用名要和 library 名要一樣。

注意:qiankun 會使用 JS 沙箱來隔離微應用的環境,所以這裡的 globalObject 並不是 window 而是微應用對應的沙箱物件 windowProxy

在微應用裡寫 console.log(window['microApp']) 或在主應用裡輸入 console.log(window.proxy['microApp']) 即可看到微應用匯出的生命週期:

因此,在主應用中註冊微應用的時候,微應用 name 最好要和 Webpack 的 output.library 一致,這樣才能命中 qiankun 的兜底邏輯。

總結

最後總結一下,qiankun 要找入口是因為要從中拿到生命週期回撥,把它們給 single-spa 做排程。

qiankun 支援 2 種找入口的方式:

  • 正則匹配 帶有 entry 屬性的 <script> ,找到就把這個 JS 作為入口
  • 當找不到時,預設把 最後一個 JS 作為入口

如果這兩種方法都無法幫你正確定位入口,那麼你需要:

  • 在微應用設定 library, libraryTarget 以及 globalObject,把入口匯出的內容掛載到 window
  • 載入微應用時,主應用會試著從 window[library] 找微應用的生命週期回撥,找到後依然能正常載入
  • 在主應用註冊微應用時,要把微應用的 name 和 Webpack 的 output.library 設為一致,這樣才能命中第二步的邏輯

最後還要注意的是,上面說到的 window 並不是全域性物件,而是 qiankun 提供的 JS 沙箱物件 windowProxy

以上就是qiankun 找不到入口問題徹底解決的詳細內容,更多關於qiankun 找不到入口的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com