微前端框架qiankun源碼剖析之上篇
引言
注意: 受篇幅限制,本文中所粘貼的代碼都是經(jīng)過作者刪減梳理后的,只為講述qiankun框架原理而展示,并非完整源碼。
一、single-spa簡介
要了解qiankun的實現(xiàn)機制,那我們不得不從其底層依賴的single-spa說起。隨著微前端的發(fā)展,我們看到在這個領(lǐng)域之中出現(xiàn)了各式各樣的工具包和框架來幫助我們方便快捷的實現(xiàn)自己的微前端應(yīng)用。在發(fā)展早期,single-spa可以說是獨樹一幟,為我們提供了一種簡便的微前端路由工具,大大降低了實現(xiàn)一個微前端應(yīng)用的成本。我們來看一下一個典型single-spa微前端應(yīng)用的架構(gòu)及代碼。
主應(yīng)用(基座):
作為整個微前端應(yīng)用中的項目調(diào)度中心,是用戶進(jìn)入該微前端應(yīng)用時首先加載的部分。在主應(yīng)用中,通過向single-spa提供的registerApplication
函數(shù)傳入指定的參數(shù)來注冊子應(yīng)用,這些參數(shù)包括子應(yīng)用名稱name
、子應(yīng)用如何加載app
、子應(yīng)用何時激活activeWhen
、以及需要向子應(yīng)用中傳遞的參數(shù)customProps
等等。在完成整體注冊后調(diào)用start
函數(shù)啟動整個微前端項目。
// single-spa-config.js import { registerApplication, start } from 'single-spa'; // Config with more expressive API registerApplication({ name: 'app1', app: () => import('src/app1/main.js'), activeWhen: ['/myApp', (location) => location.pathname.startsWith('/some/other/path')], customProps: { some: 'value', } }); start();
子應(yīng)用:
子應(yīng)用是實際展示內(nèi)容的部分,最主要的工作是導(dǎo)出single-spa中所規(guī)定的生命周期函數(shù),以便于主應(yīng)用調(diào)度。其中,bootstrap在子應(yīng)用第一次加載時調(diào)用,mount在子應(yīng)用每次激活時調(diào)用,unmount在子應(yīng)用被移出時調(diào)用。此外在這些生命周期函數(shù)中我們可以看到props參數(shù)被傳入,這個參數(shù)中包含了子應(yīng)用注冊名稱、singleSpa實例、用戶自定義參數(shù)等信息,方便子應(yīng)用的使用。
console.log("The registered application has been loaded!"); export async function bootstrap(props) { const { name, // The name of the application singleSpa, // The singleSpa instance mountParcel, // Function for manually mounting customProps, // Additional custom information } = props; // Props are given to every lifecycle return Promise.resolve(); } export async function mount(props) {...} export async function unmount(props) {...}
可以看到Single-spa作為一個微前端框架領(lǐng)域最為廣泛使用的包,其為我們提供了良好的子應(yīng)用路由機制。但是除此之外,single-spa也留下了很多需要用戶自行解決的問題:
子應(yīng)用究竟應(yīng)該如何加載,從哪里加載?
子應(yīng)用運行時會不會互相影響?
主應(yīng)用與子應(yīng)用、子應(yīng)用之間具體可以通過customProps互相通信,但是怎樣才能知道customProps發(fā)生了變化呢?
因此,市面上出現(xiàn)了很多基于single-spa二次封裝的微前端框架。他們分別使用不同的方式,基于各自不同的側(cè)重點包裝出了更加完善的產(chǎn)品。對于這些產(chǎn)品,我們可以將single-spa在其中的作用類比位理解為react-router之于react項目的作用——single-spa作為一個沒有框架、技術(shù)棧限制的微前端路由為它們提供了最底層的子應(yīng)用間路由及生命周期管理的服務(wù)。在近幾年微前端的發(fā)展壯大過程中,早期推出并經(jīng)久不衰的阿里qiankun框架算的上是一枝獨秀了。
二、qiankun簡介
作為目前微前端領(lǐng)域首屈一指的框架,qiankun無論是從接入的方便程度還是從框架本身提供的易用性來說都是可圈可點的。qiankun基于single-spa進(jìn)行了二次開發(fā),不但為用戶提供了簡便的接入方式(包括減少侵入性,易于老項目的改造),還貼心的提供了沙箱隔離以及實現(xiàn)了基于發(fā)布訂閱模式的應(yīng)用間通信方式,大大降低了微前端的準(zhǔn)入門檻,對于微前端工程化的推動作用是不可忽視的。
因為其基于single-spa二次開發(fā), 所以qiankun微前端架構(gòu)與第一章中所提及的并無二致,下面我們列出一個典型的qiankun應(yīng)用的代碼并類比其與single-spa的代碼區(qū)別。
主應(yīng)用:
這里qiankun將single-spa中的app改為了entry并對其功能進(jìn)行了增強,用戶只需要輸入子應(yīng)用的html入口路徑即可,其余加載工作由qiankun內(nèi)部完成,當(dāng)然也可以自行列出所需加載的資源。此外加入了container選項,讓用戶顯示指定并感知到子應(yīng)用所掛載的容器,簡化了多個子應(yīng)用同時激活的場景。
import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'react app', // app name registered entry: '//localhost:7100', container: '#yourContainer', activeRule: '/yourActiveRule', }, { name: 'vue app', entry: { scripts: ['//localhost:7100/main.js'] }, container: '#yourContainer2', activeRule: '/yourActiveRule2', }, ]); start();
子應(yīng)用:
與single-spa基本一致,導(dǎo)出了三個生命周期函數(shù)。這里可以看到在mount中我們手動將react應(yīng)用渲染到了頁面上,反之在unmount中我們將其從頁面上清除。
/** * bootstrap 只會在微應(yīng)用初始化的時候調(diào)用一次,下次微應(yīng)用重新進(jìn)入時會直接調(diào)用 mount 鉤子,不會再重復(fù)觸發(fā) bootstrap。 * 通常我們可以在這里做一些全局變量的初始化,比如不會在 unmount 階段被銷毀的應(yīng)用級別的緩存等。 */ export async function bootstrap() { console.log('react app bootstraped'); } /** * 應(yīng)用每次進(jìn)入都會調(diào)用 mount 方法,通常我們在這里觸發(fā)應(yīng)用的渲染方法 */ export async function mount(props) { ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root')); } /** * 應(yīng)用每次 切出/卸載 會調(diào)用的方法,通常在這里我們會卸載微應(yīng)用的應(yīng)用實例 */ export async function unmount(props) { ReactDOM.unmountComponentAtNode( props.container ? props.container.querySelector('#root') : document.getElementById('root'), ); }
可以看到,由于其幫助我們完成了子應(yīng)用的加載工作,所以用戶的配置相比于single-spa更為簡便了。但是,除了這個明面上的工作,qiankun還在暗處為我們的易用性做出了很多努力,接下來,我們會圍繞著以下三個方面來深入剖析qiankun內(nèi)部源碼和相關(guān)實現(xiàn)原理:
qiankun如何實現(xiàn)用戶只需配置一個URL就可以加載相應(yīng)子應(yīng)用資源的;
qiankun如何幫助用戶做到子應(yīng)用間獨立運行的(包括JS互不影響和CSS互不污染);
qiankun如何幫助用戶實現(xiàn)更簡便高效的應(yīng)用間通信的;
三、子應(yīng)用加載
qiankun的子應(yīng)用注冊方式非常簡單,用戶只需要調(diào)用registerMicroApps函數(shù)并將所需參數(shù)傳入即可.前文中我們說到qiankun是基于single-spa二次封裝的框架,因此qiankun中的路由監(jiān)聽和子應(yīng)用生命周期管理實際上都是交給了single-spa來進(jìn)行實現(xiàn)的。我們一起來看一下該方法的實現(xiàn)方式(部分截取)
import { registerApplication } from 'single-spa'; let microApps: Array<RegistrableApp<Record<string, unknown>>> = []; export function registerMicroApps<T extends ObjectType>( apps: Array<RegistrableApp<T>>, lifeCycles?: FrameworkLifeCycles<T>, ) { // 判斷應(yīng)用是否注冊過,保證每個應(yīng)用只注冊一次 const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name)); microApps = [...microApps, ...unregisteredApps]; unregisteredApps.forEach((app) => { // 取出用戶輸入的參數(shù) const { name, activeRule, loader = noop, props, ...appConfig } = app; // 調(diào)用single-spa的子應(yīng)用注冊函數(shù),將用戶輸入的參數(shù)轉(zhuǎn)換為single-spa所需的參數(shù) registerApplication({ name, // 這里提供了single-spa所需的子應(yīng)用加載方式函數(shù) app: async () => { loader(true); await frameworkStartedDefer.promise; // 調(diào)用轉(zhuǎn)換函數(shù)loadApp將用戶輸入的url等解析轉(zhuǎn)換運行,最終生成增強后的子應(yīng)用生命周期函數(shù)(包括mount,unmount,bootstrap) const { mount, ...otherMicroAppConfigs } = ( await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles) )(); // 返回值為loadApp生成的一系列生命周期函數(shù),其中mount函數(shù)數(shù)組再次增強 return { mount: [async () => loader(true), ...toArray(mount), async () => loader(false)], ...otherMicroAppConfigs, }; }, activeWhen: activeRule, customProps: props, }); }); }
可以看到,qiankun在子應(yīng)用加載上所做的工作就是將用戶調(diào)用registerMicroApps
時所提供的參數(shù)經(jīng)過一系列處轉(zhuǎn)換之后,改造成single-spa中registerApplication
所需要的參數(shù)。下面,我們給出qiankun中實現(xiàn)該轉(zhuǎn)換子的主要函數(shù)loadApp
的部分實現(xiàn)代碼(源代碼地址github.com/umijs/qiank…
import { importEntry } from 'import-html-entry'; export async function loadApp<T extends ObjectType>( app: LoadableApp<T>, configuration: FrameworkConfiguration = {}, lifeCycles?: FrameworkLifeCycles<T>, ): Promise<ParcelConfigObjectGetter> { const { entry, name: appName } = app; const { singular = false, sandbox = true, excludeAssetFilter, globalContext = window, ...importEntryOpts } = configuration; // 。。。。。。 // 依賴了import-html-entry庫中的方法解析了用戶輸入的url(entry參數(shù)),得到了template(HTML模版),execScripts(所依賴JS文件的執(zhí)行函數(shù))以及assetPublicPath(公共資源路徑) const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts); // 。。。。。。 // 在window沙箱中(global參數(shù))執(zhí)行entry依賴的js文件,得到相關(guān)生命周期( bootstrap, mount, unmount, update) // 這里可以忽略getLifecyclesFromExports函數(shù),其返回與scriptExports一致,只是為了檢查子應(yīng)用是否導(dǎo)出了必須的生命周期 const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, { scopedGlobalVariables: speedySandbox ? trustedGlobals : [], }); const { bootstrap, mount, unmount, update } = getLifecyclesFromExports( scriptExports, appName, global, sandboxContainer?.instance?.latestSetProp, ); // 。。。。。 // 導(dǎo)出single-spa所需配置的getter方法(因為配置項與子應(yīng)用掛在的container相關(guān),默認(rèn)為用戶輸入的container,后續(xù)用戶可以手動加載子應(yīng)用并指定其渲染位置) const initialContainer = 'container' in app ? app.container : undefined; const parcelConfigGetter: ParcelConfigObjectGetter = (remountContainer = initialContainer) => { const parcelConfig: ParcelConfigObject = { name: appInstanceId, bootstrap, // mount數(shù)組在子應(yīng)用渲染時依次執(zhí)行 mount: [ // 。。。。。。 // 執(zhí)行沙箱隔離 mountSandbox, // 調(diào)用用戶自定義mount生命周期,并傳入setGlobalState/onGlobalStateChange的應(yīng)用間通信方法函數(shù) async (props) => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }), // 。。。。。。 ], // unmount數(shù)組在子應(yīng)用卸載時依次執(zhí)行 unmount: [ // 。。。。。。。 // 調(diào)用用戶自定義unmount生命周期 async (props) => unmount({ ...props, container: appWrapperGetter() }), // 卸載隔離沙箱 unmountSandbox, // 清理工作 async () => { render({ element: null, loading: false, container: remountContainer }, 'unmounted'); // 清理子應(yīng)用對全局通信的訂閱 offGlobalStateChange(appInstanceId); // for gc appWrapperElement = null; syncAppWrapperElement2Sandbox(appWrapperElement); }, // 。。。。。。。 ], }; return parcelConfig; } return parcelConfigGetter }
可以看到,qiankun在其加載函數(shù)loadApp
中做了一些額外的工作。
為了方便使用,qiankun提供了基于url入口來加載子應(yīng)用的方式。為了獲取用戶提供的html文件(或者資源文件數(shù)組)并解析出其中所需的資源,qiankun依賴了import-html-entry
庫中的相關(guān)方法,執(zhí)行并得到了子應(yīng)用導(dǎo)出的用戶自定義生命周期。
對用戶自定義的生命周期進(jìn)行增強(包括掛載/卸載應(yīng)用間的隔離沙箱,初始化或傳入應(yīng)用間通信方法等等),返回框架增強后的生命周期函數(shù)數(shù)組并注冊在single-spa中。
經(jīng)過源碼的分析我們可以看出,qiankun在子應(yīng)用加載上就是作為中間層存在的,其主要作用就是簡化用戶對于子應(yīng)用注冊的輸入,通過框架內(nèi)部的方法轉(zhuǎn)換并增強了用戶的輸入最終將其傳入了single-spa之中,在后續(xù)的執(zhí)行中真正負(fù)責(zé)子應(yīng)用加載卸載的是single-spa。
以上就是微前端框架qiankun源碼剖析之上篇的詳細(xì)內(nèi)容,更多關(guān)于微前端框架qiankun剖析的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript實現(xiàn)字典Dictionary示例基礎(chǔ)
這篇文章主要為大家介紹了javascript實現(xiàn)字典Dictionary基礎(chǔ)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08