一、single-spa 框架概述
1. Single-spa 是一种组织微前端路由的方案,每个微前端应用都是浏览器中的一个JS模块
2. Single-spa 通过劫持路由的方式来做子应用之间的切换,但接入方式需要融合自身的路由,有一定的局限性
3. Single-spa 为我们提供了基座化的微前端方案,将应用分为两类,基座应用和子应用
- 基座应用: 会维护一个注册表-每个路由对应一个子应用,基座应用启动之后,当我们切换路由,如果是新的子应用,会动态获取子应用的js脚本,然后执行脚本并渲染出相应的页面; 如果是曾经访问过的页面,那么会从基座应用的缓存中获取已经缓存的子应用,激活子应用并渲染出对应的页面
子应用可以独立构建,运行时动态加载和主应用完全解耦,技术栈无关,靠的是协议接入(子应用必须导出boostrap、mount、unmount方法)
二、single-spa app 的主要组成部分
2.1 Configuring single-spa
用于启动子应用的基座配置文件,主要组成部分包括
1. 所有微前端应用共享的根HTML页面
2. 调用 singleSpa.registerApplication()
注册子应用
// single-spa-config.js
import {registerApplication, start} from 'single-spa'
// 创建script标签将js脚本添加到当前上下文中
function createScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
})
}
// 异步加载子应用
fucntion loadApp(url, globalVar) {
return async () => {
await createScript(`${url}/js/chunk-vendors.js`);
await createScript(`${url}/js/app.js`);
return window[globalVar]
}
}
// 注册子应用
registerApplication({
name: 'app1',
app: loadApp('http://localhost:8801', 'app1'),
activeWhen: location => location.pathname.startsWith('/app1'),
customProps: {
name: 'micro app1'
},
// customProps: (name, loaction) => ({
// name: 'micro app1',
// })
})
注册配置参数:
- name: 应用名称
- app: 加载函数(loading funtion), 必须返回promise(resolve 之后的结果必须是一个可以解析的应用), 会在应用第一次被下载时调用
- activeWhen: 激活函数(activity function), window.location 作为这个函数的第一个参数被调用,当函数的返回值是一个truth时应用会被激活,activity function 回根据window.location.path来决定该应用是否需要被激活, 在下面几种情况下,single-spa将会调用每个应用的活动函数
- haschange 或者 popstate 事件触发时
- pushstate 或者 replaceState 被调用时
- 在single-spa上手动调用 triggerAppchange 的方法
- checkActivityFunction 方法被调用时
- customProps: 自定义属性, 这个参数的内容会被传递给子应用的生命周期函数中, 可以时一个对象,也可以时一个返回Object的函数,当时返回Object 的函数时,函数的参数是,name 和 location
3. 调用 singleSpa.start()
挂载子应用
- 在start 方法被调用之前,应用会先被下载,但是不会初始化/挂载/卸载,只有调用start方法,应用才会被真正挂载
2.2 Single-spa Application
: 子应用,除了在single-spa application 中没有html页面之外,其余特征和我们平时的单页应用是一样的。并且这些子应用可以使用不同的框架。
- 子应用被注册之后只用维护自己的客户端路由,只用自己需要的框架或者类库
- 可以通过
Activity function
来判断子应用是否已经被挂载了 - 子应用在未挂载之前会一直处于休眠状态
2.2.1 注册应用的生命周期
load
: 下载, 注册的应用会被基座应用懒加载,注册应用在activity function 第一次返回truth 的时候,会执行下载。- 在下载的过程中要尽量较少各种操作的执行,可以在boostrap 生命周期再执行各项操作
- 确实有需要执行的操作时,可以将代码放到子应用的入口文件中执行
boostrap
: 初始化,获取静态资源, 会在应用第一次挂载前执行一次mount
: 当应用根据activity function 返回的truth 时,但是应用处于未挂载状态时,这个生命周期会被调用,调用时,函数会根据URL 来确定当前被激活的路由,创建DOM元素,监听dom事件等以向用户呈现渲染的内容。- 任何子路由的改变(haschange/popstate等)都不回再次出发mount,需要各应用自行处理
unmount
: 卸载应用,当activity function 返回false,但是该应用已经挂载时,卸载的生命周期函数旧会被调用。- unmount 被调用时,会清理在挂载应用时被创建的DOM元素、事件舰艇、内存、全局变量和消息订阅等
unload
: 移除应用- 移除的目的是,各应用在移除之前执行部分逻辑,一旦应用被一处,它的状态将会变成NOT_LOADED,下次激活是会被重新初始化
- 移除函数的设计动机是对所有注册的应用实现热下载。不过在其他场景中也非常有用(如想重新初始化一个应用,且在重新初始化之前执行一些逻辑)
- 全局超时配置: 默认情况下,所有注册的应用遵循全局超时配置,但对于每个应用,也可以通过在主入口文件导出一个timeouts对象来重新定义
export function bootstarp(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: false,
warningMillis: 2500,
},
unload: {
millis: 5000,
dieOnTimeout: false,
warningMillis: 2500,
}
}
四、 Parcel
Parcel 是 single-spa 的一个高级特性。指一个与框架无关的组件,封装一系列的功能,可以被应用手动卸载
- parcel 和注册应用的api一致,区别在于,parcel 需要手动挂载, 而不是通过activity function 激活
- 一个 parcel 可以大到一个应用,也可以小刀一个组件,可以用任何语言实现,只要最后能导出正确的生命周期时间即可
- 如果我们只使用了一种框架,建议使用框架组件(React, vue, angular),而不是parcel 共享功能
- parcel 比起框架组件中间多包裹了一层中间层,而框架组件在应用之间调用起来更容易,可以通过import语法直接导入
只有在涉及到跨框架的应用之间进行组件调用时才需要考虑parcel
// parcel 实现
const parcelConfig = {
bootstrap() {
// 初始化
retrun Promise.resolve()
},
mount() {
// 使用某个框架来创建和初始化dom
return Promise.resolve()
},
unmount() {
// 使用某个框架来卸载dom,进去其他的清理工作
return Promise.resolve()
}
}
// 挂载parcel
const domElement = document.getElementById('parcel-in-dom-to-mount-parcel');
const parcelProps = {domElement, customProps1: 'foo'};
const parcel = singleSpa.mountRootParcel(parcelConfig, parcelProps);
// parcel 被挂载之后在mountPromise 中结束挂载
parcel.mountPromise.then(() => {
console.log(('finished mounting parcel');
// 如果想重新渲染parcel,可以调用update生命周期方法,其返回值是一个promise
parcelProps.customProp1 = 'bar';
return parcel.update(parcelProps);
})
.then(() => {
// 此处调用unmount生命周期方法来卸载parcel,返回一个promise
return parcel.unmount()
})
4.1 parcel 生命周期
Bootstrap
: 初始化, 在parcel第一次挂载时调用一次Mount
: 挂载,在mountParcel方法被调用且parcel未挂载时触发,一般会创建DOM元素、初始化事件监听等,从而为用户展示内容Unmount
: 卸载, parcel 已被卸载并且满足unmount()被调用
+父parcel或者父应用 被卸载
时触发,被调用时这个方法会清除dom元素、dom事件监听,亲历内存泄漏,全局变量,事件订阅等在挂载parcel时创建的内容Update
: 触发更新生命周期函数
五、single-spa 优势
- 能支持大部分主流的前端框架,也能支持传统的前端框架
- 提供更好的用户体验,不需要页面跳转,直接在当前页面载入
- 方便迁移旧的遗留系统
六、 single-spa 缺陷
- 系统构建复杂,应用需要集成在一起进行构建
- 不支持不同应用的部署分离
- 代码结构复杂
- 学习成本高
七、single-spa 遗留了哪些问题?
single-spa 实现了路由切换时,对子应用的加载、卸载,但是single-spa 没有解决资源加载、沙箱、全局状态管理的问题,qiankun 基于single-spa进行封装,完善了这些问题