走进微前端(1)手写single-spa核心原理

写作背景

最近在做微前端的事情,因为不太熟悉微前端机制,导致出了一些问题一开始是不知所措的,虽然最后解决了,但是还是一知半解,遂研究其原理。

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 核心功能实现。

核心概念实现

只要是实现四个方法

  1. registerApplication 注册子应用
  2. start启动
  3. reroute 加载、卸载、挂载子应用
  4. 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();

核心功能说明

  1. 应用注册 :通过 registerApplication 注册微应用
  2. 路由匹配:根据 URL 变化匹配需要激活的应用
  3. 生命周期管理
    • 加载(load):获取应用代码
    • 引导(bootstrap):初始化应用
    • 挂载(mount):渲染应用
    • 卸载(unmount):清理应用
  4. 状态管理:跟踪每个应用的状态

这个简化版实现了 Single-SPA 的核心功能。j接下来实现一个并发请求处理的single-spa,直接看待码,每一行都带上了注释

加强版的 Single-SPA 实现

主要加了两个变量

  1. started 标记 single-spa 是否已启动
  2. 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 的核心实现,包括:

  1. 应用注册和管理
  2. 路由状态机
  3. 生命周期管理
  4. 错误处理机制
  5. 导航控制
  6. 并发请求处理

接下来实现一个接近核心源码的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);
}

核心设计要点解析

  1. 状态机管理

    • 11种明确的应用状态(APP_STATUS
    • 严格的状态转换控制(如不能从MOUNTED直接到NOT_BOOTSTRAPPED
  2. 路由队列系统

    • pendingPromises 存储等待中的路由变更
    • appChangeUnderway 作为互斥锁防止并发冲突
  3. 生命周期控制

    • 每个阶段都有独立的超时控制(timeboundPromise
    • 错误自动标记为 SKIP_BECAUSE_BROKEN
  4. history 劫持

    • 重写 pushState/replaceState 方法
    • 确保路由变更触发 reroute()
  5. 性能优化

    • 批量处理应用变更(Promise.all
    • 合理的超时默认值(3000ms)

这个实现保留了 Single-SPA 90% 的核心功能,代码结构清晰且注释详尽,适合用于深入理解微前端架构原理。

相关推荐
太阳伞下的阿呆2 小时前
本地环境vue与springboot联调
前端·vue.js·spring boot
飞翔的佩奇2 小时前
基于SpringBoot+MyBatis+MySQL+VUE实现的名城小区物业管理系统(附源码+数据库+毕业论文+开题报告+部署教程+配套软件)
数据库·vue.js·spring boot·mysql·毕业设计·mybatis·小区物业管理系统
chancygcx_3 小时前
前端框架Vue3(二)——Vue3核心语法之OptionsAPI与CompositionAPI与setup
vue.js·前端框架
烛阴3 小时前
Ceil -- 从平滑到阶梯
前端·webgl
90后的晨仔3 小时前
🔍Vue 模板引用(Template Refs)全解析:当你必须操作 DOM 时
前端·vue.js
90后的晨仔3 小时前
👂 Vue 侦听器(watch)详解:监听数据的变化
前端·vue.js
90后的晨仔4 小时前
深入浅出 Vue 的 computed:不仅仅是“计算属性”那么简单!
前端·vue.js
Nan_Shu_6144 小时前
学习:入门uniapp Vue3组合式API版本(17)
前端·vue.js·学习·uni-app
止观止4 小时前
Remix框架:高性能React全栈开发实战
前端·react.js·前端框架·remix