【微前端】微前端笔记(三)

前言

  • 本篇手写简易single-spa

流程

  • 首先初始化项目
npm init -y
npm install rollup rollup-plugin-serve
  • 编写rollup.config.js
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:
	"scripts": {
		"dev": "rollup -c -w"
	},
  • 添加index.html,写上singlespa的基本用法:

<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>
  • 新建src 与single-spa.js,开始编写代码,入口处导入:
export { registerApplication } from "./applications/app";
export { start } from "./start";
  • 同时建立这2文件。
  • start.js:
export function start() {}
  • app.js
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,
	});
}
  • app.js就把配置项导入全局的app数组。activeWhen就是到时候激活执行的那个startwith
  • 一个应用有多种状态:
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);
}

  • 下面对app.push后需要有个方法reroute,当注册应用时reroute的功能是加载子应用,当调用start方法时是挂载应用,新建navigations文件夹并新建reroute.js文件:
import { started } from "../start";

export function reroute() {
	if (started) {
		console.log("调用start");
	} else {
		console.log("调用register");
	}
}
  • start中设定全局变量调用它
import { reroute } from "./navigations/reroute";

export let started = false;
export function start() {
	started = true;
	//挂载应用
	reroute(); //除了去加载应用还需要挂载应用
}
  • 下面就需要让加载对应路由时拿到对应状态的app:
  • 制作getAppChanges函数:
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,
	};
}
  • 在reroute里可以拿到它:
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>
  • lifecircle文件夹下编写对app生命周期的处理:
    bootstrap:
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;
}
  • reroute中进行处理,调用start后,先卸载应用,再去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);
		});
	}
}
  • 这里都是对状态的判断,并没有什么特别难的地方。
  • 打开页面验证,如果访问http://localhost:3000/#/app1 或者#/app2 能弹出对应console.log即为成功。
  • 目前这个方式有个缺点,就是必须要刷新才能加载对应页面。所以需要对其进行路由劫持。

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>
  • 当应用正常切换卸载即ok。
  • 在history路由也需要改写:
//浏览器路由改写 如果切换不会触发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>
  • 这样就完成了路由切换

总结

  • 实际主要就是换window,以及改写路由事件,切换加载,并没啥特别难懂的东西。思路上主要是通过约定每个应用的启动卸载和显示条件,做成lib,父应用去调用进行加载。
  • 需要代码自取https://github.com/yehuozhili/learnsinglespa

你可能感兴趣的:(微前端)