npm init -y
npm install rollup rollup-plugin-serve
import serve from "rollup-plugin-serve";
export default {
input: "./src/single-spa.js",
output: {
file: "./lib/umd/single-spa.js",
format: "umd",
name: "singleSpa",
sourcemap: true,
},
plugins: [
serve({
openPage: "/index.html",
contentBase: "",
port: 3000,
}),
],
};
"scripts": {
"dev": "rollup -c -w"
},
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>
<script src="/lib/umd/single-spa.js">script>
<script>
singleSpa.registerApplication(
"app1",
async () => {
return {
bootstrap: async () => {},
mount: async () => {},
unmount: async () => {},
};
},
(location) => location.hash.startsWith("#/app1"),
{ store: { name: "yehuozhili" } }
);
singleSpa.start();
script>
body>
html>
export { registerApplication } from "./applications/app";
export { start } from "./start";
export function start() {}
const app=[]//用来存放所有应用
/**
*
*
* @export
* @param {*} appName 应用名
* @param {*} loadApp 加载的应用
* @param {*} activeWhen 激活时会调用loadApp
* @param {*} customProps 自定义属性
*/
export function registerApplication(appName, loadApp, activeWhen, customProps) {
app.push({
name: appName,
loadApp,
activeWhen,
customProps,
status: NOT_LOADED,
});
}
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"; // 运行出错
export function isActive(app) {
// 当前app是否已经挂载
return app.status === MOUNTED;
}
export function shouldBeActive(app) {
// 当前app是否应该激活
return app.activeWhen(window.location);
}
import { started } from "../start";
export function reroute() {
if (started) {
console.log("调用start");
} else {
console.log("调用register");
}
}
import { reroute } from "./navigations/reroute";
export let started = false;
export function start() {
started = true;
//挂载应用
reroute(); //除了去加载应用还需要挂载应用
}
export function getAppChanges() {
const appsToUnmount = [];
const appsToLoad = [];
const appstoMount = [];
app.forEach((v) => {
const appShouldBeActive = shouldBeActive(v);
switch (v.status) {
case NOT_LOADED:
case LOADING_SOURCE_CODE:
if (appShouldBeActive) {
appsToLoad.push(v);
}
break;
case NOT_BOOTSTRAPPED:
case BOOTSTRAPPING:
case NOT_MOUNTED:
if (appShouldBeActive) {
appstoMount.push(v);
}
break;
case MOUNTED:
if (!appShouldBeActive) {
appsToUnmount.push(v);
}
break;
default:
break;
}
});
return {
appsToUnmount,
appsToLoad,
appstoMount,
};
}
import { started } from "../start";
import { getAppChanges } from "../applications/app";
export function reroute() {
const { appsToUnmount, appsToLoad, appstoMount } = getAppChanges();
console.log(appsToUnmount, appsToLoad, appstoMount);
if (started) {
return performAppChanges(); //根据路径装载
} else {
return loadApps(); //预先加载
}
async function loadApps() {}
async function performAppChanges() {}
}
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>
<script src="/lib/umd/single-spa.js">script>
<script>
singleSpa.registerApplication(
"app1",
async (props) => {
console.log("加载执行");
return {
bootstrap: [
async (props) => {
console.log("b1");
},
async (props) => {
console.log("b1");
},
async (props) => {
console.log("b1");
},
],
mount: async (props) => {
console.log("mount");
},
unmount: async (props) => {
console.log("unmount");
},
};
},
(location) => location.hash.startsWith("#/app1"),
{ store: { name: "yehuozhili" } }
);
singleSpa.registerApplication(
"app2",
async (props) => {
console.log("加载执行2");
return {
bootstrap: [
async (props) => {
console.log("b2");
},
async (props) => {
console.log("b2");
},
async (props) => {
console.log("b2");
},
],
mount: async (props) => {
console.log("mount2");
},
unmount: async (props) => {
console.log("unmount2");
},
};
},
(location) => location.hash.startsWith("#/app2"),
{ store: { name: "yehuozhili2" } }
);
singleSpa.start();
script>
body>
html>
import {
NOT_BOOTSTRAPPED,
BOOTSTRAPPING,
NOT_MOUNTED,
} from "../applications/app.helper";
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;
}
load.js
import {
LOADING_SOURCE_CODE,
NOT_BOOTSTRAPPED,
} from "../applications/app.helper";
function flattenFnArray(fns) {
fns = Array.isArray(fns) ? fns : [fns];
return function (props) {
return fns.reduce(
(p, fn) => p.then(() => fn(props)),
Promise.resolve()
);
};
}
export async function toLoadPromise(app) {
if (app.loadPromise) {
return app.loadPromise;
}
return (app.loadPromise = Promise.resolve().then(async () => {
app.status = LOADING_SOURCE_CODE;
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;
}));
}
mount.js
import { NOT_MOUNTED, MOUNTING, MOUNTED } from "../applications/app.helper";
export async function toMountPromise(app) {
if (app.status !== NOT_MOUNTED) {
return app;
}
app.status = MOUNTING;
await app.mount(app.customProps);
app.status = MOUNTED;
return app;
}
unmount.js
import { MOUNTED, UNMOUNTING, NOT_MOUNTED } from "../applications/app.helper";
export async function toUnmountPromise(app) {
if (app.status !== MOUNTED) {
return app;
}
app.status = UNMOUNTING;
await app.unmount(app.customProps);
app.status = NOT_MOUNTED;
return app;
}
import { started } from "../start";
import { getAppChanges } from "../applications/app";
import { toLoadPromise } from "../lifecycles/load";
import { toBootStrapPromise } from "../lifecycles/bootstrap";
import { toMountPromise } from "../lifecycles/mount";
import { toUnmountPromise } from "../lifecycles/unmount";
export function reroute() {
const { appsToUnmount, appsToLoad, appstoMount } = getAppChanges();
if (started) {
return performAppChanges(); //根据路径装载
} else {
return loadApps(); //预先加载
}
async function loadApps() {
let apps = await Promise.all(appsToLoad.map(toLoadPromise)); //获取3方法放到app上
}
async function performAppChanges() {
//卸载不需要应用,加载需要应用
let unmount = appsToUnmount.map(toUnmountPromise);
appsToLoad.map(async (app) => {
app = await toLoadPromise(app);
app = await toBootStrapPromise(app);
return toMountPromise(app);
});
appstoMount.map(async (app) => {
app = await toBootStrapPromise(app);
return toMountPromise(app);
});
}
}
navigator.js
import { reroute } from "./reroute";
const captureEventListener = {
hashchange: [],
popstate: [],
};
export const routingEventsListeningTo = ["hashchange", "popstate"];
function urlReroute() {
reroute([], arguments);
}
//挂应用逻辑
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
//应用切换后还需要处理原来的方法,需要在应用切换后再执行。
const originalAddEventListenter = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
//改写监听方法,把要执行的存起来。
window.addEventListener = function (eventName, fn) {
if (
routingEventsListeningTo.indexOf(eventName) >= 0 &&
!captureEventListener[eventName].some((listener) => listener == fn)//看重复
) {
captureEventListener[eventName].push(fn);
return;
}
return originalAddEventListenter.apply(this, arguments);
};
window.removeEventListener = function (eventName, fn) {
if (routingEventsListeningTo.indexOf(eventName) >= 0) {
captureEventListener[eventName] = captureEventListener[
eventName
].filter((l) => l !== fn);
return;
}
return originalRemoveEventListener.apply(this, arguments);
};
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>
<a href="#/app1">
应用1
a>
<a href="#/app2">
应用2
a href="">
<script src="/lib/umd/single-spa.js">script>
<script>
singleSpa.registerApplication(
"app1",
async (props) => {
console.log("加载执行");
return {
bootstrap: [
async (props) => {
console.log("b1");
},
async (props) => {
console.log("b1");
},
async (props) => {
console.log("b1");
},
],
mount: async (props) => {
console.log("mount1");
},
unmount: async (props) => {
console.log("unmount1");
},
};
},
(location) => location.hash.startsWith("#/app1"),
{ store: { name: "yehuozhili" } }
);
singleSpa.registerApplication(
"app2",
async (props) => {
console.log("加载执行2");
return {
bootstrap: [
async (props) => {
console.log("b2");
},
async (props) => {
console.log("b2");
},
async (props) => {
console.log("b2");
},
],
mount: async (props) => {
console.log("mount2");
},
unmount: async (props) => {
console.log("unmount2");
},
};
},
(location) => location.hash.startsWith("#/app2"),
{ store: { name: "yehuozhili2" } }
);
singleSpa.start();
script>
body>
html>
//浏览器路由改写 如果切换不会触发popstate
function patchedUpdateState(updateState, methodName) {
return function () {
const urlBefore = window.location.href;
updateState.apply(this, arguments); //调用切换
const urlAfter = window.location.href;
if (urlBefore !== urlAfter) {
//重新加载应用,传入事件源
urlReroute(new PopStateEvent("popstate"));
}
};
}
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>
<script>
function a(){
history.pushState({},'','/app3')
}
function b(){
history.pushState({},'','/app4')
}
script>
<a href="#/app1">
应用1
a>
<a href="#/app2">
应用2
a href="">
<button onclick="a()">应用3button>
<button onclick="b()">应用4button>
<script src="/lib/umd/single-spa.js">script>
<script>
singleSpa.registerApplication(
"app1",
async (props) => {
console.log("加载执行");
return {
bootstrap: [
async (props) => {
console.log("b1");
},
async (props) => {
console.log("b1");
},
async (props) => {
console.log("b1");
},
],
mount: async (props) => {
console.log("mount1");
},
unmount: async (props) => {
console.log("unmount1");
},
};
},
(location) => location.hash.startsWith("#/app1"),
{ store: { name: "yehuozhili" } }
);
singleSpa.registerApplication(
"app2",
async (props) => {
console.log("加载执行2");
return {
bootstrap: [
async (props) => {
console.log("b2");
},
async (props) => {
console.log("b2");
},
async (props) => {
console.log("b2");
},
],
mount: async (props) => {
console.log("mount2");
},
unmount: async (props) => {
console.log("unmount2");
},
};
},
(location) => location.hash.startsWith("#/app2"),
{ store: { name: "yehuozhili2" } }
);
singleSpa.registerApplication(
"app3",
async (props) => {
console.log("加载执行3");
return {
bootstrap: [
async (props) => {
console.log("b3");
},
async (props) => {
console.log("b3");
},
async (props) => {
console.log("b3");
},
],
mount: async (props) => {
console.log("mount3");
},
unmount: async (props) => {
console.log("unmount3");
},
};
},
(location) => location.pathname.startsWith("/app3"),
{ store: { name: "yehuozhili3" } }
);
singleSpa.registerApplication(
"app4",
async (props) => {
console.log("加载执行4");
return {
bootstrap: [
async (props) => {
console.log("b4");
},
async (props) => {
console.log("b4");
},
async (props) => {
console.log("b4");
},
],
mount: async (props) => {
console.log("mount4");
},
unmount: async (props) => {
console.log("unmount4");
},
};
},
(location) => location.pathname.startsWith("/app4"),
{ store: { name: "yehuozhili4" } }
);
singleSpa.start();
script>
body>
html>