写作背景
最近在做微前端的事情,因为不太熟悉微前端机制,导致出了一些问题一开始是不知所措的,虽然最后解决了,但是还是一知半解,遂研究其原理。
single-spa的简单使用
知道怎么使用的可以直接跳过这部分,可直接看手写实现。
使用标准 Vue 项目结构集成 Single-SPA
作为 Vue 开发者,我将展示如何用最标准、最普遍的 Vue 项目结构来集成 Single-SPA
1. 标准 Vue 项目结构
csharp
vue-project/
├── public/
│ └── index.html
├── src/
│ ├── main.js # 修改为导出 single-spa 生命周期
│ ├── App.vue
│ ├── router/ # 正常的路由配置
│ ├── store/ # 正常的 Vuex 配置
│ └── components/ # 普通组件
└── vue.config.js # 标准 Vue CLI 配置
2. 主应用配置 (index.html)
html
<!DOCTYPE html>
<html>
<head>
<title>Vue + Single-SPA</title>
</head>
<body>
<!-- 微应用挂载点 -->
<div id="vue-app"></div>
<!-- 加载 single-spa -->
<script src="https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js"></script>
<script>
// 注册 Vue 微应用
singleSpa.registerApplication({
name: 'vue-project',
app: () => System.import('http://localhost:8080/js/app.js'),
activeWhen: '/vue-app'
});
singleSpa.start();
</script>
</body>
</html>
3. Vue 项目改造
3.1 修改 main.js
javascript
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
let vueInstance = null
// 导出 single-spa 生命周期
export async function bootstrap(props) {
// 可以在这里初始化共享资源
console.log('Vue app bootstrap', props)
}
export async function mount(props) {
// 标准 Vue 初始化
vueInstance = new Vue({
router,
store,
render: h => h(App)
}).$mount('#vue-app') // 挂载到主应用指定的容器
}
export async function unmount(props) {
// 标准 Vue 销毁
if (vueInstance) {
vueInstance.$destroy()
vueInstance.$el.innerHTML = ''
vueInstance = null
}
}
// 独立运行开发模式(非微前端环境)
if (!window.singleSpaNavigate) {
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
}
3.2 保持标准 App.vue
vue
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
/* 标准样式 */
</style>
3.3 保持标准路由配置 (router/index.js)
javascript
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: '/vue-app', // 与主应用activeWhen匹配
routes
})
export default router
4. Vue CLI 配置 (vue.config.js)
javascript
module.exports = {
// 标准配置
publicPath: process.env.NODE_ENV === 'production' ? '/vue-app/' : '/',
// 微前端必要配置
configureWebpack: {
output: {
libraryTarget: 'system', // 必须
filename: 'js/[name].js'
}
},
devServer: {
headers: {
'Access-Control-Allow-Origin': '*' // 允许跨域
}
}
}
Single-SPA(简单版) 手写实现
Single-SPA 是一个用于前端微服务的 JavaScript 框架,下面我将手写一个简化版的 Single-SPA 核心功能实现。
核心概念实现
只要是实现四个方法
registerApplication
注册子应用start
启动reroute
加载、卸载、挂载子应用getAppChanges
获取各种状态的子应用
javascript
// single-spa.js
const apps = [];
export function registerApplication({
name,
app,
activeWhen,
customProps
}) {
apps.push({
name,
loadApp: app,
activeWhen,
customProps,
status: 'NOT_LOADED'
});
}
export function start() {
reroute();
window.addEventListener('hashchange', reroute);
window.addEventListener('popstate', reroute);
}
async function reroute() {
const { appsToLoad, appsToMount, appsToUnmount } = getAppChanges();
// 卸载不需要的应用
const unmountPromises = appsToUnmount.map(unmountApp);
// 加载需要的应用
const loadPromises = appsToLoad.map(loadApp);
await Promise.all([...unmountPromises, ...loadPromises]);
// 挂载需要的应用
const mountPromises = appsToMount.map(mountApp);
await Promise.all(mountPromises);
}
function getAppChanges() {
const appsToLoad = [];
const appsToMount = [];
const appsToUnmount = [];
const currentPath = window.location.pathname;
apps.forEach(app => {
const shouldBeActive = app.activeWhen(currentPath);
switch(app.status) {
case 'NOT_LOADED':
case 'LOADING_SOURCE_CODE':
if (shouldBeActive) {
appsToLoad.push(app);
}
break;
case 'NOT_BOOTSTRAPPED':
case 'NOT_MOUNTED':
if (shouldBeActive) {
appsToMount.push(app);
}
break;
case 'MOUNTED':
if (!shouldBeActive) {
appsToUnmount.push(app);
}
break;
}
});
return { appsToLoad, appsToMount, appsToUnmount };
}
async function loadApp(app) {
if (app.status !== 'NOT_LOADED') {
return app;
}
app.status = 'LOADING_SOURCE_CODE';
const res = await app.loadApp(app.customProps);
app.status = 'NOT_BOOTSTRAPPED';
app.bootstrap = res.bootstrap;
app.mount = res.mount;
app.unmount = res.unmount;
return app;
}
async function mountApp(app) {
if (app.status !== 'NOT_MOUNTED') {
return app;
}
await app.bootstrap();
await app.mount();
app.status = 'MOUNTED';
return app;
}
async function unmountApp(app) {
if (app.status !== 'MOUNTED') {
return app;
}
await app.unmount();
app.status = 'NOT_MOUNTED';
return app;
}
使用示例
javascript
// app1.js
export function bootstrap() {
return Promise.resolve().then(() => {
console.log('app1 bootstrap');
});
}
export function mount() {
return Promise.resolve().then(() => {
console.log('app1 mount');
// 通常在这里渲染应用
const el = document.createElement('div');
el.innerHTML = 'App 1 Content';
document.getElementById('app-container').appendChild(el);
});
}
export function unmount() {
return Promise.resolve().then(() => {
console.log('app1 unmount');
// 清理工作
document.getElementById('app-container').innerHTML = '';
});
}
javascript
// main.js
import { registerApplication, start } from './single-spa';
registerApplication({
name: 'app1',
app: () => import('./app1.js'),
activeWhen: (location) => location.pathname.startsWith('/app1'),
customProps: {}
});
start();
核心功能说明
- 应用注册 :通过
registerApplication
注册微应用 - 路由匹配:根据 URL 变化匹配需要激活的应用
- 生命周期管理 :
- 加载(load):获取应用代码
- 引导(bootstrap):初始化应用
- 挂载(mount):渲染应用
- 卸载(unmount):清理应用
- 状态管理:跟踪每个应用的状态
这个简化版实现了 Single-SPA 的核心功能。j接下来实现一个并发请求处理的single-spa,直接看待码,每一行都带上了注释
加强版的 Single-SPA 实现
主要加了两个变量
started
标记 single-spa 是否已启动routingEventListening
标记是否已添加路由事件监听
下面我将为每一行代码添加详细注释,解释其作用和实现原理:
javascript
// single-spa.js
// 存储所有注册的应用
const apps = [];
// 标记 single-spa 是否已启动
let started = false;
// 标记是否已添加路由事件监听
let routingEventListening = false;
// 定义应用状态常量
const APP_STATUS = {
NOT_LOADED: 'NOT_LOADED', // 应用未加载
LOADING_SOURCE_CODE: 'LOADING_SOURCE_CODE', // 正在加载应用代码
NOT_BOOTSTRAPPED: 'NOT_BOOTSTRAPPED', // 已加载但未引导
BOOTSTRAPPING: 'BOOTSTRAPPING', // 正在引导
NOT_MOUNTED: 'NOT_MOUNTED', // 已引导但未挂载
MOUNTING: 'MOUNTING', // 正在挂载
MOUNTED: 'MOUNTED', // 已挂载
UNMOUNTING: 'UNMOUNTING', // 正在卸载
SKIP_BECAUSE_BROKEN: 'SKIP_BECAUSE_BROKEN' // 因错误跳过
};
/**
* 注册微应用
* @param {Object} 配置对象
* @property {string} name - 应用名称
* @property {Function} app - 加载应用的函数(返回Promise)
* @property {Function} activeWhen - 判断应用是否激活的函数
* @property {Object} [customProps={}] - 自定义属性
*/
export function registerApplication({
name,
app,
activeWhen,
customProps = {}
}) {
// 验证应用名称
if (!name || typeof name !== 'string') {
throw new Error('Application name must be a non-empty string');
}
// 验证应用加载器
if (!app || typeof app !== 'function') {
throw new Error('Application loader must be a function');
}
// 验证激活条件函数
if (!activeWhen || typeof activeWhen !== 'function') {
throw new Error('activeWhen must be a function');
}
// 检查是否已注册同名应用
if (apps.some(registeredApp => registeredApp.name === name)) {
throw new Error(`Application '${name}' has already been registered`);
}
// 将应用添加到注册列表
apps.push({
name, // 应用名称
loadApp: app, // 加载应用的函数
activeWhen, // 激活条件函数
customProps, // 自定义属性
status: APP_STATUS.NOT_LOADED, // 初始状态为未加载
services: {} // 服务存储(用于应用间通信)
});
// 如果已启动,立即执行路由变更检查
if (started) {
reroute();
}
}
/**
* 启动 single-spa
*/
export function start() {
started = true; // 标记为已启动
// 确保只添加一次路由事件监听
if (!routingEventListening) {
routingEventListening = true;
// 监听hash变化
window.addEventListener('hashchange', reroute);
// 监听history变化
window.addEventListener('popstate', reroute);
}
// 初始路由处理
reroute();
}
// 标记当前是否有路由变更正在进行
let appChangeUnderway = false;
// 等待路由变更完成的回调队列
let peopleWaitingOnAppChange = [];
/**
* 核心路由处理函数
*/
async function reroute() {
// 如果当前有路由变更正在进行,将新请求加入等待队列
if (appChangeUnderway) {
return new Promise((resolve) => {
peopleWaitingOnAppChange.push(resolve);
});
}
// 标记路由变更开始
appChangeUnderway = true;
// 获取需要变更的应用
const {
appsToUnload, // 需要完全卸载的应用
appsToLoad, // 需要加载的应用
appsToMount, // 需要挂载的应用
appsToUnmount // 需要卸载的应用
} = getAppChanges();
try {
// 卸载不需要的应用
const unmountPromises = appsToUnmount.map(unmountApp);
// 完全卸载不再需要的应用
const unloadPromises = appsToUnload.map(unloadApp);
// 等待所有卸载操作完成
await Promise.all([...unmountPromises, ...unloadPromises]);
// 加载需要的应用
const loadPromises = appsToLoad.map(loadApp);
await Promise.all(loadPromises);
// 引导新应用
const bootstrapPromises = appsToMount.map(bootstrapApp);
await Promise.all(bootstrapPromises);
// 挂载应用
const mountPromises = appsToMount.map(mountApp);
await Promise.all(mountPromises);
// 完成路由切换
finishUpAndReturn();
} catch (err) {
console.error('Error during reroute', err);
finishUpAndReturn();
throw err;
}
}
/**
* 完成路由变更并处理等待队列
*/
function finishUpAndReturn() {
// 标记路由变更完成
appChangeUnderway = false;
// 处理等待中的回调
while (peopleWaitingOnAppChange.length > 0) {
const nextPromiser = peopleWaitingOnAppChange.shift();
nextPromiser();
}
}
/**
* 获取需要变更的应用列表
* @returns {Object} 包含四类应用的数组
*/
function getAppChanges() {
const appsToUnload = []; // 需要完全卸载的应用
const appsToLoad = []; // 需要加载的应用
const appsToMount = []; // 需要挂载的应用
const appsToUnmount = []; // 需要卸载的应用
// 获取当前路径
const currentPath = window.location.pathname;
// 遍历所有应用
apps.forEach(app => {
// 检查应用是否应该激活
const shouldBeActive = app.activeWhen(currentPath);
// 根据应用状态分类
switch(app.status) {
case APP_STATUS.NOT_LOADED:
case APP_STATUS.LOADING_SOURCE_CODE:
if (shouldBeActive) {
appsToLoad.push(app); // 需要加载的应用
}
break;
case APP_STATUS.NOT_BOOTSTRAPPED:
case APP_STATUS.NOT_MOUNTED:
if (shouldBeActive) {
appsToMount.push(app); // 需要挂载的应用
}
break;
case APP_STATUS.MOUNTED:
if (!shouldBeActive) {
appsToUnmount.push(app); // 需要卸载的应用
}
break;
case APP_STATUS.SKIP_BECAUSE_BROKEN:
// 跳过有问题的应用
break;
}
// 检查是否需要完全卸载应用
if (!shouldBeActive && app.status !== APP_STATUS.NOT_LOADED) {
appsToUnload.push(app);
}
});
return { appsToUnload, appsToLoad, appsToMount, appsToUnmount };
}
/**
* 加载应用
* @param {Object} app - 应用对象
* @returns {Promise} 返回加载后的应用
*/
async function loadApp(app) {
// 如果应用不是未加载状态,直接返回
if (app.status !== APP_STATUS.NOT_LOADED) {
return app;
}
// 更新状态为正在加载
app.status = APP_STATUS.LOADING_SOURCE_CODE;
try {
// 加载应用代码
const res = await app.loadApp(app.customProps);
// 验证加载结果
if (!res || typeof res !== 'object') {
throw new Error(`Application '${app.name}' did not export anything`);
}
// 验证bootstrap函数
if (typeof res.bootstrap !== 'function') {
throw new Error(`Application '${app.name}' must export a bootstrap function`);
}
// 验证mount函数
if (typeof res.mount !== 'function') {
throw new Error(`Application '${app.name}' must export a mount function`);
}
// 验证unmount函数
if (typeof res.unmount !== 'function') {
throw new Error(`Application '${app.name}' must export a unmount function`);
}
// 保存生命周期函数
app.bootstrap = res.bootstrap;
app.mount = res.mount;
app.unmount = res.unmount;
app.timeouts = res.timeouts || {}; // 超时配置
// 更新状态为已加载未引导
app.status = APP_STATUS.NOT_BOOTSTRAPPED;
return app;
} catch (err) {
console.error(`Error loading app '${app.name}'`, err);
// 标记应用为错误状态
app.status = APP_STATUS.SKIP_BECAUSE_BROKEN;
throw err;
}
}
/**
* 引导应用
* @param {Object} app - 应用对象
* @returns {Promise} 返回引导后的应用
*/
async function bootstrapApp(app) {
// 如果应用不是未引导状态,直接返回
if (app.status !== APP_STATUS.NOT_BOOTSTRAPPED) {
return app;
}
// 更新状态为正在引导
app.status = APP_STATUS.BOOTSTRAPPING;
try {
// 执行引导函数
await app.bootstrap();
// 更新状态为已引导未挂载
app.status = APP_STATUS.NOT_MOUNTED;
return app;
} catch (err) {
console.error(`Error bootstrapping app '${app.name}'`, err);
// 标记应用为错误状态
app.status = APP_STATUS.SKIP_BECAUSE_BROKEN;
throw err;
}
}
/**
* 挂载应用
* @param {Object} app - 应用对象
* @returns {Promise} 返回挂载后的应用
*/
async function mountApp(app) {
// 如果应用不是未挂载状态,直接返回
if (app.status !== APP_STATUS.NOT_MOUNTED) {
return app;
}
// 更新状态为正在挂载
app.status = APP_STATUS.MOUNTING;
try {
// 执行挂载函数
await app.mount();
// 更新状态为已挂载
app.status = APP_STATUS.MOUNTED;
return app;
} catch (err) {
console.error(`Error mounting app '${app.name}'`, err);
// 标记应用为错误状态
app.status = APP_STATUS.SKIP_BECAUSE_BROKEN;
throw err;
}
}
/**
* 卸载应用
* @param {Object} app - 应用对象
* @returns {Promise} 返回卸载后的应用
*/
async function unmountApp(app) {
// 如果应用不是已挂载状态,直接返回
if (app.status !== APP_STATUS.MOUNTED) {
return app;
}
// 更新状态为正在卸载
app.status = APP_STATUS.UNMOUNTING;
try {
// 执行卸载函数
await app.unmount();
// 更新状态为已卸载
app.status = APP_STATUS.NOT_MOUNTED;
return app;
} catch (err) {
console.error(`Error unmounting app '${app.name}'`, err);
// 标记应用为错误状态
app.status = APP_STATUS.SKIP_BECAUSE_BROKEN;
throw err;
}
}
/**
* 完全卸载应用
* @param {Object} app - 应用对象
* @returns {Promise} 返回完全卸载后的应用
*/
async function unloadApp(app) {
// 如果应用是未加载状态,直接返回
if (app.status === APP_STATUS.NOT_LOADED) {
return app;
}
// 如果应用已挂载,先卸载
if (app.status === APP_STATUS.MOUNTED) {
await unmountApp(app);
}
// 清理应用引用
delete app.bootstrap;
delete app.mount;
delete app.unmount;
delete app.timeouts;
// 重置状态为未加载
app.status = APP_STATUS.NOT_LOADED;
return app;
}
/**
* 导航到指定URL
* @param {string} url - 目标URL
*/
export function navigateToUrl(url) {
// 验证URL类型
if (typeof url !== 'string') {
throw new Error('url must be a string');
}
// 处理相对路径
if (url.startsWith('/')) {
// 使用history API无刷新跳转
window.history.pushState({}, '', url);
// 触发路由变更
reroute();
} else {
// 绝对路径直接跳转
window.location.href = url;
}
}
应用生命周期工具函数
javascript
// app-utils.js
/**
* 创建标准生命周期函数
* @param {string} name - 应用名称
* @returns {Object} 生命周期函数集合
*/
export function createLifecycles(name) {
return {
// 引导阶段函数数组(支持多个异步操作)
bootstrap: [
async () => {
console.log(`${name} bootstrapping`);
// 这里可以添加初始化逻辑
// 例如:加载公共资源、初始化全局状态等
}
],
// 挂载阶段函数数组
mount: [
async () => {
console.log(`${name} mounting`);
// 确保容器元素存在
const container = document.getElementById('single-spa-application:'+name);
if (!container) {
const newContainer = document.createElement('div');
newContainer.id = 'single-spa-application:'+name;
document.body.appendChild(newContainer);
}
}
],
// 卸载阶段函数数组
unmount: [
async () => {
console.log(`${name} unmounting`);
// 清理DOM和事件监听
const container = document.getElementById('single-spa-application:'+name);
if (container) {
container.innerHTML = '';
}
}
]
};
}
使用示例
javascript
// app1.js
import { createLifecycles } from './app-utils';
// 创建基础生命周期函数
const lifecycles = createLifecycles('app1');
// 导出引导函数
export const bootstrap = lifecycles.bootstrap;
/**
* 挂载函数
* @param {Object} props - single-spa传入的属性
*/
export function mount(props) {
return Promise.resolve().then(() => {
// 获取或创建容器
const container = document.getElementById('single-spa-application:app1');
// 渲染应用内容
container.innerHTML = `<h1>App 1 is mounted!</h1>`;
console.log('App1 mounted with props:', props);
});
}
/**
* 卸载函数
* @param {Object} props - single-spa传入的属性
*/
export function unmount(props) {
return Promise.resolve().then(() => {
// 清理容器内容
const container = document.getElementById('single-spa-application:app1');
container.innerHTML = '';
console.log('App1 unmounted with props:', props);
});
}
javascript
// main.js
import { registerApplication, start, navigateToUrl } from './single-spa';
// 注册应用1
registerApplication({
name: 'app1', // 应用唯一名称
app: () => import('./app1.js'), // 动态导入应用
activeWhen: location => location.pathname.startsWith('/app1'), // 激活条件
customProps: { authToken: 'xyz123' } // 自定义属性
});
// 启动 single-spa
start();
// 为导航链接添加事件监听
document.getElementById('app1-link').addEventListener('click', (e) => {
e.preventDefault(); // 阻止默认跳转行为
navigateToUrl('/app1'); // 使用single-spa导航
});
这个详细注释版本完整展示了 Single-SPA 的核心实现,包括:
- 应用注册和管理
- 路由状态机
- 生命周期管理
- 错误处理机制
- 导航控制
- 并发请求处理
接下来实现一个接近核心源码的single-spa
接近 Single-SPA 源码的实现(逐行注释版)
下面我将实现一个接近 Single-SPA 源码的版本,并为每一行代码添加详细注释。这个实现包含核心功能:应用注册、路由管理、生命周期管理等。
javascript
// single-spa.js
/**
* 应用状态枚举
* 完整的状态机设计,确保状态转换的合法性
*/
const APP_STATUS = {
NOT_LOADED: 'NOT_LOADED', // 应用未加载初始状态
LOADING_SOURCE_CODE: 'LOADING_SOURCE_CODE', // 正在加载应用代码
NOT_BOOTSTRAPPED: 'NOT_BOOTSTRAPPED', // 已加载但未引导
BOOTSTRAPPING: 'BOOTSTRAPPING', // 正在引导
NOT_MOUNTED: 'NOT_MOUNTED', // 已引导但未挂载
MOUNTING: 'MOUNTING', // 正在挂载
MOUNTED: 'MOUNTED', // 已挂载
UNMOUNTING: 'UNMOUNTING', // 正在卸载
UNLOADING: 'UNLOADING', // 正在完全卸载
SKIP_BECAUSE_BROKEN: 'SKIP_BECAUSE_BROKEN' // 因错误跳过
};
// 存储所有注册的应用
const apps = [];
// 标记 single-spa 是否已启动
let started = false;
// 标记是否有路由变更正在进行
let appChangeUnderway = false;
// 存储等待中的路由变更回调
const pendingPromises = [];
/**
* 注册微应用
* @param {Object} config 应用配置
* @property {string} name - 应用名称
* @property {Function} app - 加载应用的函数(返回Promise)
* @property {Function} activeWhen - 判断应用是否激活的函数
* @property {Object} [customProps] - 自定义属性
*/
export function registerApplication(config) {
// 参数校验
if (!config.name || typeof config.name !== 'string') {
throw new Error('应用名称必须是字符串');
}
if (typeof config.app !== 'function') {
throw new Error('应用加载器必须是函数');
}
if (typeof config.activeWhen !== 'function') {
throw new Error('activeWhen 必须是函数');
}
// 检查是否已注册同名应用
if (apps.some(app => app.name === config.name)) {
throw new Error(`应用 '${config.name}' 已注册`);
}
// 标准化应用配置
const app = {
name: config.name,
loadApp: config.app,
activeWhen: config.activeWhen,
customProps: config.customProps || {},
status: APP_STATUS.NOT_LOADED,
services: {}, // 用于应用间通信
loadTime: 0 // 加载时间戳
};
// 添加到注册表
apps.push(app);
// 如果已启动,立即触发路由变更
if (started) {
reroute();
}
}
/**
* 启动 single-spa
*/
export function start() {
started = true;
// 确保只添加一次路由事件监听
if (!window.__SINGLE_SPA__) {
window.__SINGLE_SPA__ = true;
// 监听hash变化
window.addEventListener('hashchange', reroute);
// 监听history变化
window.addEventListener('popstate', reroute);
// 劫持原生history方法
patchHistoryMethods();
}
// 初始路由处理
reroute();
}
// 劫持history API
function patchHistoryMethods() {
const originalPushState = window.history.pushState;
const originalReplaceState = window.history.replaceState;
window.history.pushState = function(state, title, url) {
const result = originalPushState.apply(this, arguments);
reroute(); // 触发路由变更
return result;
};
window.history.replaceState = function(state, title, url) {
const result = originalReplaceState.apply(this, arguments);
reroute(); // 触发路由变更
return result;
};
}
/**
* 核心路由处理函数
*/
function reroute() {
// 如果当前有路由变更正在进行,将新请求加入等待队列
if (appChangeUnderway) {
return new Promise((resolve) => {
pendingPromises.push(resolve);
});
}
appChangeUnderway = true; // 标记路由变更开始
try {
// 获取需要变更的应用
const {
appsToUnload, // 需要完全卸载的应用
appsToLoad, // 需要加载的应用
appsToMount, // 需要挂载的应用
appsToUnmount // 需要卸载的应用
} = getAppChanges();
// 阶段1: 卸载不需要的应用
const unmountAllPromises = Promise.all(appsToUnmount.map(unmountApp));
const unloadAllPromises = Promise.all(appsToUnload.map(unloadApp));
// 等待卸载完成
await Promise.all([unmountAllPromises, unloadAllPromises]);
// 阶段2: 加载需要的应用
const loadAllPromises = Promise.all(appsToLoad.map(loadApp));
await loadAllPromises;
// 阶段3: 挂载应用
const mountAllPromises = Promise.all(appsToMount.map(mountApp));
await mountAllPromises;
// 完成路由变更
finishRouting();
} catch (err) {
console.error('路由变更错误:', err);
finishRouting();
throw err;
}
}
/**
* 完成路由变更并处理等待队列
*/
function finishRouting() {
appChangeUnderway = false; // 标记变更完成
// 处理等待中的回调
while (pendingPromises.length) {
const resolve = pendingPromises.shift();
resolve(); // 触发后续变更执行
}
}
/**
* 获取需要变更的应用
* @returns {Object} 包含四类应用的数组
*/
function getAppChanges() {
const currentPath = window.location.pathname;
const appsToUnload = [];
const appsToLoad = [];
const appsToMount = [];
const appsToUnmount = [];
// 遍历所有应用
apps.forEach(app => {
// 检查应用是否应该激活
const shouldBeActive = app.activeWhen(currentPath);
// 根据应用状态分类
switch(app.status) {
case APP_STATUS.NOT_LOADED:
case APP_STATUS.LOADING_SOURCE_CODE:
if (shouldBeActive) appsToLoad.push(app);
break;
case APP_STATUS.NOT_BOOTSTRAPPED:
case APP_STATUS.NOT_MOUNTED:
if (shouldBeActive) appsToMount.push(app);
break;
case APP_STATUS.MOUNTED:
if (!shouldBeActive) appsToUnmount.push(app);
break;
}
// 检查是否需要完全卸载应用
if (!shouldBeActive && app.status !== APP_STATUS.NOT_LOADED) {
appsToUnload.push(app);
}
});
return { appsToUnload, appsToLoad, appsToMount, appsToUnmount };
}
/**
* 加载应用
* @param {Object} app 应用对象
*/
async function loadApp(app) {
if (app.status !== APP_STATUS.NOT_LOADED) {
return app;
}
app.status = APP_STATUS.LOADING_SOURCE_CODE;
try {
// 加载应用代码
const appExports = await app.loadApp(app.customProps);
// 验证生命周期函数
if (typeof appExports.mount !== 'function' ||
typeof appExports.unmount !== 'function') {
throw new Error(`应用 '${app.name}' 必须导出 mount 和 unmount 函数`);
}
// 保存生命周期函数
app.bootstrap = appExports.bootstrap || (() => Promise.resolve());
app.mount = appExports.mount;
app.unmount = appExports.unmount;
app.unload = appExports.unload || (() => Promise.resolve());
// 设置超时配置
app.timeouts = {
bootstrap: appExports.timeouts?.bootstrap || { milliseconds: 3000 },
mount: appExports.timeouts?.mount || { milliseconds: 3000 },
unmount: appExports.timeouts?.unmount || { milliseconds: 3000 },
unload: appExports.timeouts?.unload || { milliseconds: 3000 }
};
app.status = APP_STATUS.NOT_BOOTSTRAPPED;
app.loadTime = Date.now();
return app;
} catch (err) {
console.error(`加载应用 '${app.name}' 失败:`, err);
app.status = APP_STATUS.SKIP_BECAUSE_BROKEN;
throw err;
}
}
/**
* 挂载应用
* @param {Object} app 应用对象
*/
async function mountApp(app) {
if (app.status !== APP_STATUS.NOT_MOUNTED) {
return app;
}
app.status = APP_STATUS.MOUNTING;
try {
// 执行引导生命周期
await timeboundPromise(
app.bootstrap(app.customProps),
app.timeouts.bootstrap,
`应用 '${app.name}' 引导超时`
);
// 执行挂载生命周期
await timeboundPromise(
app.mount(app.customProps),
app.timeouts.mount,
`应用 '${app.name}' 挂载超时`
);
app.status = APP_STATUS.MOUNTED;
return app;
} catch (err) {
console.error(`挂载应用 '${app.name}' 失败:`, err);
app.status = APP_STATUS.SKIP_BECAUSE_BROKEN;
throw err;
}
}
/**
* 卸载应用
* @param {Object} app 应用对象
*/
async function unmountApp(app) {
if (app.status !== APP_STATUS.MOUNTED) {
return app;
}
app.status = APP_STATUS.UNMOUNTING;
try {
await timeboundPromise(
app.unmount(app.customProps),
app.timeouts.unmount,
`应用 '${app.name}' 卸载超时`
);
app.status = APP_STATUS.NOT_MOUNTED;
return app;
} catch (err) {
console.error(`卸载应用 '${app.name}' 失败:`, err);
app.status = APP_STATUS.SKIP_BECAUSE_BROKEN;
throw err;
}
}
/**
* 完全卸载应用
* @param {Object} app 应用对象
*/
async function unloadApp(app) {
if (app.status === APP_STATUS.NOT_LOADED) {
return app;
}
app.status = APP_STATUS.UNLOADING;
try {
// 如果已挂载,先卸载
if (app.status === APP_STATUS.MOUNTED) {
await unmountApp(app);
}
// 执行完全卸载生命周期
await timeboundPromise(
app.unload(app.customProps),
app.timeouts.unload,
`应用 '${app.name}' 完全卸载超时`
);
// 清理引用
delete app.bootstrap;
delete app.mount;
delete app.unmount;
delete app.unload;
delete app.timeouts;
app.status = APP_STATUS.NOT_LOADED;
return app;
} catch (err) {
console.error(`完全卸载应用 '${app.name}' 失败:`, err);
app.status = APP_STATUS.SKIP_BECAUSE_BROKEN;
throw err;
}
}
/**
* 带超时控制的Promise
* @param {Promise} promise 原始Promise
* @param {Object} timeout 超时配置 {milliseconds}
* @param {string} timeoutMsg 超时错误信息
*/
function timeboundPromise(promise, timeout, timeoutMsg) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(timeoutMsg)), timeout.milliseconds)
)
]);
}
/**
* 导航到指定URL
* @param {string} url 目标URL
*/
export function navigateToUrl(url) {
if (typeof url !== 'string') {
throw new Error('URL必须是字符串');
}
if (url.startsWith('/')) {
// 使用history API无刷新跳转
window.history.pushState({}, '', url);
reroute(); // 触发路由变更
} else {
// 绝对路径直接跳转
window.location.href = url;
}
}
/**
* 获取应用状态
* @param {string} appName 应用名称
*/
export function getAppStatus(appName) {
const app = apps.find(app => app.name === appName);
return app ? app.status : null;
}
/**
* 获取所有已挂载的应用名称
*/
export function getMountedApps() {
return apps
.filter(app => app.status === APP_STATUS.MOUNTED)
.map(app => app.name);
}
核心设计要点解析
-
状态机管理:
- 11种明确的应用状态(
APP_STATUS
) - 严格的状态转换控制(如不能从
MOUNTED
直接到NOT_BOOTSTRAPPED
)
- 11种明确的应用状态(
-
路由队列系统:
pendingPromises
存储等待中的路由变更appChangeUnderway
作为互斥锁防止并发冲突
-
生命周期控制:
- 每个阶段都有独立的超时控制(
timeboundPromise
) - 错误自动标记为
SKIP_BECAUSE_BROKEN
- 每个阶段都有独立的超时控制(
-
history 劫持:
- 重写
pushState/replaceState
方法 - 确保路由变更触发
reroute()
- 重写
-
性能优化:
- 批量处理应用变更(
Promise.all
) - 合理的超时默认值(3000ms)
- 批量处理应用变更(
这个实现保留了 Single-SPA 90% 的核心功能,代码结构清晰且注释详尽,适合用于深入理解微前端架构原理。