single-spa 文档阅读笔记

820 阅读9分钟

single-spa 文档阅读笔记

官网地址:single-spa.js.org/docs/gettin…

我这篇笔记的语雀地址:www.yuque.com/zuiyu-qwofk…

SPA架构预览

single-spa 应用有一下几个部分组成:

  1. 一个 single-spa root config,他绘制了基础的 html 页面结构 和 一些注册app的javascript文件。每一个 app 注册的时候需要以下几个部分:
    1. 一个 name
    2. 一个加载app 的 js 的 function
    3. 一个function,决定 这个app什么时候进入 active/inactive状态
  2. app,可以认为是将每一个 single-spa应用打包进入模块。
    1. 每一个应用必须知道如何在真实的 Dom 上 bootstrap,mount,和 umount 自己;
    2. 和普通应用的区别在于,single-spa 必须能够和其他 apps 共存,并且他们都没有自己的 html page;
  3. 举例:你的react、angular 的 app 在active的时候,他们监听 url 的改变,并进行路由,将自己对应的内容渲染到 DOM 上。当 inactive 的时候,他们取消监听,并将自己从DOM节点移除;

SPAs中的3种 microfrontends(MF)

  1. single-spa applications :针对特定路由渲染不同组件的 MF

    1. 使用 registerApplication进行公开声明的,在生命的时候并不直接挂载 apps
    2. 拥有统一的 SPAs的生命周期
    3. 都必须导出他们的生命周期,这样他们才可以被SPAs框架统一管理
    4. 他们还可以导出额外的 Funtions、components、logic、environment variables,来尽量做成高内聚低耦合的 MF;
  2. single-spa parcels : 不受路由控制的,渲染组件的 MF

    1. parcels 以多种方式作为正常声明流的逃生舱口存在。他存在的主要目的是为了可以在使用多个用不同frameworks编写的应用中,重用各类小UI;

    2. 用户自己管理他的生命周期,可以调用 mountParcel、moutRootParcel,这样 parcel 就立刻挂载了,并返回 parcel obejct,也可以调用 umountParcel来卸载 parcel;

    3. Parcel是最适合共享 UI 组件的地方,使用 single-spa helpers 列表中为特定框架写的助手函数,可以方便创建 parcel;他们返回一个 parcelConfig,这样 SPAs可以加载这些Parcel;举例如下:

      1. app1使用vue编写,导出一些 UI 和 logic 来创建一个 User UI组件;
      2. app2使用react编写,需要使用 app1中使用vue写的User 组件,那么在 app2 中 import { user} from "@mf/app1"; SPAs允许这样不同框架之间合作渲染;
      3. 可以认为 parels 是 SPAs 实现了 webcomponent;
  3. utility modules :不受路由控制的,自身可以不渲染任何组件,逻辑上作为共享组件导出的 MF

    1. 他不像 app 和 parcels 这两种类型工作,但是他可以导出 Funtions、variables供其他 MF import 和 use;
    2. 是存放公共逻辑的最佳位置,可以存放 plain Javascript object。举例如下:
      1. Notifications service
      2. Styleguide/component library
      3. Error tracking service
      4. Authorization service
      5. Data fetching
TopicApplicationParcelUtility
Routinghas multiple routeshas no routeshas no routes
APIdeclarative APIimperative APIexports a public interface
Renders UIrenders UIrenders UImay or may not render UI
Lifecyclessingle-spa managed lifecyclescustom managed lifecyclesexternal module: no direct single-spa lifecycles
When to usecore building blockonly needed with multiple frameworksuseful to share common logic, or create a service

Roo Config

包含以下两个部分:

  1. root html 文件,是有所的 apps 共享的;
  2. 调用 singleSpa.registerApplication()的js文件;

html文件

原文引用:

You do not have to use SystemJS when using single-spa, but many examples and tutorials will encourage you to do so because it allows you to independently deploy your applications.

你不是必须使用systemjs,但是所有的sample都鼓励你使用systemjs,它可以让你的各个apps独立部署。

registerApplications - 散落4个参数调用形式

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

第一个参数: name

必须是 string 类型

第二个参数:loading function || app config

有两种形态:

  1. 加载函数 或者 Application,返回的是一个promise 加载函数,或者是一个resolved application

例如:第二个参数可以直接是:() => import('/path/to/application.js')

  1. 一个application对象
const application = {
  bootstrap: () => Promise.resolve(), //bootstrap function
  mount: () => Promise.resolve(), //mount function
  unmount: () => Promise.resolve(), //unmount function
}
registerApplication('applicationName', application, activityFunction)

第三个参数:activity_function(location):boolean | string

activity_function(location):boolean 必须是一个纯函数:

  1. 第一个参数是 window.location;
  2. 返回值:当程序应该被激活的时候,返回 true;
  3. 考虑到很多app有多级子路由,而SPAs只作为顶级路由;
  4. SPAs会在以下场景会调用每一个app的activity function:
    1. hashchange 或 popstate event;
    2. pushstate 或 replaceState 被调用;
    3. triggerAppChange 这个api被single-spa调用;
    4. 任何时候,在checkActivityFunctions这个方法被调用时;

第四个参数: Custom props (object | (void): object)

这个参数会传递给 single-spa 生命周期函数,

  1. 是一个对象;
  2. 或者是返回一个对象的函数,参数是(appName, window.location);

registerApplication - 一个配置对象调用形式

只有第三个参数 activeWhen多了一种混合数组形式:[ string | activity_function(location):boolean]

path prefix 的匹配样例:

'/app1'

app.com/app1

app.com/app1/anythi…

🚫 app.com/app2

'/users/:userId/profile'

app.com/users/123/p…

app.com/users/123/p…

🚫 app.com/users//prof…

🚫 app.com/users/profi…

'/pathname/#/hash'

app.com/pathname/#/…

app.com/pathname/#/…

🚫 app.com/pathname#/h…

🚫 app.com/pathname#/a…

['/pathname/#/hash', '/app1']

app.com/pathname/#/…

app.com/app1/anythi…

🚫 app.com/pathname/ap…

🚫 app.com/app2

Applications

构建 SPAs app

新建一个可注册的app: create-single-spa

可以使用 SPAs官方提供的CLI: create-single-spa 源码库地址

npm install --global create-single-spa

or

yarn global add create-single-spa

create-single-spa 文档 中的参数如下:

  • --moduleType root-config | app-parcel | util-module
  • --framework react | vue | augular

已注册App的生命周期

  • 实现bootstrap、mount、umount是必须的,unload是可选的;
  • 每一个生命周期函数必须返回一个 Promise 或者 async function;
  • 如果返回的是一个函数数组 [ fun ],这些函数会在第一个调用完之后调用另一个;
  • 如果 SPAs 是一个 不启动的 not started, apps 会被loaded,但是不会执行 bootstrap、mount、unmounted;

生命周期函数的参数

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

customProps的使用场景:

  • 共享全局通用的token;
  • 向子程序发送初始化信息,比如渲染目标;
  • 共享一个全局通用事件总线,以便 apps 相互通讯;

生命周期函数

  1. load
  2. bootstrap
  3. mount
  4. unmout
  5. unload

Timeouts

在已注册的app他们都遵守一个全局timeouts配置,也可以在入口文件导出一个timeouts对象来自定义自己的配置

export function bootstrap(props) {...}
export function mount(props) {...}
export function unmount(props) {...}

export const timeouts = {
  bootstrap: {
    millis: 5000,
    dieOnTimeout: true,
    warningMillis: 2500,
  },
  mount: {
    millis: 5000,
    dieOnTimeout: false,
    warningMillis: 2500,
  },
  unmount: {
    millis: 5000,
    dieOnTimeout: true,
    warningMillis: 2500,
  },
  unload: {
    millis: 5000,
    dieOnTimeout: true,
    warningMillis: 2500,
  },
};

过度动画

分割 apps

Single-spa does not solve how code is hosted, built, or deployed.
SPAs 不是解决代码如何组织,构建或部署的。但是这里可以给出一些策略来切分。

  1. One code repo, one build
  2. Npm package
  3. Monorepos
  4. Dynamic Module loading

Parcels

  1. 可以把parcels看作是 框架无关的 组件。他是函数型的 chunk,由 app 手动挂载,而不必担心是用 vue还是用react实现的 parcels;
  2. parcel 和 app 差别在于: app 使用的是 activeWhen,而 parcels 是手动的 挂载;
  1. 一个 parcel 可以大到和一样 app 一样,也可以小到和一个组件一样小,只要他们暴露的生命周期是正确的;
  2. SPAs建议这样使用: SPA应该包含 已注册的app和许多持久的parcels;
  1. SPAS建议这样使用:在你的app中mount摸一个parcel,因为这样 parcel 会和 app 一起 unmouted;
// The parcel implementation
const parcelConfig = {
  // optional
  bootstrap(props) {
    // one time initialization
    return Promise.resolve();
  },
  // required
  mount(props) {
    // use a framework to create dom nodes and mount the parcel
    return Promise.resolve();
  },
  // required
  unmount(props) {
    // use a framework to unmount dom nodes and perform other cleanup
    return Promise.resolve();
  },
  // optional
  update(props) {
    // use a framework to update dom nodes
    return Promise.resolve();
  },
};

// How to mount the parcel
const domElement = document.getElementById('place-in-dom-to-mount-parcel');
const parcelProps = { domElement, customProp1: 'foo' };
const parcel = singleSpa.mountRootParcel(parcelConfig, parcelProps);

// The parcel is being mounted. We can wait for it to finish with the mountPromise.
parcel.mountPromise
  .then(() => {
    console.log('finished mounting parcel!');
    // If we want to re-render the parcel, we can call the update lifecycle method, which returns a promise
    parcelProps.customProp1 = 'bar';
    return parcel.update(parcelProps);
  })
  .then(() => {
    // Call the unmount lifecycle when we need the parcel to unmount. This function also returns a promise
    return parcel.unmount();
  });

生命周期:bootstrap (optional)

只会被调用一次,在第一次mount之前

生命周期:mount (required)

  1. 无论何时 mountParcel 被调用时,如果当前 parcel 没有挂载,那么 mount就会被调用;
  2. 这个function 应该 创建 Dom元素、Dom事件监听 等,用于渲染给用户;

生命周期:update (optional)

当用户调用 parcel.update()时,这个update会被调用。

由于这个生命周期是可选的,请确认当前 parcel 是否实现了这个hook。

生命周期:unmount (required)

当以下两种情况任一达成,那么已经挂载的parcel的unmount就会被调用

  1. unmount()被调用;
  2. 他的父 parcel 或者 app 被卸载时;

这个function中,应该 清理 Dom元素,Dom监听,释放内存, observable 的订阅等;

MF之间的消息通讯

import { things } from 'other-microfronted',是最有力的通讯方式

app内部的通讯

最好的MF架构是各个MF之后解耦的,不需要频繁通讯的设计。

依照这个原则,基于路由的 SPAs 应该严格按照这样的设计思路。

有三种类型在各个MF之间 通讯 / 共享:

  1. Funtions,components, logic, environment variables;
  2. API data;
  1. UI state;

第一类通讯 Funtions, components

推荐使用 跨MF的imports。

  1. 每一个MF应该(必须)有唯一的一个入口文件(entry file)作为 公共接口文件(public interface),用于控制当前MF暴露给外部的内容;
  2. 在当前的 MF 工程种,把每一个其他 MF 都设置成 extends 的,这样就标记了他们为 in-brower modules,就可以统一 import了;

API Data

  1. 到处一个带有 cache 的api获取函数, fetchWitchCache function;
  2. 导入这个函数;

UI state

注意:如果有两个 MF 频繁的通讯 UI state,可以考虑合并他们作为一个 MF。

  1. Observables / Subject(rxjs),使用这个技术方案,一个 MF 可以发送一个新值,在另一个 MF 种响应这个值的变化。 暴露这个 observable 给所有 in-browser module 的 MF,这样其他的 MF 可以 import
  2. CustomEvents,浏览器有内建的事件发送系统允许发送自定义事件,creating and triggering events ( MDN种搜索 自定义 事件)。使用 window.dispatchEvent 发送消息,在其他的 MF 种使用 window.addEventListener 订阅消息。
  3. 任何形式的 pub/sub (发布/订阅)事件发送系统;

快速开始

  1. 使用命令: npx create-single-spa --moduleType root-config 在与命令提示交互的过程中,请记住:
    1. single-spa Layout Engine 只是可选的;
    2. orgName需要保持一至,这样才能获取到你所有的app,这些app都是使用同样这个名字当作 namespace,且都是用了 in-browser module resolution;
  2. 进入新建的工程,run start,用本地浏览器打开 http://localhost:9000/;