single-spa 源码解析
single-spa 源码解析
single-spa是一种微前端的实现方案。阿里的qiankun其实是基于这个项目做了二次开发,其实是做了个拓展,提供了html解析与js沙盒两个功能。本文从single-spa的代码实现角度解析一下它的实现原理。
前提假设
single-spa首先要求每个子应用需要提供bootstrap,mount,unmount,update这四个方法,这样每个子应用从single-spa的角度而言,就是一个具有四个方法的对象。
实现的大体原理
single-spa是基于路由来决定每个子应用的加载与卸载。所以,它是通过拦截与监听浏览器的路由事件做到的。具体的可以分为以下两个方面。
- 监听浏览器的popstate,hashchange这两个事件,同时它拦截了其他应用对于这两个事件的监听。
- 它也对于改变路由的两个API也进行了更换window.history.pushState(),window.history.replaceState()。这样,路由变化的事件被single-spa接管,改变路由的API也被接管。所以,整个路由都被single-spa接管。这个是single-spa工作的核心。下面是源码的片段
export const routingEventsListeningTo = ["hashchange", "popstate"];
// 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;
}
}
return originalRemoveEventListener.apply(this, arguments);
};
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
工作的核心
single-spa在接管路由后,当路由发生变化,其实也就是popstate,hashchange这两个事件发生的时候。single-spa会去计算更新每个app的状态。代码里面叫reroute()
。这个方法的逻辑就是计算每个app的状态,然后,首先卸载inactive的app.接着挂载需要激活的app.同时会发布一些生命周期的事件以及原始的popstate,hashchange事件。下面是最主要的代码片段。忽略了部分细节
function performAppChanges() {
return Promise.resolve().then(() => {
// https://github.com/single-spa/single-spa/issues/545
window.dispatchEvent(
new CustomEvent(
appsThatChanged.length === 0
? "single-spa:before-no-app-change"
: "single-spa:before-app-change",
getCustomEventDetail(true)
)
);
window.dispatchEvent(
new CustomEvent(
"single-spa:before-routing-event",
getCustomEventDetail(true, { cancelNavigation })
)
);
// 在这个时间点,我们是有可能取消这个事件的
if (navigationIsCanceled) {
window.dispatchEvent(
new CustomEvent(
"single-spa:before-mount-routing-event",
getCustomEventDetail(true)
)
);
finishUpAndReturn();
navigateToUrl(oldUrl);
return;
}
// 首先卸载不用的app,其实就调用app.unload 方法
const unloadPromises = appsToUnload.map(toUnloadPromise);
// 摘出不用的app, 调用 app.unmount方法
const unmountUnloadPromises = appsToUnmount
.map(toUnmountPromise)
.map((unmountPromise) => unmountPromise.then(toUnloadPromise));
const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
const unmountAllPromise = Promise.all(allUnmountPromises);
// 完成了不用的app 的摘除与卸载, 发布对应的事件
unmountAllPromise.then(() => {
window.dispatchEvent(
new CustomEvent(
"single-spa:before-mount-routing-event",
getCustomEventDetail(true)
)
);
});
//加载使用的app,加载完后调用app.bootstrap(),这些都是异步的
/* We load and bootstrap apps while other apps are unmounting, but we
* wait to mount the app until all apps are finishing unmounting
*/
const loadThenMountPromises = appsToLoad.map((app) => {
return toLoadPromise(app).then((app) =>
tryToBootstrapAndMount(app, unmountAllPromise)
);
});
// bootstrap 并且mount 应该active的app
/* These are the apps that are already bootstrapped and just need
* to be mounted. They each wait for all unmounting apps to finish up
* before they mount.
*/
const mountPromises = appsToMount
.filter((appToMount) => appsToLoad.indexOf(appToMount) < 0)
.map((appToMount) => {
return tryToBootstrapAndMount(appToMount, unmountAllPromise);
});
// 当不用的应用卸载后,我们会通知所有监听popstate,hashchange的监听者。
// 当所有的应用加载后,我们会发布single-spa:app-change, single-spa:routing-event,
return unmountAllPromise
.catch((err) => {
callAllEventListeners();
throw err;
})
.then(() => {
/* Now that the apps that needed to be unmounted are unmounted, their DOM navigation
* events (like hashchange or popstate) should have been cleaned up. So it's safe
* to let the remaining captured event listeners to handle about the DOM event.
*/
callAllEventListeners();
return Promise.all(loadThenMountPromises.concat(mountPromises))
.catch((err) => {
pendingPromises.forEach((promise) => promise.reject(err));
throw err;
})
.then(finishUpAndReturn);
});
});
}
上面就是single-spa工作的最核心的原理。
上面的代码就是针对registerApplicaiton(),start()以后的single-spa的工作流程。是不是非常简单。不过这个思想确实非常的新颖。不过这个里面有个问题,就是它要求所有的应用自己抽象成那四个方法。而且所有的应用都在同一个js的上下文中执行,所有很容易会出现互相打架的情况。qiankun 提供的js沙盒,就是为了这个准备的。对应用之间做了隔离。