qiankun 找不到入口問(wèn)題徹底解決
前言
嗨害嗨,好久不見(jiàn),我是海怪。
有一陣子沒(méi)寫(xiě)文章了,今天來(lái)更一期關(guān)于 qiankun 找不到生命周期的問(wèn)題。
剛開(kāi)始給項(xiàng)目接入 qiankun 的時(shí)候,時(shí)不時(shí)就會(huì)報(bào)
Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry:
開(kāi)發(fā)的時(shí)候一切正常,只有在打包發(fā)布后才會(huì)報(bào)這個(gè) Bug,讓人非常惱火。相信有不少同學(xué)也遇到過(guò)這個(gè)問(wèn)題,今天就來(lái)分享一下這個(gè)問(wèn)題的思考和解決方案吧。
為什么要找生命周期
首先,我們要知道為什么 qiankun 加載微應(yīng)用時(shí)要找生命周期鉤子。
早在 qiankun 出來(lái)前,已經(jīng)有一個(gè)微前端框架 single-spa 了。
它的思想是:無(wú)論 React、Vue 還是 Angular,項(xiàng)目打包最終的產(chǎn)物都是 JS。如果在 合適的時(shí)機(jī) 以 某種執(zhí)行方式 去執(zhí)行微應(yīng)用的 JS 代碼,大概就能實(shí)現(xiàn) 主-微 結(jié)構(gòu)的微前端開(kāi)發(fā)了。
這里有兩個(gè)關(guān)鍵詞:合適的時(shí)機(jī) 和 執(zhí)行方式。對(duì)于前者,single-spa 參考了單頁(yè)應(yīng)用(Single Page Application)的思路,也希望用生命周期來(lái)管理微應(yīng)用的 bootstrap, mount, update, unmount。而對(duì)于后者,則需要開(kāi)發(fā)者自己實(shí)現(xiàn)執(zhí)行微應(yīng)用 JS 的方式。
總的來(lái)說(shuō),開(kāi)發(fā)者需要在微應(yīng)用的入口文件 main.js
里寫(xiě)好生命周期實(shí)現(xiàn):
export async function bootstrap() { // 啟動(dòng)微應(yīng)用 } export async function mount() { // 加載微應(yīng)用 } export async function unmount() { // 卸載微應(yīng)用 } export async function update() { // 更新微應(yīng)用 }
single-spa 會(huì)自動(dòng)劫持和監(jiān)聽(tīng)網(wǎng)頁(yè)地址 URL 的變化,在命中路由規(guī)則后,執(zhí)行這些生命周期鉤子,從而實(shí)現(xiàn)微應(yīng)用的加載、卸載和更新。
但這就有一個(gè)嚴(yán)重的問(wèn)題了:一般我們項(xiàng)目的入口文件就只有:
React.render(<App/>, document.querySelector('#root'))
這要如何和主應(yīng)用交互呢?而且里面的樣式、全局變量隔離又要怎么實(shí)現(xiàn)呢?Webpack 又該如何改造呢?然而,single-spa 只提供了生命周期的調(diào)度,并沒(méi)有解決這一系列問(wèn)題。
既然前人解決不了,后人則可以基于原有框架繼續(xù)優(yōu)化,這就是 qiankun。
qiankun 和 single-spa 最大的不同是:qiankun 是 HTML 入口。它的原理如圖所示:
可以看到 qiankun 自己實(shí)現(xiàn)了一套通過(guò) HTML 地址加載微應(yīng)用的機(jī)制,但對(duì)于 “要在什么時(shí)候執(zhí)行 JS” 依然用了 single-spa 的生命周期調(diào)度能力。
這就是為什么微應(yīng)用的入口文件 main.js
依然需要提供 single-spa 的生命周期回調(diào)。
如何找入口
現(xiàn)在我們來(lái)聊聊如何找入口的問(wèn)題。
對(duì)于一個(gè)簡(jiǎn)單的 SPA 項(xiàng)目來(lái)說(shuō),一個(gè) <div id="app"></div>
+ 一個(gè) main.js
就夠了,入口很好找。
但真實(shí)項(xiàng)目往往會(huì)做分包拆包、自動(dòng)注入 <script>
腳本等操作,使得最終訪問(wèn)的 HTML 會(huì)有多個(gè) <script>
標(biāo)簽:
<script> // 初始化 XX SDK </script> <body> ... </body> <script src="你真實(shí)的入口 main.js"></script> <script src="ant-design.js"></script> <script> // 打包后自動(dòng)注入的靜態(tài)資源 retry 邏輯 </script> <script> // 公司代碼網(wǎng)關(guān)自動(dòng)注入的 JS 邏輯 </script>
對(duì)于這樣復(fù)雜的情況,qiankun 提供了 2 種定位入口的方式:
- 找 帶有
entry
屬性的<script entry src="main.js"></script>
- 如果找不到,那么把 最后一個(gè)
<script>
作為入口
第一種方法是最穩(wěn)妥的,可以使用 html-webpack-inject-attributes-plugin 這個(gè) Webpack 插件,在打包的時(shí)候就給入口 main.js
添加 entry
屬性:
plugins = [ new HtmlWebpackPlugin(), new htmlWebpackInjectAttributesPlugin({ entry: "true", }) ]
不推薦大家使用最后一種方法來(lái)確定入口,這種方式很不可靠。 因?yàn)槲?yīng)用 HTML 有可能在一些公司代理、網(wǎng)關(guān)層中被攔截,自動(dòng)注入一些腳本。
這樣最終拿到 HTML 里最后的一個(gè) <script>
就不是原先的入口 main.js
文件了:
<script src="你真實(shí)的入口 main.js"></script> <script> // 自動(dòng)注入的網(wǎng)關(guān)層的代理邏輯 </script>
兜底找入口
上面兩種找入口方式并不能 100% 覆蓋所有情況,比如我就遇到過(guò)這樣的場(chǎng)景:
- 腳手架封裝得太黑盒,導(dǎo)致添加插件不生效,無(wú)法在打包時(shí)注入
entry
屬性 - 測(cè)試環(huán)境中,代理工具會(huì)自動(dòng)往 HTML 插入
<script>
,無(wú)法將最后一個(gè) JS 作為入口
這下 qiankun 徹底找不到我的入口了。你總不能說(shuō):手寫(xiě)一個(gè) JS 腳本,然后每次打包后用正則去 replace
HTML,以此來(lái)添加 entry
屬性吧???
當(dāng)然不行!
曾經(jīng)我在 qiankun 的文檔里看到過(guò)這段配置:
module.exports = { webpack: (config) => { config.output.library = `microApp`; config.output.libraryTarget = 'umd'; config.output.jsonpFunction = `webpackJsonp_${name}`; config.output.globalObject = 'window'; return config; }, ... };
文檔里說(shuō)這是一個(gè)兜底找入口的邏輯:
但文檔沒(méi)有說(shuō)這里的細(xì)節(jié),下面就來(lái)一起研究一下。
微應(yīng)用的 Webpack 配置
libraryTarget
指定打包成 umd 格式,也即最終模塊會(huì)兼容 CommonJS 和 AMD 等多種格式來(lái)進(jìn)行導(dǎo)出,最終 main.js
會(huì)是這樣:
(function webpackUniversalModuleDefinition(root, factory) { // CommonJS 導(dǎo)出 if (typeof exports === 'object' && typeof module === 'object') module.exports = factory(require('lodash')); // AMD 導(dǎo)出 else if (typeof define === 'function' && define.amd) define(['lodash'], factory); // 另一種導(dǎo)出 else if (typeof exports === 'object') exports['microApp'] = factory(require('lodash')); // 關(guān)鍵點(diǎn) else root['microApp'] = factory(root['_']); })(this, function (__WEBPACK_EXTERNAL_MODULE_1__) { // 入口文件的內(nèi)容 // ... return { bootstrap() {}, mount() {}, // ... } });
直接看最后一種導(dǎo)出方式 root['microApp'] = factory(root['_'])
。Webpack 配置的 globalObject
和 library
正好對(duì)應(yīng)了里面的 root
以及 'microApp'
。
而且上面的函數(shù) factory
則是入口文件的執(zhí)行函數(shù),理論上當(dāng)執(zhí)行 factory()
后會(huì)返回模塊的輸出。
最終的效果是:Webpack 會(huì)把入口文件的輸出內(nèi)容掛在到 globalObject[library]
/window['microApp']
上:
window['microApp'] = { // main.js 所 export 的內(nèi)容 bootstrap() {}, mount() {}, unmount() {}, update() {}, // ... }
主應(yīng)用的兜底邏輯
把入口的內(nèi)容掛載到 window
上有什么好處呢?我們來(lái)稍微看點(diǎn)源碼:
// 發(fā) Http 請(qǐng)求獲取 HTML, JS 執(zhí)行器 const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts); // 執(zhí)行微應(yīng)用的 JS,但這里不一定有入口 const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox); // 獲取入口導(dǎo)出的生命周期 const { bootstrap, mount, unmount, update } = getLifecyclesFromExports( scriptExports, appName, global, sandboxContainer?.instance?.latestSetProp, );
上面的代碼很簡(jiǎn)單,就是獲取微應(yīng)用 HTML 和 JS,試圖從里面獲取生命周期,所以下面我們來(lái)看看 getLifecyclesFromExports
做了什么:
function getLifecyclesFromExports( scriptExports: LifeCycles<any>, appName: string, global: WindowProxy, globalLatestSetProp?: PropertyKey | null, ) { // 如果在獲取微應(yīng)用的 JS 時(shí)可以鎖定入口文件,那么直接返回 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
最后會(huì)試圖從 windowProxy[微應(yīng)用名]
中拿導(dǎo)出的生命周期。
這也是為什么兜底找入口操作需要微應(yīng)用配置 Webpack,同時(shí)主應(yīng)用指定的微應(yīng)用名要和 library
名要一樣。
注意:qiankun 會(huì)使用 JS 沙箱來(lái)隔離微應(yīng)用的環(huán)境,所以這里的 globalObject
并不是 window
而是微應(yīng)用對(duì)應(yīng)的沙箱對(duì)象 windowProxy
。
在微應(yīng)用里寫(xiě) console.log(window['microApp'])
或在主應(yīng)用里輸入 console.log(window.proxy['microApp'])
即可看到微應(yīng)用導(dǎo)出的生命周期:
因此,在主應(yīng)用中注冊(cè)微應(yīng)用的時(shí)候,微應(yīng)用 name
最好要和 Webpack 的 output.library
一致,這樣才能命中 qiankun 的兜底邏輯。
總結(jié)
最后總結(jié)一下,qiankun 要找入口是因?yàn)橐獜闹心玫缴芷诨卣{(diào),把它們給 single-spa 做調(diào)度。
qiankun 支持 2 種找入口的方式:
- 正則匹配 帶有
entry
屬性的<script>
,找到就把這個(gè) JS 作為入口 - 當(dāng)找不到時(shí),默認(rèn)把 最后一個(gè) JS 作為入口
如果這兩種方法都無(wú)法幫你正確定位入口,那么你需要:
- 在微應(yīng)用配置
library
,libraryTarget
以及globalObject
,把入口導(dǎo)出的內(nèi)容掛載到window
上 - 加載微應(yīng)用時(shí),主應(yīng)用會(huì)試著從
window[library]
找微應(yīng)用的生命周期回調(diào),找到后依然能正常加載 - 在主應(yīng)用注冊(cè)微應(yīng)用時(shí),要把微應(yīng)用的
name
和 Webpack 的output.library
設(shè)為一致,這樣才能命中第二步的邏輯
最后還要注意的是,上面說(shuō)到的 window
并不是全局對(duì)象,而是 qiankun 提供的 JS 沙箱對(duì)象 windowProxy
。
以上就是qiankun 找不到入口問(wèn)題徹底解決的詳細(xì)內(nèi)容,更多關(guān)于qiankun 找不到入口的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序 獲取設(shè)備信息 API實(shí)例詳解
這篇文章主要介紹了微信小程序 獲取設(shè)備信息 API實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2016-10-10微信小程序 開(kāi)發(fā)之頂部導(dǎo)航欄實(shí)例代碼
這篇文章主要介紹了微信小程序 開(kāi)發(fā)之頂部導(dǎo)航欄實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02微信小程序 利用css實(shí)現(xiàn)遮罩效果實(shí)例詳解
這篇文章主要介紹了微信小程序 利用css實(shí)現(xiàn)遮罩效果實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-01-01Javascript設(shè)計(jì)模式之原型模式詳細(xì)
這篇文章主要介紹了Javascript設(shè)計(jì)模式之原型模式,原型模式用于在創(chuàng)建對(duì)象時(shí),通過(guò)共享某個(gè)對(duì)象原型的屬性和方法,從而達(dá)到提高性能、降低內(nèi)存占用、代碼復(fù)用的效果。下面小編將詳細(xì)介紹 ,需要的朋友可以參考下2021-09-09uniapp自定義相機(jī)實(shí)現(xiàn)示例詳解
這篇文章主要為大家介紹了uniapp自定義相機(jī)實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03微信小程序 五星評(píng)價(jià)功能的實(shí)現(xiàn)
這篇文章主要介紹了微信小程序 五星評(píng)價(jià)功能的實(shí)現(xiàn)的相關(guān)資料,這里附有實(shí)例代碼及實(shí)現(xiàn)效果圖,需要的朋友可以參考下2017-03-03詳解微信小程序入門(mén)五: wxml文件引用、模版、生命周期
本篇文章主要介紹了詳解微信小程序入門(mén)五: wxml文件引用、模版、生命周期,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-01-01JavaScript 實(shí)現(xiàn)點(diǎn)擊關(guān)閉全屏示例詳解
這篇文章主要為大家介紹了JavaScript 實(shí)現(xiàn)點(diǎn)擊關(guān)閉全屏示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08