Appearance
第7章 single-spa 核心机制
"框架的灵魂不在它暴露了多少 API——在于它隐藏了多少复杂度,又在正确的地方把控制权还给你。"
本章要点
- 理解 single-spa "路由即应用边界"的设计哲学及其对微前端架构的深远影响
- 深入 registerApplication 的参数设计,掌握内部状态机如何管理应用的完整生命周期
- 掌握 12 种应用状态(NOT_LOADED → UNLOADING)的完整流转规则与边界条件
- 剖析 reroute 函数的核心调度逻辑:getAppChanges 如何决定加载、挂载与卸载
- 理解 toLoadPromise / toBootstrapPromise / toMountPromise / toUnmountPromise 四大 Promise 链的执行机制
如果说乾坤是中国微前端生态的代名词,那么 single-spa 就是全球微前端的基石。
这个由 Joel Denning 在 2018 年创建的框架,做了一件看似简单却意义深远的事情:它在浏览器的路由系统和多个独立应用之间,架起了一座桥梁。 当 URL 发生变化时,single-spa 自动判断哪些应用该加载、哪些该挂载、哪些该卸载——整个过程对用户来说是无感知的单页应用体验。
但"简单"是表象。当你打开 single-spa 的源码,你会发现:一个只有约 2000 行核心代码的框架,内部竟然维护着 12 种应用状态、一个精密的状态机、一套复杂的并发调度逻辑。每一行代码都在处理你可能从未想到过的边界情况——应用加载失败怎么办?用户在应用还没挂载完成时又切换了路由怎么办?两个应用的激活条件重叠时该如何处理?
本章将从源码层面彻底剖析 single-spa 的三大核心机制:注册(registerApplication)、状态管理(12 种状态的流转)、和调度(reroute)。读完这一章,你不仅能理解 single-spa 的每一个设计决策,更能看到乾坤、Wujie 等上层框架为什么要在 single-spa 之上做那些增强——因为你会清楚地看到 single-spa "故意不做"的部分。
7.1 设计哲学:路由即应用边界
7.1.1 一个核心假设
single-spa 的整个架构建立在一个核心假设之上:URL 路径是划分应用边界的最自然单位。
这个假设如此朴素,以至于很容易被忽略。但仔细想想——在一个 SPA 中,路由本来就是组织页面的方式。single-spa 只是把这个概念提升了一个层次:路由不仅组织页面,还组织应用。
typescript
// 传统 SPA:路由 → 页面
const routes = [
{ path: '/order', component: OrderPage },
{ path: '/product', component: ProductPage },
];
// single-spa:路由 → 应用
import { registerApplication, start } from 'single-spa';
registerApplication({
name: 'order-app',
app: () => System.import('https://cdn.example.com/order/main.js'),
activeWhen: '/order',
});
registerApplication({
name: 'product-app',
app: () => System.import('https://cdn.example.com/product/main.js'),
activeWhen: '/product',
});
start();从外部看,这只是把"组件"换成了"应用"。但这一步的跨越带来了根本性的不同:每个"应用"可以是一个独立构建、独立部署、独立运行的前端项目。
7.1.2 "不做什么"比"做什么"更重要
single-spa 最大的设计智慧不在于它做了什么,而在于它故意不做什么:
typescript
// single-spa 不做的事情
interface WhatSingleSpaDoesNot {
jsSandbox: never; // 不提供 JS 沙箱
cssSandbox: never; // 不提供 CSS 隔离
htmlEntry: never; // 不支持 HTML Entry 加载
communication: never; // 不提供应用间通信机制
}
// single-spa 只做的事情
interface WhatSingleSpaDoes {
registration: '注册应用与激活条件';
lifecycle: '管理 bootstrap / mount / unmount 生命周期';
routing: '监听路由变化,调度应用的挂载与卸载';
status: '维护每个应用的状态';
}这是一个极其克制的设计选择。single-spa 的定位是微前端的调度层——它只负责"什么时候加载什么应用",至于应用如何隔离、如何通信、如何共享依赖,全部留给上层方案或开发者自行解决。正是这种克制,使得 single-spa 成为了微前端的"Linux 内核"——乾坤在它之上加了沙箱和 HTML Entry,Wujie 在它之上加了 iframe 隔离。如果 single-spa 自己做了太多,反而会限制上层方案的设计空间。
7.1.3 架构全景
┌─────────────────────────────────────────────────────┐
│ 浏览器路由事件 │
│ (hashchange / popstate / pushState) │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ reroute() 调度中枢 │
│ ┌───────────┐ ┌────────────┐ ┌─────────────┐ │
│ │getAppChg │ │ Promise 链 │ │ 并发控制 │ │
│ │ 分类应用 │ │ load→boot→ │ │ appChangeUn │ │
│ │ 状态变更 │ │ mount→unmt │ │ derway 标志 │ │
│ └───────────┘ └────────────┘ └─────────────┘ │
└────────────────────┬────────────────────────────────┘
┌───────────┼──────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌────────┐
│ App A │ │ App B │ │ App C │
│ MOUNTED │ │ NOT_ │ │ LOAD_ │
│ │ │ LOADED │ │ ERROR │
└─────────┘ └──────────┘ └────────┘整个架构可以用一句话概括:路由变化触发 reroute,reroute 根据每个应用的激活条件和当前状态,决定执行加载、启动、挂载或卸载操作。
下图用 Mermaid 展示了 single-spa 的核心调度架构:
🔥 深度洞察:single-spa 的"管道架构"
single-spa 的架构本质上是一个管道(Pipeline)模式——路由事件作为输入,经过 getAppChanges 分类、Promise 链执行、状态更新三个阶段,最终输出一组 DOM 变更。这种管道架构的优势在于:每个阶段都是纯函数式的(输入决定输出),易于测试和推理。它的劣势也同样明显:整个管道是同步触发的,如果一个应用的 mount 函数执行时间过长,会阻塞后续应用的处理。理解这个权衡,才能理解为什么乾坤要在 single-spa 之上加入超时控制和并发优化。