qiankun 找不到入口問題徹底解決
前言
嗨害嗨,好久不見,我是海怪。
有一陣子沒寫文章瞭,今天來更一期關於 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="你真實的入口%20main.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="你真實的入口%20main.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 配置的 globalObject
和 library
正好對應瞭裡面的 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 找不到入口的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 微前端框架qiankun源碼剖析之上篇
- webpack output.library的16 種取值方法示例
- JavaScript webpack5配置及使用基本介紹
- 淺談JS前端模塊化的幾種規范
- 微前端qiankun改造日漸龐大的項目教程