qiankun
是一个基于 single-spa
的微前端实现库,旨在帮助大家能更简单、无痛 的构建一个生产可用 微前端架构系统。
本文宗旨是帮大家理解 qiankun
的原理,从而更好的使用 qiankun
。
single-spa
single-spa
可以把他看作状态管理机 ,他可以根据规则 匹配路由,启动对应的应用,并且对应的应用还有生命周期bootstrap, mount, unmount、update
。
使用如下:
js
// single-spa-config.js
import { registerApplication, start } from "single-spa";
// 注册 app2
registerApplication(
"app2", // APP 名称 需要保证唯一
() => import("src/app2/main.js"), // 入口文件
(location) => location.pathname.startsWith("/app2"), // 规则
{ some: "value" },
);
// 注册 app1
registerApplication({
name: "app1",
app: () => import("src/app1/main.js"),
activeWhen: "/app1", // 规则
customProps: {
some: "value",
},
});
start();
registerApplication
入参数app
是需要返回3个状态如下:
js
const application = {
bootstrap: () => Promise.resolve(), //bootstrap function 初始化 loading
mount: () => Promise.resolve(), //mount function // 渲染
unmount: () => Promise.resolve(), //unmount function // 卸载
};
registerApplication("applicationName", application, '/app1');
// 在或者可以是多个数组形式
const application = {
bootstrap: [() => Promise.resolve(), () => Promise.resolve()], //bootstrap function 初始化 loading
mount: [() => Promise.resolve(), () => Promise.resolve()], //mount function // 渲染
unmount: [() => Promise.resolve(), () => Promise.resolve()], //unmount function // 卸载
};
registerApplication("applicationName", application, '/app1');
-
registerApplication
入参数activeWhen
规则匹配如下: -
'/app1'
-
'/users/:userId/profile'
-
'/pathname/#/hash'
-
'/pathname/#/hash', '/app1'
匹配路由的相关原理代码如下:
js
function sanitizeActiveWhen(activeWhen) {
//1. 确保 activeWhen 是数组
let activeWhenArray = Array.isArray(activeWhen) ? activeWhen : [activeWhen];
// 2. 如果本身你 `activeWhen` 是函数就直接用你的函数返回值规则判断
activeWhenArray = activeWhenArray.map((activeWhenOrPath) =>
typeof activeWhenOrPath === "function"
? activeWhenOrPath
: pathToActiveWhen(activeWhenOrPath) // 3. 如果是字符串就转成函数,`pathToActiveWhen`返回了一个函数
);
// 6. 调用函数即可判断是否匹配到路由
return (location) =>
activeWhenArray.some((activeWhen) => activeWhen(location));
}
export function pathToActiveWhen(path, exactMatch) {
// toDynamicPathValidatorRegex 方法先略过,可以理解是根据字符串生产正则表达式的
const regex = toDynamicPathValidatorRegex(path, exactMatch);
return (location) => {
// compatible with IE10
let origin = location.origin;
if (!origin) {
origin = `${location.protocol}//${location.host}`;
}
// 4. 去掉域名、以及参数只保留路由 例如:https://www.baidu.com/s?wd=123&ie=utf-8 --> /s
const route = location.href
.replace(origin, "")
.replace(location.search, "")
.split("?")[0];
return regex.test(route); // 5. 判断路由是否匹配规则
};
}
生成正则的toDynamicPathValidatorRegex
表达式方法如下:
js
function toDynamicPathValidatorRegex(path, exactMatch) {
let lastIndex = 0,
inDynamic = false,
regexStr = "^";
if (path[0] !== "/") {
path = "/" + path;
}
for (let charIndex = 0; charIndex < path.length; charIndex++) {
const char = path[charIndex];
const startOfDynamic = !inDynamic && char === ":";
const endOfDynamic = inDynamic && char === "/";
if (startOfDynamic || endOfDynamic) {
appendToRegex(charIndex);
}
}
appendToRegex(path.length);
return new RegExp(regexStr, "i");
function appendToRegex(index) {
const anyCharMaybeTrailingSlashRegex = "[^/]+/?";
const commonStringSubPath = escapeStrRegex(path.slice(lastIndex, index));
regexStr += inDynamic
? anyCharMaybeTrailingSlashRegex
: commonStringSubPath;
if (index === path.length) {
if (inDynamic) {
if (exactMatch) {
// Ensure exact match paths that end in a dynamic portion don't match
// urls with characters after a slash after the dynamic portion.
regexStr += "$";
}
} else {
// For exact matches, expect no more characters. Otherwise, allow
// any characters.
const suffix = exactMatch ? "" : ".*";
regexStr =
// use charAt instead as we could not use es6 method endsWith
regexStr.charAt(regexStr.length - 1) === "/"
? `${regexStr}${suffix}$`
: `${regexStr}(/${suffix})?(#.*)?$`;
}
}
inDynamic = !inDynamic;
lastIndex = index;
}
function escapeStrRegex(str) {
// borrowed from https://github.com/sindresorhus/escape-string-regexp/blob/master/index.js
return str.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
}
}
toDynamicPathValidatorRegex
方法可以直接在控制台运行去调试如下:

其实咱们一个微应用尽量是一个前缀开头的比如/app*、/search*
这么用的话也比较规范。
以下是single-spa
的路由监听相关的处理
js
export function patchHistoryApi(opts) {
if (historyApiIsPatched) {
throw Error(
formatErrorMessage(
43,
__DEV__ &&
`single-spa: patchHistoryApi() was called after the history api was already patched.`
)
);
}
// True by default, as a performance optimization that reduces
// the number of extraneous popstate events
urlRerouteOnly =
opts && opts.hasOwnProperty("urlRerouteOnly") ? opts.urlRerouteOnly : true;
historyApiIsPatched = true;
originalReplaceState = window.history.replaceState;
// We will trigger an app change for any routing events.
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
// Monkeypatch addEventListener so that we can ensure correct timing
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function (eventName, fn) {
if (typeof fn === "function") {
if (
routingEventsListeningTo.indexOf(eventName) >= 0 &&
!find(capturedEventListeners[eventName], (listener) => listener === fn)
) {
capturedEventListeners[eventName].push(fn);
return;
}
}
return originalAddEventListener.apply(this, arguments);
};
window.removeEventListener = function (eventName, listenerFn) {
if (typeof listenerFn === "function") {
if (routingEventsListeningTo.indexOf(eventName) >= 0) {
capturedEventListeners[eventName] = capturedEventListeners[
eventName
].filter((fn) => fn !== listenerFn);
}
}
return originalRemoveEventListener.apply(this, arguments);
};
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
originalReplaceState,
"replaceState"
);
}
运转流程图如下:

single-spa 总结
可以从分享中看到single-spa
并没有处理你的应用要怎么挂载到界面的问题(都是执行了对应的mount
),你可以通过customProps
传进来一个container
来指定挂载到那个位置下。
Q: 那么对于vue、react
框架来说应该怎么让single-spa
去loadApp
呢?
A: 需要把vue、react
整体通过webpack
打包成一个lib
并且导出对应的生命周期就行了。
比如:
js
// 加载子应用的 js 脚本
async function loadScript(url) {
return new Promise((resole, reject) => {
let script = document.createElement("script");
script.src = url;
script.onload = resole; // 加载成功
script.onerror = reject; //加载失败
document.head.appendChild(script); //把script放在html的head标签里
});
}
// 注册应用
registerApplication(
"app1",
async () => {
// 动态创建script标签 把这个模块引入进来
await loadScript("./app1/bundle.js");
return (window as any).singleApp1; //bootstrap mount unmount
},
'/app1',
{
container: "#single-spa-layout",
}
);
// 开启应用
start();
Q: 如果single-spa
没有匹配到路由会怎么样?
A: 需要你自己处理,比如:新注册一个404的微应用
js
// 注册应用
registerApplication(
"app404",
async () => {
// 动态创建script标签 把这个模块引入进来
await loadScript("./app404/bundle.js");
return (window as any).singleApp1; //bootstrap mount unmount
},
() => {
return true;
},
{
container: "#single-spa-layout",
}
);
第二种:
js
// 通过single-spa 事件监听的方式处理
window.addEventListener('single-spa:no-app-change', () => {
const mountedApps = singleSpa.getMountedApps();
if (!mountedApps.length) {
singleSpa.navigateToUrl('/default-route'); // 导航到默认路由
}
});
Q:对比qiankun
他们有什么区别?
A: qiankun
是基于single-spa
开发的,它提供了对框架更加便捷的挂载引入的方式,让你接入微应用像使用 iframe 一样简单。并且它内部做了css、js
沙箱隔离,可以预加载子应用的等优化。