初始化开发环境
- 项目目录
- 初始化配置安装 rollup
npm init -y npm install rollup rollup-plugin-serve
- 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);
}
navigation/reroute.js
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); // 挂载
});
}
}
navigation/navigator-events.js
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;
}
代码附件下载
打赏作者
您的打赏是我前进的动力
微信
支付宝
js 沙箱机制
上一篇
评论