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

引言

註意: 受篇幅限制,本文中所粘貼的代碼都是經過作者刪減梳理後的,隻為講述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剖析的資料請關註WalkonNet其它相關文章!

推薦閱讀: