首頁 > 軟體

微前端框架qiankun原始碼剖析之上篇

2023-02-10 06:00:54

引言

注意: 受篇幅限制,本文中所貼上的程式碼都是經過作者刪減梳理後的,只為講述qiankun框架原理而展示,並非完整原始碼。

一、single-spa簡介

要了解qiankun的實現機制,那我們不得不從其底層依賴的single-spa說起。隨著微前端的發展,我們看到在這個領域之中出現了各式各樣的工具包和框架來幫助我們方便快捷的實現自己的微前端應用。在發展早期,single-spa可以說是獨樹一幟,為我們提供了一種簡便的微前端路由工具,大大降低了實現一個微前端應用的成本。我們來看一下一個典型single-spa微前端應用的架構及程式碼。

主應用(基座):

作為整個微前端應用中的專案排程中心,是使用者進入該微前端應用時首先載入的部分。在主應用中,通過向single-spa提供的registerApplication函數傳入指定的引數來註冊子應用,這些引數包括子應用名稱name、子應用如何載入app、子應用何時啟用activeWhen、以及需要向子應用中傳遞的引數customProps等等。在完成整體註冊後呼叫start函數啟動整個微前端專案。

// 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();

子應用:

子應用是實際展示內容的部分,最主要的工作是匯出single-spa中所規定的生命週期函數,以便於主應用排程。其中,bootstrap在子應用第一次載入時呼叫,mount在子應用每次啟用時呼叫,unmount在子應用被移出時呼叫。此外在這些生命週期函數中我們可以看到props引數被傳入,這個引數中包含了子應用註冊名稱、singleSpa範例、使用者自定義引數等資訊,方便子應用的使用。

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作為一個微前端框架領域最為廣泛使用的包,其為我們提供了良好的子應用路由機制。但是除此之外,single-spa也留下了很多需要使用者自行解決的問題:

子應用究竟應該如何載入,從哪裡載入?

子應用執行時會不會互相影響?

主應用與子應用、子應用之間具體可以通過customProps互相通訊,但是怎樣才能知道customProps發生了變化呢?

因此,市面上出現了很多基於single-spa二次封裝的微前端框架。他們分別使用不同的方式,基於各自不同的側重點包裝出了更加完善的產品。對於這些產品,我們可以將single-spa在其中的作用類比位理解為react-router之於react專案的作用——single-spa作為一個沒有框架、技術棧限制的微前端路由為它們提供了最底層的子應用間路由及生命週期管理的服務。在近幾年微前端的發展壯大過程中,早期推出並經久不衰的阿里qiankun框架算的上是一枝獨秀了。

二、qiankun簡介

作為目前微前端領域首屈一指的框架,qiankun無論是從接入的方便程度還是從框架本身提供的易用性來說都是可圈可點的。qiankun基於single-spa進行了二次開發,不但為使用者提供了簡便的接入方式(包括減少侵入性,易於老專案的改造),還貼心的提供了沙箱隔離以及實現了基於釋出訂閱模式的應用間通訊方式,大大降低了微前端的准入門檻,對於微前端工程化的推動作用是不可忽視的。

因為其基於single-spa二次開發, 所以qiankun微前端架構與第一章中所提及的並無二致,下面我們列出一個典型的qiankun應用的程式碼並類比其與single-spa的程式碼區別。

主應用:

這裡qiankun將single-spa中的app改為了entry並對其功能進行了增強,使用者只需要輸入子應用的html入口路徑即可,其餘載入工作由qiankun內部完成,當然也可以自行列出所需載入的資源。此外加入了container選項,讓使用者顯示指定並感知到子應用所掛載的容器,簡化了多個子應用同時啟用的場景。

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();

子應用:

與single-spa基本一致,匯出了三個生命週期函數。這裡可以看到在mount中我們手動將react應用渲染到了頁面上,反之在unmount中我們將其從頁面上清除。

/**
 * bootstrap 只會在微應用初始化的時候呼叫一次,下次微應用重新進入時會直接呼叫 mount 勾點,不會再重複觸發 bootstrap。
 * 通常我們可以在這裡做一些全域性變數的初始化,比如不會在 unmount 階段被銷燬的應用級別的快取等。
 */
export async function bootstrap() {
  console.log('react app bootstraped');
}
/**
 * 應用每次進入都會呼叫 mount 方法,通常我們在這裡觸發應用的渲染方法
 */
export async function mount(props) {
  ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
 * 應用每次 切出/解除安裝 會呼叫的方法,通常在這裡我們會解除安裝微應用的應用範例
 */
export async function unmount(props) {
  ReactDOM.unmountComponentAtNode(
    props.container ? props.container.querySelector('#root') : document.getElementById('root'),
  );
}

可以看到,由於其幫助我們完成了子應用的載入工作,所以使用者的設定相比於single-spa更為簡便了。但是,除了這個明面上的工作,qiankun還在暗處為我們的易用性做出了很多努力,接下來,我們會圍繞著以下三個方面來深入剖析qiankun內部原始碼和相關實現原理:

qiankun如何實現使用者只需設定一個URL就可以載入相應子應用資源的;

qiankun如何幫助使用者做到子應用間獨立執行的(包括JS互不影響和CSS互不汙染);

qiankun如何幫助使用者實現更簡便高效的應用間通訊的;

三、子應用載入

qiankun的子應用註冊方式非常簡單,使用者只需要呼叫registerMicroApps函數並將所需引數傳入即可.前文中我們說到qiankun是基於single-spa二次封裝的框架,因此qiankun中的路由監聽和子應用生命週期管理實際上都是交給了single-spa來進行實現的。我們一起來看一下該方法的實現方式(部分擷取)

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>,
) {
  // 判斷應用是否註冊過,保證每個應用只註冊一次
  const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name));
  microApps = [...microApps, ...unregisteredApps];
  unregisteredApps.forEach((app) => {
    // 取出使用者輸入的引數
    const { name, activeRule, loader = noop, props, ...appConfig } = app;
    // 呼叫single-spa的子應用註冊函數,將使用者輸入的引數轉換為single-spa所需的引數
    registerApplication({
      name,
      // 這裡提供了single-spa所需的子應用載入方式函數
      app: async () => {
        loader(true);
        await frameworkStartedDefer.promise;
				// 呼叫轉換函數loadApp將使用者輸入的url等解析轉換執行,最終生成增強後的子應用生命週期函數(包括mount,unmount,bootstrap)
        const { mount, ...otherMicroAppConfigs } = (
          await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
        )();
				// 返回值為loadApp生成的一系列生命週期函數,其中mount函數陣列再次增強
        return {
          mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
          ...otherMicroAppConfigs,
        };
      },
      activeWhen: activeRule,
      customProps: props,
    });
  });
}

可以看到,qiankun在子應用載入上所做的工作就是將使用者呼叫registerMicroApps時所提供的引數經過一系列處轉換之後,改造成single-spa中registerApplication所需要的引數。下面,我們給出qiankun中實現該轉換子的主要函數loadApp的部分實現程式碼(原始碼地址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引數),得到了template(HTML模版),execScripts(所依賴JS檔案的執行函數)以及assetPublicPath(公共資源路徑)
  const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
  // 。。。。。。
  // 在window沙箱中(global引數)執行entry依賴的js檔案,得到相關生命週期( bootstrap, mount, unmount, update)
  // 這裡可以忽略getLifecyclesFromExports函數,其返回與scriptExports一致,只是為了檢查子應用是否匯出了必須的生命週期
  const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {
    scopedGlobalVariables: speedySandbox ? trustedGlobals : [],
  });
  const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
    scriptExports,
    appName,
    global,
    sandboxContainer?.instance?.latestSetProp,
  );
  // 。。。。。
  // 匯出single-spa所需設定的getter方法(因為設定項與子應用掛在的container相關,預設為使用者輸入的container,後續使用者可以手動載入子應用並指定其渲染位置)
  const initialContainer = 'container' in app ? app.container : undefined;
  const parcelConfigGetter: ParcelConfigObjectGetter = (remountContainer = initialContainer) => {
    const parcelConfig: ParcelConfigObject = {
      name: appInstanceId,
      bootstrap,
      // mount陣列在子應用渲染時依次執行
      mount: [
        // 。。。。。。
        // 執行沙箱隔離
        mountSandbox,
        // 呼叫使用者自定義mount生命週期,並傳入setGlobalState/onGlobalStateChange的應用間通訊方法函數
        async (props) => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }),
        // 。。。。。。
      ],
      // unmount陣列在子應用解除安裝時依次執行
      unmount: [
        // 。。。。。。。
        // 呼叫使用者自定義unmount生命週期
        async (props) => unmount({ ...props, container: appWrapperGetter() }),
        // 解除安裝隔離沙箱
        unmountSandbox,
        // 清理工作
        async () => {
          render({ element: null, loading: false, container: remountContainer }, 'unmounted');
          // 清理子應用對全域性通訊的訂閱
          offGlobalStateChange(appInstanceId);
          // for gc
          appWrapperElement = null;
          syncAppWrapperElement2Sandbox(appWrapperElement);
        },
        // 。。。。。。。
      ],
    };
    return parcelConfig;
  }
	return parcelConfigGetter
}

可以看到,qiankun在其載入函數loadApp中做了一些額外的工作。

為了方便使用,qiankun提供了基於url入口來載入子應用的方式。為了獲取使用者提供的html檔案(或者資原始檔陣列)並解析出其中所需的資源,qiankun依賴了import-html-entry庫中的相關方法,執行並得到了子應用匯出的使用者自定義生命週期。

對使用者自定義的生命週期進行增強(包括掛載/解除安裝應用間的隔離沙箱,初始化或傳入應用間通訊方法等等),返回框架增強後的生命週期函數陣列並註冊在single-spa中。

經過原始碼的分析我們可以看出,qiankun在子應用載入上就是作為中間層存在的,其主要作用就是簡化使用者對於子應用註冊的輸入,通過框架內部的方法轉換並增強了使用者的輸入最終將其傳入了single-spa之中,在後續的執行中真正負責子應用載入解除安裝的是single-spa。

微前端框架qiankun原始碼剖析之下篇

以上就是微前端框架qiankun原始碼剖析之上篇的詳細內容,更多關於微前端框架qiankun剖析的資料請關注it145.com其它相關文章!


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