qiankun 找不到入口問題徹底解決
前言
嗨害嗨,好久不見,我是海怪。
有一陣子沒寫文章了,今天來更一期關(guān)于 qiankun 找不到生命周期的問題。
剛開始給項目接入 qiankun 的時候,時不時就會報
Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry:

開發(fā)的時候一切正常,只有在打包發(fā)布后才會報這個 Bug,讓人非常惱火。相信有不少同學也遇到過這個問題,今天就來分享一下這個問題的思考和解決方案吧。
為什么要找生命周期
首先,我們要知道為什么 qiankun 加載微應(yīng)用時要找生命周期鉤子。
早在 qiankun 出來前,已經(jīng)有一個微前端框架 single-spa 了。

它的思想是:無論 React、Vue 還是 Angular,項目打包最終的產(chǎn)物都是 JS。如果在 合適的時機 以 某種執(zhí)行方式 去執(zhí)行微應(yīng)用的 JS 代碼,大概就能實現(xiàn) 主-微 結(jié)構(gòu)的微前端開發(fā)了。
這里有兩個關(guān)鍵詞:合適的時機 和 執(zhí)行方式。對于前者,single-spa 參考了單頁應(yīng)用(Single Page Application)的思路,也希望用生命周期來管理微應(yīng)用的 bootstrap, mount, update, unmount。而對于后者,則需要開發(fā)者自己實現(xiàn)執(zhí)行微應(yīng)用 JS 的方式。

總的來說,開發(fā)者需要在微應(yīng)用的入口文件 main.js 里寫好生命周期實現(xiàn):
export async function bootstrap() {
// 啟動微應(yīng)用
}
export async function mount() {
// 加載微應(yīng)用
}
export async function unmount() {
// 卸載微應(yīng)用
}
export async function update() {
// 更新微應(yīng)用
}
single-spa 會自動劫持和監(jiān)聽網(wǎng)頁地址 URL 的變化,在命中路由規(guī)則后,執(zhí)行這些生命周期鉤子,從而實現(xiàn)微應(yīng)用的加載、卸載和更新。
但這就有一個嚴重的問題了:一般我們項目的入口文件就只有:
React.render(<App/>, document.querySelector('#root'))
這要如何和主應(yīng)用交互呢?而且里面的樣式、全局變量隔離又要怎么實現(xiàn)呢?Webpack 又該如何改造呢?然而,single-spa 只提供了生命周期的調(diào)度,并沒有解決這一系列問題。
既然前人解決不了,后人則可以基于原有框架繼續(xù)優(yōu)化,這就是 qiankun。
qiankun 和 single-spa 最大的不同是:qiankun 是 HTML 入口。它的原理如圖所示:

可以看到 qiankun 自己實現(xiàn)了一套通過 HTML 地址加載微應(yīng)用的機制,但對于 “要在什么時候執(zhí)行 JS” 依然用了 single-spa 的生命周期調(diào)度能力。
這就是為什么微應(yīng)用的入口文件 main.js 依然需要提供 single-spa 的生命周期回調(diào)。
如何找入口
現(xiàn)在我們來聊聊如何找入口的問題。
對于一個簡單的 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> // 打包后自動注入的靜態(tài)資源 retry 邏輯 </script> <script> // 公司代碼網(wǎng)關(guān)自動注入的 JS 邏輯 </script>
對于這樣復(fù)雜的情況,qiankun 提供了 2 種定位入口的方式:
- 找 帶有
entry屬性的<script entry src="main.js"></script> - 如果找不到,那么把 最后一個
<script>作為入口
第一種方法是最穩(wěn)妥的,可以使用 html-webpack-inject-attributes-plugin 這個 Webpack 插件,在打包的時候就給入口 main.js 添加 entry 屬性:
plugins = [
new HtmlWebpackPlugin(),
new htmlWebpackInjectAttributesPlugin({
entry: "true",
})
]
不推薦大家使用最后一種方法來確定入口,這種方式很不可靠。 因為微應(yīng)用 HTML 有可能在一些公司代理、網(wǎng)關(guān)層中被攔截,自動注入一些腳本。
這樣最終拿到 HTML 里最后的一個 <script> 就不是原先的入口 main.js 文件了:
<script src="你真實的入口 main.js"></script> <script> // 自動注入的網(wǎng)關(guān)層的代理邏輯 </script>
兜底找入口
上面兩種找入口方式并不能 100% 覆蓋所有情況,比如我就遇到過這樣的場景:
- 腳手架封裝得太黑盒,導致添加插件不生效,無法在打包時注入
entry屬性 - 測試環(huán)境中,代理工具會自動往 HTML 插入
<script>,無法將最后一個 JS 作為入口
這下 qiankun 徹底找不到我的入口了。你總不能說:手寫一個 JS 腳本,然后每次打包后用正則去 replace HTML,以此來添加 entry 屬性吧???
當然不行!
曾經(jīng)我在 qiankun 的文檔里看到過這段配置:
module.exports = {
webpack: (config) => {
config.output.library = `microApp`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';
return config;
},
...
};
文檔里說這是一個兜底找入口的邏輯:

但文檔沒有說這里的細節(jié),下面就來一起研究一下。
微應(yīng)用的 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'));
// 關(guān)鍵點
else root['microApp'] = factory(root['_']);
})(this, function (__WEBPACK_EXTERNAL_MODULE_1__) {
// 入口文件的內(nèi)容
// ...
return {
bootstrap() {},
mount() {},
// ...
}
});
直接看最后一種導出方式 root['microApp'] = factory(root['_'])。Webpack 配置的 globalObject 和 library 正好對應(yīng)了里面的 root 以及 'microApp'。
而且上面的函數(shù) factory 則是入口文件的執(zhí)行函數(shù),理論上當執(zhí)行 factory() 后會返回模塊的輸出。
最終的效果是:Webpack 會把入口文件的輸出內(nèi)容掛在到 globalObject[library]/window['microApp'] 上:
window['microApp'] = {
// main.js 所 export 的內(nèi)容
bootstrap() {},
mount() {},
unmount() {},
update() {},
// ...
}
主應(yīng)用的兜底邏輯
把入口的內(nèi)容掛載到 window 上有什么好處呢?我們來稍微看點源碼:
// 發(fā) Http 請求獲取 HTML, JS 執(zhí)行器
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
// 執(zhí)行微應(yīng)用的 JS,但這里不一定有入口
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox);
// 獲取入口導出的生命周期
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
scriptExports,
appName,
global,
sandboxContainer?.instance?.latestSetProp,
);
上面的代碼很簡單,就是獲取微應(yīng)用 HTML 和 JS,試圖從里面獲取生命周期,所以下面我們來看看 getLifecyclesFromExports 做了什么:
function getLifecyclesFromExports(
scriptExports: LifeCycles<any>,
appName: string,
global: WindowProxy,
globalLatestSetProp?: PropertyKey | null,
) {
// 如果在獲取微應(yīng)用的 JS 時可以鎖定入口文件,那么直接返回
if (validateExportLifecycle(scriptExports)) {
return scriptExports;
}
// 不用看
if (globalLatestSetProp) {
const lifecycles = (<any>global)[globalLatestSetProp];
if (validateExportLifecycle(lifecycles)) {
return lifecycles;
}
}
// 獲取 globalObject[library] 里的內(nèi)容
const globalVariableExports = (global as any)[appName];
// 判斷 globalObject[library] 里的內(nèi)容是否為生命周期
// 如果是合法生命周期,那么直接返回
if (validateExportLifecycle(globalVariableExports)) {
return globalVariableExports;
}
throw new QiankunError(`You need to export lifecycle functions in ${appName} entry`);
}
從上面可以看到,在 getLifecyclesFromExports 最后會試圖從 windowProxy[微應(yīng)用名] 中拿導出的生命周期。
這也是為什么兜底找入口操作需要微應(yīng)用配置 Webpack,同時主應(yīng)用指定的微應(yīng)用名要和 library 名要一樣。
注意:qiankun 會使用 JS 沙箱來隔離微應(yīng)用的環(huán)境,所以這里的 globalObject 并不是 window 而是微應(yīng)用對應(yīng)的沙箱對象 windowProxy。
在微應(yīng)用里寫 console.log(window['microApp']) 或在主應(yīng)用里輸入 console.log(window.proxy['microApp']) 即可看到微應(yīng)用導出的生命周期:

因此,在主應(yīng)用中注冊微應(yīng)用的時候,微應(yīng)用 name 最好要和 Webpack 的 output.library 一致,這樣才能命中 qiankun 的兜底邏輯。
總結(jié)
最后總結(jié)一下,qiankun 要找入口是因為要從中拿到生命周期回調(diào),把它們給 single-spa 做調(diào)度。
qiankun 支持 2 種找入口的方式:
- 正則匹配 帶有
entry屬性的<script>,找到就把這個 JS 作為入口 - 當找不到時,默認把 最后一個 JS 作為入口
如果這兩種方法都無法幫你正確定位入口,那么你需要:
- 在微應(yīng)用配置
library,libraryTarget以及globalObject,把入口導出的內(nèi)容掛載到window上 - 加載微應(yīng)用時,主應(yīng)用會試著從
window[library]找微應(yīng)用的生命周期回調(diào),找到后依然能正常加載 - 在主應(yīng)用注冊微應(yīng)用時,要把微應(yīng)用的
name和 Webpack 的output.library設(shè)為一致,這樣才能命中第二步的邏輯
最后還要注意的是,上面說到的 window 并不是全局對象,而是 qiankun 提供的 JS 沙箱對象 windowProxy。
以上就是qiankun 找不到入口問題徹底解決的詳細內(nèi)容,更多關(guān)于qiankun 找不到入口的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序?qū)崿F(xiàn)給循環(huán)列表添加點擊樣式實例
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)給循環(huán)列表添加點擊樣式實例的相關(guān)資料,需要的朋友可以參考下2017-04-04
微信小程序 Windows2008 R2服務(wù)器配置TLS1.2方法
微信小程序免費SSL證書https、TLS版本問題的解決方案《二十四》request:fail錯誤(含https解決方案)(真機預(yù)覽問題把下面的代碼復(fù)制到PowerShell里運行一下,然后重啟服務(wù)器。# Enables TLS 1.2 on ...,需要的朋友可以參考下2016-12-12

