如何监听浏览器URL变化?

496 阅读3分钟

因业务需要,我们团队开发的所有系统都需要使用同样的菜单导航组件,但随着技术更新,新的业务系统难免会做技术升级,比如从vue2升级到vue3,或者采用更合适的UI框架。这就导致菜单组件要想适用所有业务系统,要不需要维护多个版本支持不同技术栈,要不需要开发一套与框架无关的组件。经过调研,最终决定采用第2种方案--基于Web Components重构菜单组件。

在重构过程中遇到一个问题,**如果在业务代码里触发了路由变更,菜单组件内部如何感知到,并更新相关样式?**例如,路由变化了,菜单组件需要重新计算展开项以及当前选中项。

以基于Vue开发的项目为例,这类项目一般都使用vue-router管理路由,了解过vue-router源码的都知道,其底层使用了History Api。我们菜单组件是基于Web Components做的,那么自然要使用原生的事件监听来获取路由的变化。一般来说,我们监听浏览器URL的变化是通过监听popstate事件来实现,但遗憾的是,History Api中有两个方法的调用(pushState和replaceState),虽然改变了URL,但是并不会触发popstate事件,而且也没找到原生支持的事件可以获取到这个改变。详情见这里

那怎么解决这个问题呢?骚操作有几个,其中有2个方案还比较靠谱:

  1. 重写原生的pushState和replaceState方法
  2. 通过Proxy对象拦截和监听pushState和replaceState方法的调用

重写原生方法

// 保存原生的 pushState方法
const originalPushState = history.pushState;

// 覆盖 pushState 方法
history.pushState = function(state, title, url) {
  // 调用原生的 pushState 方法
  originalPushState.apply(this, arguments);

  // 创建并触发自定义事件
  const pushAndReplaceStateEvent = new CustomEvent('pushandreplacestate', { detail: { state, title, url } });
  window.dispatchEvent(pushAndReplaceStateEvent);};
// 监听自定义的 pushstate 事件
window.addEventListener('pushandreplacestate', function(event) {  console.log('pushandreplacestate event triggered:', event);});

replaceState按照同样的方法操作。

通过Proxy拦截监听

// 创建一个代理处理程序
const handler = {
  apply: function(target, thisArg, argumentsList) {
    // 调用原生的 pushState 方法
    const result = target.apply(thisArg, argumentsList);

    // 创建并触发自定义事件
    const pushAndReplaceStateEvent = new CustomEvent('pushandreplacestate', { detail: { state: argumentsList[0], title: argumentsList[1], url: argumentsList[2] } });    window.dispatchEvent(pushAndReplaceStateEvent);
    return result;
  }
};

// 使用 Proxy 包装原生的 pushState 方法
const pushStateProxy = new Proxy(history.pushState, handler);
// 使用 Proxy 包装原生的 replaceState 方法
const replaceStateProxy = new Proxy(history.replaceState, handler);

// 替换原生的 pushState 方法
history.pushState = pushStateProxy;
// 替换原生的 pushState 方法
history.replaceState = replaceStateProxy;
// 监听自定义的 pushstate 事件
window.addEventListener('pushandreplacestate', function(event) {  console.log('pushandreplacestate event triggered:', event);});

我采用的第2种方案,通过Proxy代理监听pushState和replaceState方法的调用。这种方法不需要覆盖原生的方法,而是通过代理捕获调用。当调用pushState或replaceState方法时,代理程序会触发自定义的pushandreplacestate事件。这种方法可以避免直接修改原生方法,从而降低代码冲突风险。