前言
乍一看标题,很多人可能会说,这也太简单了吧?现在前端的项目基本都是SPA,SPA主要用Vue,React,Angular的某一种框架,它们都会提供现成的钩子函数,供大家调用,我直接调用现成的方法不就可以了吗?
但是我想说的是:如果让你写一个通用的js库,无论在哪个框架上使用,无论路由是history还是hash模式,都可以监听页面的变化,进而做些相应的处理,那么你会怎么办呢?
浏览器自带的监听URL事件都有哪些?
1. hashchange
hashchange事件可以监听hash路由的变化。使用方法如下:
//第一种
window.onhashchange = fn;
//第二种
<body onhashchange="fn();">
//第三种
window.addEventListener("hashchange", fn, false);
兼容性如下:
2. popstate
popstate也可以监听url变化:
//第一种
window.onpopstate = fn;
//第二种
<body onpopstate="fn();">
//第三种
window.addEventListener("popstate", funcRef, false);
具体是
- 当用户点击浏览器
前进/后退按钮时,会触发该事件。 - 当用户手动调用
window.history.go(),window.history.back(),window.history.forward()会触发该事件。 - 和
hashchange事件一样,当hash路由变化时(即用形如window.location.hash="#home"改变url时)会触发该事件。
由以上可知,popstate事件比hashchange事件更加强大,二者肯定优先选择popstate事件。它兼容性虽然差了一些,但现代浏览器大多都是支持的,也还好。
兼容性如下:
3. 使用定时器
这个理解起来比较简单,下面是一个简单的例子:
let oldUrl = window.location.href
setInterval(function test() {
let newUrl = window.location.href
if(oldUrl !== newUrl) {
console.log("page change")
}
}, 100)
但是setInterval存在几个问题,详见:
- 不能保证时间间隔:如果调用的函数需要花很长时间执行,那某些调用会忽略
- 无视代码错误: 如果调用的代码出现错误,也会不断的调用
- 无视网络延迟: 当使用其请求数据时, 如果服务器过载或临时断网等等,请求要花的时间远比想象的长,此时它还会源源不断的发起请求,最终浏览器网络队列会塞满ajax调用。
解决办法就是使用setTimeout,它可以让其他宏任务有插入宏任务队列的机会,不会一直占着主线程。
但是,无论是使用setInterval,抑或是setTimeout,总归是不够智能。
4. MutationObserver
它具有监听DOM树改变的能力,例如:
// Select the node that will be observed for mutations
const targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
const config = { childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
for(const mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
这个功能足够强大,但是却并不十分符合要求,因为对于不同的SPA项目来说,根节点不一样,只能监听body,对于一个交互较多,Dom变更比较频繁的项目而言,会过度的触发这个函数,因此看起来也不足够智能。
5. 重写pushstate,replaceState方法
pushState()原理是向历史记录中添加一条记录,replaceState()是替换当前的记录,二者虽然会改变url,但是页面并不会刷新。因此,许多SPA项目如果要是使用history路由模式,正常页面跳转功能是正常的,但是一刷新页面就会报404。因为实际上并没有这个路径。此时需要配置服务器。
查找源码可以发现,
无论是vue, react,还是angular应用,几乎所有的SPA项目,history路由跳转都是通过window.history.pushState和window.history.replaceState方法进行路由跳转。但是浏览器没有监听这两个方法的事件。
那么我们应该怎么做呢?
伟大的毛主席说过:不破不立,不塞不流,不止不行。
聪明的你应该想到了,没错,既然有跳转的方法,我们就重写这个方法,既然没有这个事件,那么我们就创造这个事件。那么来吧:
function registerEventHandler(target) {
return function registerTargetEventHandler(methodName) {
const originMethod = target[methodName]
return function eventHandler(...args) {
const event = new Event(methodName.toLowerCase())
originMethod.apply(target, args)
window.dispatchEvent(event)
return originMethod
}
}
}
const registerHistoryEventHandler = registerEventHandler(window.history)
window.history.pushState = registerHistoryEventHandler("pushState")
window.history.replaceState = registerHistoryEventHandler("replaceState")
window.addEventListener("pushstate", function() {
console.log("The pushstate Event is dispatched!")
}, false)
window.addEventListener("replacestate", function() {
console.log("The replacestate Event is dispatched!")
}, false)
总结
综上所述,我们现在既可以监听hash,又可以监听history模式。完整代码如下:
function registerEventHandler(target) {
return function registerTargetEventHandler(methodName) {
const originMethod = target[methodName]
return function eventHandler(...args) {
const event = new Event(methodName.toLowerCase())
originMethod.apply(target, args)
window.dispatchEvent(event)
return originMethod
}
}
}
const registerHistoryEventHandler = registerEventHandler(window.history)
window.history.pushState = registerHistoryEventHandler("pushState")
window.history.replaceState = registerHistoryEventHandler("replaceState")
function todo() {
console.log("The Url is changed")
}
window.addEventListener("pushstate", todo, false)
window.addEventListener("replacestate", todo, false)
window.addEventListener("popstate", todo, false)