single-spa 文档阅读笔记
官网地址:single-spa.js.org/docs/gettin…
我这篇笔记的语雀地址:www.yuque.com/zuiyu-qwofk…
SPA架构预览
single-spa 应用有一下几个部分组成:
- 一个 single-spa root config,他绘制了基础的 html 页面结构 和 一些注册app的javascript文件。每一个 app 注册的时候需要以下几个部分:
- 一个 name
- 一个加载app 的 js 的 function
- 一个function,决定 这个app什么时候进入 active/inactive状态
- app,可以认为是将每一个 single-spa应用打包进入模块。
- 每一个应用必须知道如何在真实的 Dom 上 bootstrap,mount,和 umount 自己;
- 和普通应用的区别在于,single-spa 必须能够和其他 apps 共存,并且他们都没有自己的 html page;
- 举例:你的react、angular 的 app 在active的时候,他们监听 url 的改变,并进行路由,将自己对应的内容渲染到 DOM 上。当 inactive 的时候,他们取消监听,并将自己从DOM节点移除;
SPAs中的3种 microfrontends(MF)
-
single-spa applications :针对特定路由渲染不同组件的 MF
- 使用 registerApplication进行公开声明的,在生命的时候并不直接挂载 apps
- 拥有统一的 SPAs的生命周期
- 都必须导出他们的生命周期,这样他们才可以被SPAs框架统一管理
- 他们还可以导出额外的 Funtions、components、logic、environment variables,来尽量做成高内聚低耦合的 MF;
-
single-spa parcels : 不受路由控制的,渲染组件的 MF
-
parcels 以多种方式作为正常声明流的逃生舱口存在。他存在的主要目的是为了可以在使用多个用不同frameworks编写的应用中,重用各类小UI;
-
用户自己管理他的生命周期,可以调用 mountParcel、moutRootParcel,这样 parcel 就立刻挂载了,并返回 parcel obejct,也可以调用 umountParcel来卸载 parcel;
-
Parcel是最适合共享 UI 组件的地方,使用 single-spa helpers 列表中为特定框架写的助手函数,可以方便创建 parcel;他们返回一个 parcelConfig,这样 SPAs可以加载这些Parcel;举例如下:
- app1使用vue编写,导出一些 UI 和 logic 来创建一个 User UI组件;
- app2使用react编写,需要使用 app1中使用vue写的User 组件,那么在 app2 中 import { user} from "@mf/app1"; SPAs允许这样不同框架之间合作渲染;
- 可以认为 parels 是 SPAs 实现了 webcomponent;
-
-
utility modules :不受路由控制的,自身可以不渲染任何组件,逻辑上作为共享组件导出的 MF
- 他不像 app 和 parcels 这两种类型工作,但是他可以导出 Funtions、variables供其他 MF import 和 use;
- 是存放公共逻辑的最佳位置,可以存放 plain Javascript object。举例如下:
- Notifications service
- Styleguide/component library
- Error tracking service
- Authorization service
- Data fetching
| Topic | Application | Parcel | Utility |
|---|---|---|---|
| Routing | has multiple routes | has no routes | has no routes |
| API | declarative API | imperative API | exports a public interface |
| Renders UI | renders UI | renders UI | may or may not render UI |
| Lifecycles | single-spa managed lifecycles | custom managed lifecycles | external module: no direct single-spa lifecycles |
| When to use | core building block | only needed with multiple frameworks | useful to share common logic, or create a service |
Roo Config
包含以下两个部分:
- root html 文件,是有所的 apps 共享的;
- 调用 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
有两种形态:
- 加载函数 或者 Application,返回的是一个promise 加载函数,或者是一个resolved application
例如:第二个参数可以直接是:() => import('/path/to/application.js')
- 一个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 必须是一个纯函数:
- 第一个参数是 window.location;
- 返回值:当程序应该被激活的时候,返回 true;
- 考虑到很多app有多级子路由,而SPAs只作为顶级路由;
- SPAs会在以下场景会调用每一个app的activity function:
- hashchange 或 popstate event;
- pushstate 或 replaceState 被调用;
- triggerAppChange 这个api被single-spa调用;
- 任何时候,在checkActivityFunctions这个方法被调用时;
第四个参数: Custom props (object | (void): object)
这个参数会传递给 single-spa 生命周期函数,
- 是一个对象;
- 或者是返回一个对象的函数,参数是(appName, window.location);
registerApplication - 一个配置对象调用形式
只有第三个参数 activeWhen多了一种混合数组形式:[ string | activity_function(location):boolean]
path prefix 的匹配样例:
'/app1'
'/users/:userId/profile'
'/pathname/#/hash'
['/pathname/#/hash', '/app1']
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 相互通讯;
生命周期函数
- load
- bootstrap
- mount
- unmout
- 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 不是解决代码如何组织,构建或部署的。但是这里可以给出一些策略来切分。
- One code repo, one build
- Npm package
- Monorepos
- Dynamic Module loading
Parcels
- 可以把parcels看作是 框架无关的 组件。他是函数型的 chunk,由 app 手动挂载,而不必担心是用 vue还是用react实现的 parcels;
- parcel 和 app 差别在于: app 使用的是 activeWhen,而 parcels 是手动的 挂载;
- 一个 parcel 可以大到和一样 app 一样,也可以小到和一个组件一样小,只要他们暴露的生命周期是正确的;
- SPAs建议这样使用: SPA应该包含 已注册的app和许多持久的parcels;
- 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)
- 无论何时 mountParcel 被调用时,如果当前 parcel 没有挂载,那么 mount就会被调用;
- 这个function 应该 创建 Dom元素、Dom事件监听 等,用于渲染给用户;
生命周期:update (optional)
当用户调用 parcel.update()时,这个update会被调用。
由于这个生命周期是可选的,请确认当前 parcel 是否实现了这个hook。
生命周期:unmount (required)
当以下两种情况任一达成,那么已经挂载的parcel的unmount就会被调用
- unmount()被调用;
- 他的父 parcel 或者 app 被卸载时;
这个function中,应该 清理 Dom元素,Dom监听,释放内存, observable 的订阅等;
MF之间的消息通讯
import { things } from 'other-microfronted',是最有力的通讯方式
app内部的通讯
最好的MF架构是各个MF之后解耦的,不需要频繁通讯的设计。
依照这个原则,基于路由的 SPAs 应该严格按照这样的设计思路。
有三种类型在各个MF之间 通讯 / 共享:
- Funtions,components, logic, environment variables;
- API data;
- UI state;
第一类通讯 Funtions, components
推荐使用 跨MF的imports。
- 每一个MF应该(必须)有唯一的一个入口文件(entry file)作为 公共接口文件(public interface),用于控制当前MF暴露给外部的内容;
- 在当前的 MF 工程种,把每一个其他 MF 都设置成 extends 的,这样就标记了他们为 in-brower modules,就可以统一 import了;
API Data
- 到处一个带有 cache 的api获取函数, fetchWitchCache function;
- 导入这个函数;
UI state
注意:如果有两个 MF 频繁的通讯 UI state,可以考虑合并他们作为一个 MF。
- Observables / Subject(rxjs),使用这个技术方案,一个 MF 可以发送一个新值,在另一个 MF 种响应这个值的变化。 暴露这个 observable 给所有 in-browser module 的 MF,这样其他的 MF 可以 import
- CustomEvents,浏览器有内建的事件发送系统允许发送自定义事件,creating and triggering events ( MDN种搜索 自定义 事件)。使用 window.dispatchEvent 发送消息,在其他的 MF 种使用 window.addEventListener 订阅消息。
- 任何形式的 pub/sub (发布/订阅)事件发送系统;
快速开始
- 使用命令: npx create-single-spa --moduleType root-config 在与命令提示交互的过程中,请记住:
- single-spa Layout Engine 只是可选的;
- orgName需要保持一至,这样才能获取到你所有的app,这些app都是使用同样这个名字当作 namespace,且都是用了 in-browser module resolution;
- 进入新建的工程,run start,用本地浏览器打开 http://localhost:9000/;