初始化开发环境

  1. 项目目录
  2. 初始化配置安装 rollup
    npm init -y
    npm install rollup rollup-plugin-serve
    
  3. rollup.config.js 打包配置(一切从简,只借助 rollup 模块化和打包的能力)
    import serve from "rollup-plugin-serve";
    // 只借助 rollup 模块化和打包的能力~,不进行过多的 rollup 配置
    export default {
      input: "./src/single-spa.js",
      output: {
        file: "./lib/umd/single-spa.js",
        format: "umd", // 默认会挂载到 window 上
        name: "singleSpa", // 挂载到 window 上的名字
        sourcemap: true,
      },
      plugins: [
        serve({
          openPage: "/index.html",
          contentBase: "",
          port: 4000,
        }),
      ],
    };
    

应用的声明周期

实现代码

index.html

<a href="#/app1">app1</a>
<a href="#/app2">app2</a>
<script src="/lib/umd/single-spa.js"></script>
<script>
// 注册应用
singleSpa.registerApplication(
    // appName:当前注册应用的名字
    'app1', 
    // loadApp:加载函数(必须返回promise),返回的结果必须包含bootstrap、mount和 unmount做为接入协议
    async () => ({ 
        bootstrap: [
            async () => {
                console.log('应用启动1-1');
            },
            async () => {
                console.log('应用启动1-2');
            }
        ],
        mount: async () => {
            console.log('应用挂载1');
        },
        unmount: async () => {
            console.log('应用卸载1')
        }
    }),
    // activityWhen:满足条件时调用loadApp方法
    location => location.hash.startsWith('#/app1'), 
    // customProps:自定义属性可用于父子应用通信
    { store: { name: '张三', age: 20 } }
);
singleSpa.registerApplication(
    // appName:当前注册应用的名字
    'app1', 
    // loadApp:加载函数(必须返回promise),返回的结果必须包含bootstrap、mount和 unmount做为接入协议
    async () => ({ 
        bootstrap: [
            async (props) => {
                props.a = 1;
                console.log('应用启动2-1');
            },
            async (props) => {
                props.b = 1;
                console.log('应用启动2-2');
            }
        ],
        mount: async (props) => {
            props.c = 1;
            console.log('应用挂载2');
        },
        unmount: async (props) => {
            props.d = 1;
            console.log('应用卸载2')
        }
    }),
    // activityWhen:满足条件时调用loadApp方法
    location => location.hash.startsWith('#/app2'), 
    // customProps:自定义属性可用于父子应用通信
    { store: { name: '李四', age: 25 } } 
);

// 启动
singleSpa.start();
</script>

single-spa.js

export { registerApplication } from "./applications/app.js";
export { start } from "./start.js";

start.js

import { reroute } from "./navigation/reroute";

export let started = false;
export function start() {
  started = true;
  reroute(); // 这个是启动应用
}

applications/app.js

import { reroute } from "../navigation/reroute.js";
import {
  shouldBeActive,
  SKIP_BECAUSE_BROKEN,
  NOT_LOADED,
  LOADING_SOURCE_CODE,
  NOT_BOOTSTRAPPED,
  NOT_MOUNTED,
  MOUNTED,
} from "./app.helpers.js";

const apps = [];
/**
 * 注册应用,维护应用的状态(状态机)
 * @param {*} appName 当前注册应用的名字
 * @param {*} loadApp 加载函数(必须返回的是promise),返回的结果必须包含bootstrap、mount和 unmount做为接入协议
 * @param {*} activeWhen 满足条件时调用 loadApp 方法
 * @param {*} customProps 自定义属性可用于父子应用通信
 */
export function registerApplication(appName, loadApp, activeWhen, customProps) {
  apps.push({
    name: appName,
    loadApp,
    activeWhen,
    customProps,
    status: NOT_LOADED, // 默认应用为未加载
  });
  reroute(); // 这个是加载应用
}

/**
 * 获取 app 的状态
 * @returns {
 *  appsToLoad: "获取要去加载的 app", 
 *  appsToMount: "获取要被挂载的 app", 
 *  appsToUnmount: "获取要被卸载的 app"
 * }
 */
export function getAppChanges() {
  const appsToUnmount = []; // 获取要被卸载的 app
  const appsToLoad = []; // 获取要去加载的 app
  const appsToMount = []; // 获取要被挂载的 app

  apps.forEach((app) => {
    // 是否需要被加载
    const appShouldBeActive =
      app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);
    switch (app.status) {
      case NOT_LOADED: // 没有被加载
      case LOADING_SOURCE_CODE: // 没有被加载
        if (appShouldBeActive) {
          appsToLoad.push(app);
        }
        break;
      case NOT_BOOTSTRAPPED: // 没有被挂载
      case NOT_MOUNTED: // 没有被加载
        if (appShouldBeActive) {
          appsToMount.push(app);
        }
        break;
      case MOUNTED: // 已经被挂载
        if (!appShouldBeActive) {
          appsToUnmount.push(app);
        }
    }
  });
  return { appsToUnmount, appsToLoad, appsToMount };
}

applications/app.helpers.js

export const NOT_LOADED = "NOT_LOADED"; // 没有加载过
export const LOADING_SOURCE_CODE = "LOADING_SOURCE_CODE"; // 加载原代码
export const NOT_BOOTSTRAPPED = "NOT_BOOTSTRAPPED"; // 没有启动
export const BOOTSTRAPPING = "BOOTSTRAPPING"; // 启动中
export const NOT_MOUNTED = "NOT_MOUNTED"; // 没有挂载
export const MOUNTING = "MOUNTING"; // 挂载中
export const MOUNTED = "MOUNTED"; // 挂载完毕
export const UPDATING = "UPDATING"; // 更新中
export const UNMOUNTING = "UNMOUNTING"; // 卸载中
export const UNLOADING = "UNLOADING"; // 没有加载中
export const LOAD_ERROR = "LOAD_ERROR"; // 加载失败
export const SKIP_BECAUSE_BROKEN = "SKIP_BECAUSE_BROKEN"; // 运行出错

/**
 * 当前 app 是否已经挂载
 * @param {*} app 当前应用
 * @returns true/false
 */
export function isActive(app) {
  return app.status === MOUNTED;
}

/**
 * 当前 app 是否应该激活
 * @param {*} app 当前应用
 * @returns true/false
 */
export function shouldBeActive(app) {
  return app.activeWhen(window.location);
}
import { getAppChanges } from "../applications/app.js";
import { started } from "../start.js";
import { toLoadPromise } from "../lifecycles/load";
import { toUnmountPromise } from "../lifecycles/unmount";
import { toBootstrapPromise } from "../lifecycles/bootstrap";
import { toMountPromise } from "../lifecycles/mount";
import "./navigator-events";

export function reroute() {
  const {
    appsToLoad, // 获取要去加载的 app
    appsToMount, // 获取要被挂载的 app
    appsToUnmount, // 获取要被卸载的 app
  } = getAppChanges();

  // start 方法调用是同步的,loadApps 加载流程是异步的
  if (started) {
    return performAppChanges(); // app 装载
  } else {
    return loadApps(); //注册应用的时候需要预加载
  }

  // 预加载应用
  async function loadApps() {
    // 获取到 bootstrap、mount、unmount 放到 app 上
    let apps = await Promise.all(appsToLoad.map(toLoadPromise)); 
    console.log(apps);
  }
  // 根据路径装载应用
  async function performAppChanges() {
    // 先卸载不需要的应用(并发处理)
    let unmountPromise = appsToUnmount.map(toUnmountPromise);
    // 加载需要的应用
    appsToLoad.map(async (app) => {
      app = await toLoadPromise(app); // 加载
      app = await toBootstrapPromise(app); // 启动
      return await toMountPromise(app); // 挂载
    });
    appsToMount.map(async (app) => {
      app = await toBootstrapPromise(app); // 启动
      return await toMountPromise(app); // 挂载
    });
  }
}
import { reroute } from "./reroute.js";

export const routingEventsListeningTo = ["hashchange", "popstate"];
// 存储 hashchang 和 popstate 注册的方法
const capturedEventListeners = {
    hashchange: [],
    popstate: [], // 应用切换完成后调用
};

function urlReroute() {
    reroute(); // 根据路径重新加载不同的应用
}
// 监听 hash 变化
window.addEventListener("hashchange", urlReroute); 
// 监听 history 路由变化(pushState 和 repalceState 不会触发)
window.addEventListener("popstate", urlReroute); 
// 利用 aop 面相切面编程的思想,重写 addEventListener、removeEventListener
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
// 重写 addEventListener、removeEventListener 方法
window.addEventListener = function (eventName, fn) {
    if (
        routingEventsListeningTo.includes(eventName) &&
        !capturedEventListeners[eventName].some((listener) => listener == fn)
    ) {
        capturedEventListeners[eventName].push(fn);
        return;
    }
    return originalAddEventListener.apply(this, arguments);
};
window.removeEventListener = function (eventName, listenerFn) {
    if (routingEventsListeningTo.includes(eventName)) {
        capturedEventListeners[eventName] = capturedEventListeners[
            eventName
        ].filter((fn) => fn !== listenerFn);
        return;
    }
    return originalRemoveEventListener.apply(this, arguments);
};

function patchedUpdateState(updateState) {
    return function () {
        const urlBefore = window.location.href;
        const result = updateState.apply(this); // 调用切换路由方法
        const urlAfter = window.location.href;
        // 路由变化,重新加载应用
        if (urlBefore !== urlAfter) urlReroute();
        return result;
    };
}
// history路由,hash 变化时不会触发 popstate
// 重写 pushState 和 repalceState 方法,让 hash 变化 history 可以触发 popstate
window.history.pushState = patchedUpdateState(window.history.pushState);
window.history.replaceState = patchedUpdateState(window.history.replaceState);

lifecycles/bootstrap.js

import {
  BOOTSTRAPPING,
  NOT_MOUNTED,
  NOT_BOOTSTRAPPED,
} from "../applications/app.helpers.js";

export async function toBootstrapPromise(app) {
  // 说明还没有启动
  if (app.status !== NOT_BOOTSTRAPPED) return app;

  app.status = BOOTSTRAPPING; // 加载中
  await app.bootstrap(app.customProps); // 加载
  app.status = NOT_MOUNTED; // 加载完毕

  return app;
}

lifecycles/load.js

import {
  LOADING_SOURCE_CODE,
  NOT_BOOTSTRAPPED,
  NOT_LOADED,
} from "../applications/app.helpers";

// 将 promise[] 通过 then 链转成一个 promise(类似函数扁平化,koa、redux 都是这种写法)
function flattenFnArray(fns) {
  // 将函数通过 then 链连接起来
  fns = Array.isArray(fns) ? fns : [fns];
  return function (props) {
    return fns.reduce((p, fn) => p.then(() => fn(props)), Promise.resolve());
  };
}

/**
 * 加载 app 实例
 * @param {*} app
 * @returns
 */
export async function toLoadPromise(app) {
  // 给 app 加载时做一个缓存处理,当 loadPromise 有值,说明已经加载过了(避免重复加载)
  if (app.loadPromise) return app.loadPromise; // 缓存机制

  // 如果资源加载过,直接 return
  if (app.status !== NOT_LOADED) return app;

  app.status = LOADING_SOURCE_CODE; // 加载资源
  return (app.loadPromise = Promise.resolve().then(async () => {
    // 调用 load 函数拿到接入协议
    let { bootstrap, mount, unmount } = await app.loadApp(app.customProps); 

    app.status = NOT_BOOTSTRAPPED;
    app.bootstrap = flattenFnArray(bootstrap);
    app.mount = flattenFnArray(mount);
    app.unmount = flattenFnArray(unmount);
    delete app.loadPromise;

    return app;
  }));
}

lifecycles/mount.js

import { MOUNTED, MOUNTING, NOT_MOUNTED } from "../applications/app.helpers.js";

export async function toMountPromise(app) {
  if (app.status !== NOT_MOUNTED) return app;

  app.status = MOUNTING; // 挂载中
  await app.mount(); // 挂载
  app.status = MOUNTED; // 挂载完毕

  return app;
}

lifecycles/unmount.js

import { UNMOUNTING, NOT_MOUNTED, MOUNTED } from "../applications/app.helpers";

export async function toUnmountPromise(app) {
  // 当前应用没有被挂载,什么都不用做
  if (app.status != MOUNTED) return app;

  app.status = UNMOUNTING; // 卸载中
  await app.unmount(app); // 卸载
  app.status = NOT_MOUNTED; // 卸载完毕

  return app;
}

代码附件下载

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 初始化开发环境
  2. 2. 应用的声明周期
  3. 3. 实现代码
    1. 3.1. index.html
    2. 3.2. single-spa.js
    3. 3.3. start.js
    4. 3.4. applications/app.js
    5. 3.5. applications/app.helpers.js
    6. 3.6. navigation/reroute.js
    7. 3.7. navigation/navigator-events.js
    8. 3.8. lifecycles/bootstrap.js
    9. 3.9. lifecycles/load.js
    10. 3.10. lifecycles/mount.js
    11. 3.11. lifecycles/unmount.js
  4. 4. 代码附件下载