探究对VueRouter的Hash模式进行外部监听(未实现)
VueRouter的hash模式的普遍印象是监听hashchange事件从而改变页面显示的组件,然而在真实场景下会出现hashchange事件无效的情况,本文据此展来了一系列实践操作。
搭建Demo
通过serve开启一个本地服务器对外暴露一个html文件可以快速搭建Demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hash路由问题</title>
<script src="http://localhost:3000/libs/vue.global.js"></script>
<script src="http://localhost:3000/libs/vue-router.global.js"></script>
</head>
<body>
<div id="app">
<h1>Vue Router</h1>
<p>
<router-link to="/">主页</router-link>
<router-link to="/apple">Go to Apple</router-link>
<router-link to="/banana">Go to Banana</router-link>
<router-link to="/orange">Go to Orange</router-link>
</p>
<P>下方为路由页面</P>
<router-view></router-view>
</div>
<script>
const Home = { template: "<div>水果</div>" };
const Apple = { template: "<div>Apple</div>" };
const Banana = { template: "<div>Banana</div>" };
const Orange = { template: "<div>Orange</div>" };
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(),
routes: [
{ path: "/", component: Home },
{ path: "/apple", component: Apple },
{ path: "/banana", component: Banana },
{ path: "/orange", component: Orange },
],
});
const app = Vue.createApp({});
app.use(router);
app.mount("#app");
</script>
</body>
</html>
测试hashchange
window.addEventListener("hashchange", () => {
console.log("页面Hash值发生变化!");
});
很简单嘛,给window添加hashchange事件监听。然而实际操作下来,没有任何作用。
不管怎么改路由,hashchange事件都没有被触发,打开控制台输入location.hash
却能看见hash产生了变化。
探究原因
这篇文章有所描述:https://segmentfault.com/q/1010000040105060
vue-router的hash表面上是改变hash值,实际上调用的却是pushState和replaceState的API,能够在不触发hashchange事件的情况下替换hash值。
寻求新方法
既然他使用的是pushState和replaceState,那么监听这两个不就行了。很遗憾,原生并不支持这两个事件的监听。原生支持的是popState,此事件会在back()的情况下触发,pushState和replaceState并不会触发此事件。
在Vue-Router源码中可以看到,他手动重写了pushState和replaceState,为其添加了事件监听器,从而实现路由跳转。
function useHistoryListeners(base, historyState, currentLocation, replace) {
let listeners = [];
let teardowns = [];
// TODO: should it be a stack? a Dict. Check if the popstate listener
// can trigger twice
let pauseState = null;
const popStateHandler = ({ state, }) => {
const to = createCurrentLocation(base, location);
const from = currentLocation.value;
const fromState = historyState.value;
let delta = 0;
if (state) {
currentLocation.value = to;
historyState.value = state;
// ignore the popstate and reset the pauseState
if (pauseState && pauseState === from) {
pauseState = null;
return;
}
delta = fromState ? state.position - fromState.position : 0;
}
else {
replace(to);
}
// Here we could also revert the navigation by calling history.go(-delta)
// this listener will have to be adapted to not trigger again and to wait for the url
// to be updated before triggering the listeners. Some kind of validation function would also
// need to be passed to the listeners so the navigation can be accepted
// call all listeners
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
});
});
};
function pauseListeners() {
pauseState = currentLocation.value;
}
function listen(callback) {
// set up the listener and prepare teardown callbacks
listeners.push(callback);
const teardown = () => {
const index = listeners.indexOf(callback);
if (index > -1)
listeners.splice(index, 1);
};
teardowns.push(teardown);
return teardown;
}
function beforeUnloadListener() {
const { history } = window;
if (!history.state)
return;
history.replaceState(assign({}, history.state, { scroll: computeScrollPosition() }), '');
}
function destroy() {
for (const teardown of teardowns)
teardown();
teardowns = [];
window.removeEventListener('popstate', popStateHandler);
window.removeEventListener('beforeunload', beforeUnloadListener);
}
// set up the listeners and prepare teardown callbacks
window.addEventListener('popstate', popStateHandler);
// TODO: could we use 'pagehide' or 'visibilitychange' instead?
// https://developer.chrome.com/blog/page-lifecycle-api/
window.addEventListener('beforeunload', beforeUnloadListener, {
passive: true,
});
return {
pauseListeners,
listen,
destroy,
};
}
结语
本次操作对vue-router有了更深的理解,如果有朋友有监听vue-router内hash变化的思路欢迎私信交流~