single-spa 是什么?

Single-spa 是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架。 使用 single-spa 进行前端架构设计可以带来很多好处,例如:

  • 在同一页面上使用多个前端框架而不用刷新页面 (React, AngularJS, Angular, Ember, 你正在使用的框架)

  • 独立部署每一个单页面应用

  • 新功能使用新框架,旧的单页应用不用重写可以共存

  • 改善初始加载时间,延迟加载代码

single-spa 架构

single-spa 借鉴了组件生命周期的思想,它为应用设置了针对路由的生命周期。当应用匹配路由/处于激活状态时,应用会把自身的内容挂载到页面上;反之则卸载。典型的 single-spa 由 html 页面、应用注册脚本、应用脚本自身构成。应用注册内容包含:

  • appName:应用名

  • loadingFunction:加载应用程序的代码。

  • activityFunction 函数:确定应用程序何时处于活动状态/非活动状态。

  • customProps:自定义 props,可以不填

single-spa 又约定应用脚本包含以下生命周期:

  • load:当应用匹配路由时就会加载脚本(非函数,只是一种状态)

  • bootstrap:引导函数(对接 html,应用内容首次挂载到页面前调用)

  • mount:挂载函数

  • unmount:卸载函数(须移除事件绑定等内容)

  • unload:非必要(unload 之后会重新启动 bootstrap 流程;借助 unload 可实现热更新)。

生命周期函数获得参数包含 name(应用名)、singleSpa(实例)、mountParcel(手动挂载函数)、customProps(自定义信息),生命周期函数必须返回 Promise 或其本身为 async 函数,bootstrap、mount、unmount 生命周期函数不可缺省,生命周期函数可以指定多个,它们会构成异步调用链,逐个调用。简要流程图如下:

示例

创建一个 html 文件

<html>
  <body>
    <script src="single-spa-config.js"></script>
  </body>
</html>

config 文件

import * as singleSpa from 'single-spa';

/* loading 是一个返回 promise 的函数,用于 加载/解析 应用代码。
 * 它的目的是为延迟加载提供便利 —— single-spa 只有在需要时才会下载应用程序的代码。
 * 在这个示例中,在 webpack 中支持 import ()并返回 Promise,但是 single-spa 可以使用任何返回 Promise 的加载函数。
 */
singleSpa.registerApplication('app-1', () =>
  import ('../app1/app1.js'), pathPrefix('/app1'));
singleSpa.registerApplication('app-2', () =>
  import ('../app2/app2.js'), pathPrefix('/app2'));

singleSpa.start();

/* Single-spa 配置顶级路由,以确定哪个应用程序对于指定 url 是活动的。
  * 你可以以任何你喜欢的方式实现此路由。
  * 一种有用的约定是在 url 前面加上活动应用程序的名称,以使顶层路由保持简单。

创建应用

mport React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import Root from './root.component.tsx';

const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: Root,
  domElementGetter,
});

export function bootstrap(props) {
  return reactLifecycles.bootstrap(props);
}

export function mount(props) {
  return reactLifecycles.mount(props);
}

export function unmount(props) {
  return reactLifecycles.unmount(props);
}

function domElementGetter() {
  // 确保这里有一个 div 供渲染用
  let el = document.getElementById('app1');
  if (!el) {
    el = document.createElement('div');
    el.id = 'app1';
    document.body.appendChild(el);
  }

  return el;
}

源码解析