前言
本文仅从
history路由的情况进行分析,并未分析hash路由。
之前由于工作原因,接触到单页面应用中history路由监听的实现。了解了Vue,React等前端框架对应的路由库的原理,发现在web场景下,路由工具中,很多是基于history去进行封装的。
vue-router中的跳转,其中的routerHistory就是我们在不同场景注入的对象。
react-router中的跳转
那我们下面就来探索history到底提供的api,又做了啥,以及我们如何去实现单页面应用的history路由监听。
history 中的 API
push
history库中的push实质上,底层调用的还是window.history.pushState,同时,自己封装了一些工具函数,可以更方便的得到下一次的路由状态,以及做一些调用路由监听的回调的操作。
export function createBrowserHistory(
options: BrowserHistoryOptions = {}
): BrowserHistory {
// ... code
function push(to: To, state?: any) {
let nextAction = Action.Push;
let nextLocation = getNextLocation(to, state);
function retry() {
push(to, state);
}
if (allowTx(nextAction, nextLocation, retry)) {
let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
// TODO: Support forced reloading
// try...catch because iOS limits us to 100 pushState calls :/
try {
// 实质上的globalHistory就是window上的history对象, 实质调用了pushState的方法。
globalHistory.pushState(historyState, "", url);
} catch (error) {
// They are going to lose state here, but there is no real
// way to warn them about it since the page will refresh...
window.location.assign(url);
}
applyTx(nextAction);
}
}
return {
// other api,
push,
}
}
replace
replace的操作和push操作也基本一样,只不过调用的是window.history.replace。
export function createBrowserHistory(
options: BrowserHistoryOptions = {}
): BrowserHistory {
// ... code
function replace(to: To, state?: any) {
let nextAction = Action.Replace;
let nextLocation = getNextLocation(to, state);
function retry() {
replace(to, state);
}
if (allowTx(nextAction, nextLocation, retry)) {
let [historyState, url] = getHistoryStateAndUrl(nextLocation, index);
// TODO: Support forced reloading
globalHistory.replaceState(historyState, "", url);
applyTx(nextAction);
}
}
return {
// other api,
replace,
}
}
listen
listen是实现监听的方法,我们着重看一下listen的实现吧。
函数签名
let history = {
listen(listener: Listener) {
return listeners.push(listener);
}
}
listeners对象
let listeners = createEvents<Listener>();
function createEvents<F extends Function>(): Events<F> {
let handlers: F[] = [];
return {
get length() {
return handlers.length;
},
push(fn: F) {
handlers.push(fn);
return function () {
handlers = handlers.filter((handler) => handler !== fn);
};
},
call(arg) {
handlers.forEach((fn) => fn && fn(arg));
},
};
}
上方代码我们可以得到以下两个信息
push的时候会新存入一个处理函数fn。call的时候会调用所有以存入的函数(代码中使用handlers进行存储)。
何时调用
这里,其实我们只需要看listeners何时调用call即可。发现在applyTx函数中调用。
function applyTx(nextAction: Action) {
action = nextAction;
[index, location] = getIndexAndLocation();
listeners.call({ action, location });
}
我们具体看看何时进行了,发现其实在push或replace的时候使用了该函数,并且是在globalHistory调用pushState或repalceState之后调用的。
function push(to: To, state?: any) {
// code
if (allowTx(nextAction, nextLocation, retry)) {
let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
// TODO: Support forced reloading
// try...catch because iOS limits us to 100 pushState calls :/
try {
globalHistory.pushState(historyState, "", url);
} catch (error) {
// They are going to lose state here, but there is no real
// way to warn them about it since the page will refresh...
window.location.assign(url);
}
applyTx(nextAction);
}
}
history模式下的路由监听
上面我们都分析了vue-router和react-router在history模式下,实际底层都是调用了window.history的replaceState/pushState。
所以,我们只要监听replaceState/pushState的事件,并执行我们自定义回调,就能够实现我们要的效果了。
参考了社区的方案,大多都是如下的思路。(较为hack)
- 重写
window.history的replaceState/pushState方法。 - 在重写的方法中执行回调。
- 如果要监听游览器的前进/后退事件,还需要监听
popstate事件。
重写replaceState/pushState方法
enum HistoryEventType {
pushState = 'pushState',
replaceState = 'replaceState'
}
export const rewriteHistoryPushState = (type: HistoryEventType, callback: () => void) => {
const prevHistoryFn = window.history[type];
window.history[type] = (...args: any[]) => {
const ret = prevHistoryFn.apply(window.history, args as any);
callback();
return ret;
}
}
重写的方法中执行回调
rewriteHistoryChangeStateFn(HistoryEventType.pushState, () => {
console.log('custom handler');
});
监听popstate事件
window.addEventListener('popstate', () => {
console.log('handler');
});
上述基本就可以实现一个简单的单页面history路由监听。
链接:codesandbox.io/s/condescen…
效果图: