js实现监听单页面应用url的办法
errol发表于2025-03-12 | 分类为 编程 | 标签为js单页面应用监听urlspa油猴

最近在写油猴脚本的时候,偶尔遇到一些单页面应用模型的网站。

单页面应用(Single Page Applications,SPA),顾名思义,是只有一个页面的web应用,与传统的web应用不同,它只需要首次从服务器加载资源(静态),在不刷新的情况下,后续的任何内容更新仅发生于客户端浏览器而不会请求服务器,这种特性在提升了客户端性能的利用率&提升用户体验的同时,也减轻了服务器压力。

但是,该特性也间接导致了基于url匹配规则的脚本无法正常运行。

默认情况下,油猴插件会根据url的匹配结果,在页面(或文档)加载前后注入脚本,即是说,脚本的执行依赖于“加载页面”这个过程,只有加载页面时,才会执行脚本。

而由于SPA只会从服务器加载一次页面,后续看到的“切换页面”或“跳转页面”,实际上都只是在当前页面上动态生成并渲染出来的,url虽然发生变化但未真正加载页面,因此脚本不会执行。

在得知了这一原因之后,我们就应该明白,想要脚本能在单页面应用下正常执行,就需要监听网站的url(或路由),当变为某个url时,执行某些逻辑。

简而言之,相当于从油猴插件手中接管了部分匹配路径的工作。

不过,比较遗憾的是,js并未提供任何现成可用的监听url的接口或方法,因此,通常情况下无法对url实现监听。

请注意,这里说的是“通常情况”,而不是说不可实现。

众所周知,单页面应用一般是在html5的history api的基础上实现的,它提供了一套操作浏览器会话历史(browser session history)的方法,使得修改浏览器会话历史而不触发页面刷新成为可能。

主要涉及到两个方法:

  1. pushState()
  2. replaceState()

以vue为例,查看其路由vue-router框架源码可知,无论是Router.push()还是Router.replace(),在底层都调用了以上两个方法:

这里指history和hash模式

// 位置:./packages/router/src/history/html5.ts
// 版本:v4.5.0
function push(to: HistoryLocation, data?: HistoryState) {
    // ...
    changeLocation(currentState.current, currentState, true)
    // ...
    changeLocation(to, state, false)
    currentLocation.value = to
}

function replace(to: HistoryLocation, data?: HistoryState) {
    // ...
    changeLocation(to, state, true)
    currentLocation.value = to
}

function changeLocation(to: HistoryLocation, state: StateEntry, replace: boolean): void {
    // ...
    try {
        // BROWSER QUIRK
        // NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
        history[replace ? 'replaceState' : 'pushState'](state, '', url)
        historyState.value = state
    } catch (err) {
        // ...
    }
}

此外,还有go()、back()、forward()三个方法,以及一个与其密切相关的PopStateEvent接口:当浏览器历史记录改变时,该接口对应popstate事件将会被触发。

在了解了以上的信息之后,应该不难联想到,我们或许可以通过监听popstate事件来得知单页面应用的url变化,当进行路由跳转时,底层调用history api,继而触发popstate事件...

addEventListener("popstate", (event) => {
    // to do something
});

然而,现实并非如想象般美好。

我们会发现,写完代码后,无论进行怎样的跳转,监听器都没有任何反应。

原因在于pushState()和replaceState()方法不会触发popstate事件,The history stack文档中已经明确表示。

Note that just calling history.pushState() or history.replaceState() won't trigger a popstate event. The popstate event will be triggered by doing a browser action such as a click on the back or forward button (or calling history.back() or history.forward() in JavaScript).

这也是前面提到js未提供任何可监听url变化接口或方法的原因。

与之形成对比的是,处于同一层级的history.back()和history.forward()却不受这一限制,或者是浏览器的前进、后退按钮,都能正常触发popstate事件,从视觉效果上来看,它们的作用和路由跳转别无二致,所以感觉调用history api未触发事件有点奇怪,不然没什么理由,即使它不叫popstate,也应该有一个别的事件。

不过,前面也说了,办法还是有的。

既然它们没有触发,那就让它们触发不就好了...

目前已经了解到,单页面应用的路由跳转底层会调用pushState()或replaceState(),因此,只需要对这它们动一动手脚:

const methods = ['replaceState', 'pushState'];
for (const item of methods) {
    const fn = history[item];
    history[`_${item}`] = fn;
    history[item] = function (state, unused, url) {
        history[`_${item}`](state, unused, url);
        const ev = new Event('urlchange');
        ev.args = { state, unused, url };
        window.dispatchEvent(ev);
    }
}
window.addEventListener('urlchange', ev => console.log(ev));

以上代码对两个方法进行了重写,使得它们在被调用时触发自定义的“urlchange”事件,该事件对象携带了将要跳转的url信息,同时还为urlchange事件添加了监听器,以某个单页面应用网站为例:

image

图1 监听url实现-1

点击菜单,从当前页面/#/article跳转到目标页面/#/category。

image

图1 监听url实现-2

可以看到,在切换路由的时,预期的数据回传到事件处理器中,即是说,目前已经成功实现了对单页面应用url的监听效果。(有够简单的吧。。)

事实上,油猴插件其实也已经预料到这种情况,并对之做了处理:

window.onurlchange

If a script runs on a single-page application, then it can use window.onurlchange to listen for URL changes.

只需在脚本的元信息处处启用,效果是一样的,只是说,即使不用油猴插件的接口,或在油猴脚本以外的地方,也能很简单地实现对单页面应用路由的监听功能。

// ==UserScript==
...
// @grant window.onurlchange
// ==/UserScript==

if (window.onurlchange === null) {
    // feature is supported
    window.addEventListener('urlchange', (info) => ...);
}

现在可以愉快地编写脚本了。

本文完。

返回