navigation-events 路由监听
本节探讨 single 中路由管理部分路由的监听与代理的内容。我们知道,微应用需要根据一定的规则匹配到相应的路由,并根据路由去挂载和卸载微应用。从整体上来看,s-spa 需要完成这几件事情:路由监听、路由匹配、应用状态更新(mount 或者 unmount)。作为 s-spa 中独立的一部分,这部分具有一定的复杂度。
# 目录
从路由监听的角度来看我们不得不考虑以下的诸多问题:
- hash 路由和 history 路由
- url 路由变化和直接操作 history 导致 url 变化
# 路由监听
框架初始化时执行,监听 window 上 hashchange 和 popstate 事件,分别在 url hash 变化和 popstate 时触发;代码 history.pushState 和 history.replaceState,在两者执行时比较 url 是否变化。如果 url 变化将执行 reroute,调整应用匹配和应用状态更新。
if (isInBrowser) {
// 监听 hashchange 和 popstate 事件
// 这里不一定是原生的 addEventListener ,因为允许被代理
// 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;
// 代理 addEventListener 和 removeEventListener
window.addEventListener = function (eventName, fn) {
// 注意,以下代码在 addEventListener 时执行一次,在 listener 被回调时不会再执行
if (typeof fn === "function") {
// 如果是需要监听的路由事件,且未在 capturedEventListeners[eventName] 上注册
if (
routingEventsListeningTo.indexOf(eventName) >= 0 &&
!find(capturedEventListeners[eventName], (listener) => listener === fn)
) {
// 将 listener 注册到 capturedEventListeners
capturedEventListeners[eventName].push(fn);
// 注意:收集到 listeners 之后就返回了,由 s-spa 接管了 listeners 的调用
return;
}
}
return originalAddEventListener.apply(this, arguments);
};
window.removeEventListener = function (eventName, listenerFn) {
if (typeof listenerFn === "function") {
// 如果在 capturedEventListeners 中注册过此 listener,则将之删除
if (routingEventsListeningTo.indexOf(eventName) >= 0) {
capturedEventListeners[eventName] = capturedEventListeners[
eventName
].filter((fn) => fn !== listenerFn);
return;
}
}
return originalRemoveEventListener.apply(this, arguments);
};
// 代理 history.pushState 和 history.replaceState
// patchedUpdateState 需要比较 url 是否变化
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
// 如果此代码被执行了两次则出现异常
if (window.singleSpaNavigate) {
console.warn(
formatErrorMessage(
41,
__DEV__ &&
"single-spa has been loaded twice on the page. This can result in unexpected behavior."
)
);
} else {
/* For convenience in `onclick` attributes, we expose a global function for navigating to
* whatever an <a> tag's href is.
*/
// 便于调用 navigateToUrl 进行导航
window.singleSpaNavigate = navigateToUrl;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
这个函数的主要作用如下:
- 路由监听:hashchange、popstate、history.pushState 和 history.replaceState。
- 代理 window.addEventListener 和 window.removeEventListener 手机 hashchange 和 popstate 的 listener。
- 将 singleSpaNavigate 挂载到 window.singleSpaNavigate。
# urlReroute
由 hashchange 和 popstate 引起的 url 变化,执行 reroute。
function urlReroute() {
// url 已知变化,直接 reroute
reroute([], arguments);
}
2
3
4
# patchedUpdateState
由 history.pushState 和 history.replaceState 引起的 state 变化,先比较 url 是否变化,在执行 reroute。
function patchedUpdateState(updateState, methodName) {
// 从 history.pushState 和 replace.replaceState,需比对 url 是否变化
return function () {
const urlBefore = window.location.href;
// 调用原生函数
const result = updateState.apply(this, arguments);
const urlAfter = window.location.href;
// 只有不是 urlRerouteOnly,且 url 发生变化
if (!urlRerouteOnly || urlBefore !== urlAfter) {
if (isStarted()) {
// fire an artificial popstate event once single-spa is started,
// so that single-spa applications know about routing that
// occurs in a different application
// 通过事件系统仿造一个 popsState 事件,以触发 reroute,并且能够使所有微应用监听到变化
window.dispatchEvent(
createPopStateEvent(window.history.state, methodName)
);
} else {
// do not fire an artificial popstate event before single-spa is started,
// since no single-spa applications need to know about routing events
// outside of their own router.
// s-spa 还未 start,不需要以事件的形式进行通知,直接执行 reroute
// 注意:即使还没有 start,仍然需要 reroute,因为 reroute 会针对 start 情况做处理
reroute([]);
}
}
return result;
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
注意
如果框架已经 start,则需要以事件的方式引发 reroute,因为需要通知微应用路由的变化。这里使用 createPopStateEvent
仿造 popState 事件。
# navigateToUrl
Use this utility function to easily perform url navigation between registered applications without needing to deal with event.preventDefault(), pushState, triggerAppChange(), etc.
/**
* see https://single-spa.js.org/docs/api/#navigatetourl
* 不使用任何框架导航到目标 url,同时触发应用的更新 triggerAppChange
*/
export function navigateToUrl(obj) {
let url;
// obj 为 url
if (typeof obj === "string") {
url = obj;
} else if (this && this.href) {
// obj 为 a 标签
url = this.href;
} else if (
// object 为 ClickEvent
obj &&
obj.currentTarget &&
obj.currentTarget.href &&
obj.preventDefault
) {
url = obj.currentTarget.href;
obj.preventDefault();
} else {
throw Error(
// ......
);
}
// 将 currentUrl 和 url 分别生成 a 标签
const current = parseUri(window.location.href);
const destination = parseUri(url);
if (url.indexOf("#") === 0) {
// url 是 hash
window.location.hash = destination.hash;
} else if (current.host !== destination.host && destination.host) {
if (process.env.BABEL_ENV === "test") {
return { wouldHaveReloadedThePage: true };
} else {
// 改变了 host
window.location.href = url;
}
} else if (
destination.pathname === current.pathname &&
destination.search === current.search
) {
// pathname 和 search 没有变
window.location.hash = destination.hash;
} else {
// different path, host, or query params
window.history.pushState(null, null, url);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
navigateToUrl
方法不依赖于其他框架的路由组件,提供导航的功能,同时帮助我们触发 reroute。
# createPopStateEvent
createPopStateEvent
创建一个 popState 事件。在 history.pushState 和 history.replaceState 被调用且 url 发生变化时,主动发出一个 popState 事件,以使微应用可以监听到此事件而得知路由发生了变化。
function createPopStateEvent(state, originalMethodName) {
// https://github.com/single-spa/single-spa/issues/224 and https://github.com/single-spa/single-spa-angular/issues/49
// We need a popstate event even though the browser doesn't do one by default when you call replaceState, so that
// all the applications can reroute. We explicitly identify this extraneous event by setting singleSpa=true and
// singleSpaTrigger=<pushState|replaceState> on the event instance.
let evt;
try {
// IE 浏览器不支持 PopStateEvent(), see https://caniuse.com/mdn-api_popstateevent_popstateevent
evt = new PopStateEvent("popstate", { state });
} catch (err) {
// IE 11 compatibility https://github.com/single-spa/single-spa/issues/299
// https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-html5e/bd560f47-b349-4d2c-baa8-f1560fb489dd
evt = document.createEvent("PopStateEvent");
// nitializes the properties of a PopStateEvent object.Available only in IE10, IE11, and EdgeHTML Mode (All Versions).
evt.initPopStateEvent("popstate", false, false, state);
}
// 区分其他 popState 事件
evt.singleSpa = true;
evt.singleSpaTrigger = originalMethodName;
return evt;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在外界的应用中,同样可以通过监听 hashchange
和 popState
事件以得知 url 发生了变化,而不用额外封装 history.pushState
和 history.replaceState
。
参考: