single-spa中的路由实现
笔者今天为什么要介绍
single-spa
中的路由呢,在对QianKun
的源码分析中,笔者发现QianKun
未对路由进行实现,主要还是调用single-spa
中的路由。为了更好的了解微前端的机制,我们有必要对路由这块进行解读,在解读single-spa
实现方式之前,笔者先带大家了解下一些路由的前置知识。
Tips:本文中所涉及的single-spa
的版本为v5.9.3
路由的前置知识
我们先来聊一聊要实现一个路由主要需要做到哪几个部分呢?
一、当url变化的时候,保持UI界面的同步更新,即加载对应的内容
二、当浏览器刷新的时候,保持当前url地址,并且加载相应的内容
三、浏览器前进后退时,加载相应的内容
而在浏览器中,常见的路由主要分为两种:
- Browser Router
- Hash Router
Browser Router
该方式主要是由window
对象的history
对象提供的,主要是提供了对浏览器会话历史的访问,同时支持以下操作:
-
向前跳、向后跳以及跳转到会话历史的某个点
// 向前 window.history.back(); // 向 后 window.history.forward(); // 跳转到某个点,当前页面的相对位置为0 window.history.go(-1) // 查看浏览器会话历史的页数 window.history.length
-
添加会话历史
// 结合onpopstate可以获得state值 const state = { page: 1 } // 大多数浏览器忽略,目前被忽略 const title = '' // 跳转的地址 const url = '?page=1' history.pushState(state, title, url);
Tips:只会修改url不会去检查url是否存在以及加载页面
-
修改会话历史
// 这个和上面的添加历史会话使用相同,不同在于修改当前历史会话的url的信息 const state = { page: 2 } const title = '' const url = '?page=2' history.replaceState(state, title, url);
Tips:只会修改url不会去检查url是否存在以及加载页面
-
监听会话历史的变化
window.onpopstate = function(event) { alert("location: " + document.location + ", state: " + JSON.stringify(event.state)); } window.addEventListener("popstate", function(event) { alert("location: " + document.location + ", state: " + JSON.stringify(event.state)); });
Tips:不会在
history.pushState
和history.replaceState
触发
详情可以点击:History API
Hash Router
主要是根据location.hash
的变化来进行页面的更新,该路由的变化主要是通过监听hashchange
事件
window.addEventListener('hashchange', function (event) {
// ...
})
window.onhashchange = function (event) {
// ...
}
详情可以点击:Window: hashchange event
single-spa中的路由机制
经过👆的讲述,显然我们实现路由机制需要对路由进行监听,而single-spa
是支持Browser Router
以及Hash Router
两种路由方式的,即对popstate
和hashchange
进行监听
基本流程
以上流程其实做的事情很简单:
1、第一个步骤设置监听,执行urlReroute
是为了先让注册的微应用实现生命周期的转化,比如加载⇒启动、启动⇒挂载等
2、重写监听和移除函数,是为了收集微应用中路由监听的回调事件,存储起来在某一时刻再执行
3、重写pushState
和replaceState
主要是为了在路由变更的时候去触发微应用的加载
对popstate
和hashchange
进行监听
src/navigation/navigation-events.js
// 监听的路由事件数组
export const routingEventsListeningTo = ["hashchange", "popstate"];
// 记录路由回调
const capturedEventListeners = {
hashchange: [],
popstate: [],
}
// 重载路由事件
function urlReroute() {
reroute([], arguments);
}
if (isInBrowser) {
// 对hashchange以及popstate设置监听,这边回调的处理暂时不看
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
// 重写window.addEventListener以及window.removeEventListener方法,收集应用中路由变化的监听回调
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function (eventName, fn) {
// 记录回调函数到capturedEventListeners中
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);
};
// 对pushState以及replaceState进行重写
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
......
}
对pushState和replaceState的自定义处理
进入patchedUpdateState
function patchedUpdateState(updateState, methodName) {
return function () {
const urlBefore = window.location.href;
const result = updateState.apply(this, arguments);
const urlAfter = window.location.href;
if (!urlRerouteOnly || urlBefore !== urlAfter) {
// single-spa是否已经启动
if (isStarted()) {
// 触发PopState事件
window.dispatchEvent(
createPopStateEvent(window.history.state, methodName)
);
} else {
reroute([]);
}
}
return result;
};
}
function createPopStateEvent(state, originalMethodName) {
let evt;
try {
evt = new PopStateEvent("popstate", { state });
} catch (err) {
// 兼容处理
evt = document.createEvent("PopStateEvent");
evt.initPopStateEvent("popstate", false, false, state);
}
evt.singleSpa = true;
evt.singleSpaTrigger = originalMethodName;
return evt;
}
目的:使history.pushState
和history.replaceState
也会触发popstate
事件
Q1:为什么要对isStarted进行判断?
let started = false;
export function start(opts) {
started = true;
if (opts && opts.urlRerouteOnly) {
setUrlRerouteOnly(opts.urlRerouteOnly);
}
if (isInBrowser) {
reroute();
}
}
export function isStarted() {
return started;
}
官方文档中是这么说的:在调用 start
之前, 应用会被加载, 但不会初始化,挂载或卸载。 start
的原因是让你更好的控制你单页应用的性能。比如:你在注册应用之后,需要先去某些接口(比如获取登陆用户的信息),然后才是挂载相应的应用。
路由重载的回调reroute
- 对处于不同生命周期的微应用进行生命周期的变更
- 处理收集的
hashchange
和popstate
的回调事件 - 触发自定义事件
基本流程
- 关键点说明
-
appChangeUnderway:是否有应用由于**
reroute
生命周期处于变更阶段,当存在时后续的reroute
**将会返回promise,并且把参数存入peopleWaitingOnAppChange
数组中进行等待 -
getAppChanges:根据app.status和当前路由把所以应用划分为四个数组
- appsToUnload:处于
NOT_BOOTSTRAPPED
和NOT_MOUNTED
的状态,并且和当前URL不匹配的应用 - appsToUnmount:处于
MOUNTED
阶段并且和当前URL不匹配的应用 - appsToLoad:处于
NOT_LOADED
或者是LOADING_SOURCE_CODE
的资源,并且和当前URL匹配的应用 - appsToMount:处于
NOT_BOOTSTRAPPED
和NOT_MOUNTED
- appsToUnload:处于
-