你是如何监听SPA项目URL的?

2,147 阅读4分钟

前言

乍一看标题,很多人可能会说,这也太简单了吧?现在前端的项目基本都是SPA,SPA主要用VueReactAngular的某一种框架,它们都会提供现成的钩子函数,供大家调用,我直接调用现成的方法不就可以了吗?

但是我想说的是:如果让你写一个通用的js库,无论在哪个框架上使用,无论路由是history还是hash模式,都可以监听页面的变化,进而做些相应的处理,那么你会怎么办呢?

浏览器自带的监听URL事件都有哪些?

1. hashchange

hashchange事件可以监听hash路由的变化。使用方法如下:

//第一种
window.onhashchange = fn;
//第二种
<body onhashchange="fn();">
//第三种
window.addEventListener("hashchange", fn, false);

兼容性如下:

image.png

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事件。它兼容性虽然差了一些,但现代浏览器大多都是支持的,也还好。

兼容性如下:

image.png

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. 重写pushstatereplaceState方法

pushState()原理是向历史记录中添加一条记录,replaceState()是替换当前的记录,二者虽然会改变url,但是页面并不会刷新。因此,许多SPA项目如果要是使用history路由模式,正常页面跳转功能是正常的,但是一刷新页面就会报404。因为实际上并没有这个路径。此时需要配置服务器。

查找源码可以发现,

vue-router源码

image.png

react的history库

image.png

image.png

angular源码

image.png

无论是vue, react,还是angular应用,几乎所有的SPA项目,history路由跳转都是通过window.history.pushStatewindow.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)