深入分析single-spa——启动与应用管理

921 阅读3分钟

相关链接

本文是最近分析single-spa中的一篇,全部的文章如下:

  1. 深入分析single-spa——导航事件与reroute
  2. 深入分析single-spa——启动与应用管理
  3. 深入分析single-spa——事件机制
  4. 其他关于模块机制、生命周期、微前端类型的深入分析正在进行中

Application是single-spa中重要的部分,single-spa在运行时,会涉及到应用的注册/取消注册和卸载。本文将对single-spa自身的启动以及其中对应用的管理进行深入分析。

single-spa的启动

首先我们先来看一下single-spa的一段示例代码:

// single-spa-config.js
import { registerApplication, start } from 'single-spa';

// Simple usage
registerApplication(
  'app2',
  () => import('src/app2/main.js'),
  (location) => location.pathname.startsWith('/app2'),
  { some: 'value' }
);

// Config with more expressive API
registerApplication({
  name: 'app1',
  app: () => import('src/app1/main.js'),
  activeWhen: '/app1',
  customProps: {
    some: 'value',
  }
});

start();

其中,我们可以看到:

  • single-spa导出了registerApplicationstart两个方法

  • start用来启动single-spa应用;在此之前,我们还可以通过registerApplication注册微前端应用,

  • registerApplication用来在single-spa中注册微前端应用,这样single-spa就可以知道应该在何时/如何去初始化、加载、卸载应用

start

single-spa的start部分的代码如下:

export function start(opts) {
  started = true;
  if (opts && opts.urlRerouteOnly) {
    setUrlRerouteOnly(opts.urlRerouteOnly);
  }
  if (isInBrowser) {
    reroute();
  }
}

在这里我们可以看到:

  • start接受一个opts对象作为参数,其中唯一的配置项为urlReouteOnly;如果设置urlReouteOnly为true,那么history.pushState()history.replaceState()将不会触发reroute
  • 如果在浏览器环境中,调用start将会触发reroute的执行

应用管理

single-spa中对于微前端应用的管理,主要涉及到:

  • 注册应用,会配置应用名称、激活时机等信息
  • 取消注册应用,在取消注册的时候会卸载对应应用

应用注册——registerApplication

执行流程

registerApplication.png

关于registerApplication的使用,我们可以直接去看代码:

export function registerApplication(
  appNameOrConfig,
  appOrLoadApp,
  activeWhen,
  customProps
) {
  const registration = sanitizeArguments(
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  );

  if (getAppNames().indexOf(registration.name) !== -1)
    throw Error(
      formatErrorMessage(
        21,
        __DEV__ &&
          `There is already an app registered with name ${registration.name}`,
        registration.name
      )
    );

  apps.push(
    assign(
      {
        loadErrorTime: null,
        status: NOT_LOADED,
        parcels: {},
        devtools: {
          overlays: {
            options: {},
            selectors: [],
          },
        },
      },
      registration
    )
  );

  if (isInBrowser) {
    ensureJQuerySupport();
    reroute();
  }
}

registerApplication函数接受4个参数:

  • appNameOrConfig:应用名称,需要保证全局唯一;如果重复注册相同名称的应用,则会抛出错误
  • appOrLoadApp:应用的定义,用来加载应用,可以是包含single-spa生命周期的对象/加载应用的方法
  • activeWhen:用来匹配应用的函数——activity function或者需要匹配的路径,用来判断应用是否应当被激活
  • customProps:传递给应用的自定义属性

应用取消注册——unregisterApplication

unregisterApplication是与registerApplication相对的方法,但相对来说简单很多:

  • 判断应用是否已经注册过;如果没有注册过,抛出错误
  • 如果已注册过,则调用unloadApplication,并将应用从apps列表中移出
执行流程:

unregisterApplication.png

代码如下:
export function unregisterApplication(appName) {
  if (apps.filter((app) => toName(app) === appName).length === 0) {
    throw Error(
      formatErrorMessage(
        25,
        __DEV__ &&
          `Cannot unregister application '${appName}' because no such application has been registered`,
        appName
      )
    );
  }
  return unloadApplication(appName).then(() => {
    const appIndex = apps.map(toName).indexOf(appName);
    apps.splice(appIndex, 1);
  });
}

卸载应用——unloadApplication

在unregisterApplication的最终,将调用unloadApplication。卸载完成的应用,将恢复到NOT_LOADED的状态,下次激活时需要重新加载。

执行流程

unloadApplication.png

源码

export function unloadApplication(appName, opts = { waitForUnmount: false }) {
  if (typeof appName !== "string") {
    throw Error(
      formatErrorMessage(
        26,
        __DEV__ && `unloadApplication requires a string 'appName'`
      )
    );
  }
  const app = find(apps, (App) => toName(App) === appName);
  if (!app) {
    throw Error(
      formatErrorMessage(
        27,
        __DEV__ &&
          `Could not unload application '${appName}' because no such application has been registered`,
        appName
      )
    );
  }

  const appUnloadInfo = getAppUnloadInfo(toName(app));
  if (opts && opts.waitForUnmount) {
    // We need to wait for unmount before unloading the app

    if (appUnloadInfo) {
      // Someone else is already waiting for this, too
      return appUnloadInfo.promise;
    } else {
      // We're the first ones wanting the app to be resolved.
      const promise = new Promise((resolve, reject) => {
        addAppToUnload(app, () => promise, resolve, reject);
      });
      return promise;
    }
  } else {
    /* We should unmount the app, unload it, and remount it immediately.
     */

    let resultPromise;

    if (appUnloadInfo) {
      // Someone else is already waiting for this app to unload
      resultPromise = appUnloadInfo.promise;
      immediatelyUnloadApp(app, appUnloadInfo.resolve, appUnloadInfo.reject);
    } else {
      // We're the first ones wanting the app to be resolved.
      resultPromise = new Promise((resolve, reject) => {
        addAppToUnload(app, () => resultPromise, resolve, reject);
        immediatelyUnloadApp(app, resolve, reject);
      });
    }

    return resultPromise;
  }
}

在卸载应用的过程中,我们可以看到如下两个函数的调用:

  • addAppToUnload:通过将应用加入到appToUnload中,等待后续处理
  • immediatelyUnloadApp:立即调用生命周期方法中与unmountunload有关的方法,来卸载应用

addAppToUnload

通过将需要unload的应用,加入到appToUnload中,等待稍后处理

export function addAppToUnload(app, promiseGetter, resolve, reject) {
  appsToUnload[toName(app)] = { app, resolve, reject };
  Object.defineProperty(appsToUnload[toName(app)], "promise", {
    get: promiseGetter,
  });
}

immediatelyUnloadApp

链式调用toUnmountPromisetoUnloadPromise,来进行应用的卸载

执行流程

immediatelyUnloadApp.jpg

function immediatelyUnloadApp(app, resolve, reject) {
  toUnmountPromise(app)
    .then(toUnloadPromise)
    .then(() => {
      resolve();
      setTimeout(() => {
        // reroute, but the unload promise is done
        reroute();
      });
    })
    .catch(reject);
}

参考资料

Configuration