我们相信:世界是美好的,你是我也是。平行空间的世界里面,不同版本的生活也在继续...

需求是这样的:页面监控浏览器里地址栏里的url变化情况。那么问题来了,页面的url变化,(虚假),页面加载的源码就会发生变化,那么相关的程序就会发生重载,无法持续进行地址的监控。所以,这似乎存在着一个悖论。那么,真实的情况是怎么样的呢?

苏南大叔:浏览器地址变换,popstate、pushstate以及replacestate事件 - popstate-pushstate-replacestate
浏览器地址变换,popstate、pushstate以及replacestate事件(图2-1)

苏南大叔的“程序如此灵动”博客,记录苏南大叔和计算机代码的故事。本文测试环境:chrome@130.0.6723.92

需求悖论

这个事情从需求到实现,都存在着悖论。比如:

虚假的悖论

事实上,地址栏显示的网址发生变化,页面【不一定】会重新到服务器请求源码的。为了持续监控地址变化,需要保证页面不被刷新。举例来说:

  • 普通的页面,变化#后面的hash的时候,页面也是不刷新的。
  • reactvue们,它们就是只变化地址栏,而不重新请求服务器的。

因此,对于页面仅变化hash,或者reactvue的情况来说,页面并不刷新,持续监控地址栏变化是可行的。

真实的悖论

因为监控的手段就是监听相关的地址变化事件(仅变化#hash,也属于其中)。相关事件一共有四个。分别是:

  • pushstate,产生新的地址。
  • popstate,在老地址之间变换。例如:历史前进或者历史后退动作。
  • replacestate,替换当前的地址。
  • hashchange,变换#hash时触发,但同时会触发pushstatepopstate

出乎意料的是,只有popstate事件是可以被监控到的。另外的pushstatereplacestate事件,无法被监控到。参考文章:

也就是说,正常情况下,只有点击左上角的前进后退按钮,或者代码执行history.back()等事件的时候,才能被监控到popstate事件。

改写函数打补丁

为了解决上述问题,这里利用函数改写的方法,在.pushstate().replacestate()函数中,hook了对应的事件。从而在外部能够对这两个事件做出对应的反应,这算打个系统补丁。

有关技术实现的细节,可以参考文章:

pushstate

(function (history) {
  var pushState = history.pushState;
  history.pushState = function (state, title, url) {
    var result = pushState.apply(history, arguments);
    var event = new CustomEvent("pushstate", {
      detail: { state: state, title: title, url: url },
    });
    window.dispatchEvent(event);
    return result;
  };
})(window.history);

replacestate

(function (history) {
  var replaceState = history.replaceState;
  history.replaceState = function (state, title, url) {
    var result = replaceState.apply(history, arguments);
    var event = new CustomEvent("replacestate", {
      detail: { state: state, title: title, url: url },
    });
    window.dispatchEvent(event);
    return result;
  };
})(window.history);

完整测试代码

值得一提的是:这段代码必须放在nginx之类的服务器下,才能正常使用。双击是不行的,会得到错误提示信息:

Uncaught SecurityError: Failed to execute 'pushState' on 'History': A history state object with URL 'file:///D:/t/1.html' cannot be created in a document with origin 'null' and URL 'file:///D:/t/index.html#c'

完整的测试代码如下:

<style>
  a,button{
    border: 1px solid #000;
    text-align: center;
    font-size:14px;
    height:25px;
    background:#f0f0f0;
    margin-bottom: 6px;
    text-decoration: none;
    color:#000;
    display: block;
    float:left;
    margin-right: 2px;
  }
</style>
<script>
  (function (history) {
    var pushState = history.pushState;
    var replaceState = history.replaceState;
    history.pushState = function (state, title, url) {
      var result = pushState.apply(history, arguments);
      var event = new CustomEvent("pushstate", {
        detail: { state: state, title: title, url: url },
      });
      window.dispatchEvent(event);
      return result;
    };
    history.replaceState = function (state, title, url) {
      var result = replaceState.apply(history, arguments);
      var event = new CustomEvent("replacestate", {
        detail: { state: state, title: title, url: url },
      });
      window.dispatchEvent(event);
      return result;
    };
  })(window.history);
  function fakego(path) {
    window.history.pushState(null, null, path);
  }
  function fakegogo(path) {
    window.history.replaceState(
      { foo: "bar" + path },
      "title" + path,
      path
    );
  }
</script>
<a href="#a">改变Hash:a</a>
<a href="#b">改变Hash:b</a>
<button onclick="javascript:fakego('1/1.html')">pushState1</button>
<button onclick="javascript:fakego('2/2.html')">pushState2</button>
<button onclick="javascript:fakegogo('3/3.html')">replaceState3</button>
<button onclick="javascript:history.back()">后退</button>
<button onclick="javascript:history.forward()">前进</button>
<script>
  window.addEventListener("pushstate", function (event) {
    console.log("pushstate:" + document.location);
    console.log("referer:" + document.referrer);
  });
  window.addEventListener("replacestate", function (event) {
    console.log("replacestate:" + document.location);
    console.log("referer:" + document.referrer);
  });
  window.addEventListener("popstate", function (event) {
    console.log("popstate:" + document.location);
    console.log("referer:" + document.referrer);
  });
  window.addEventListener("hashchange", function () {
    console.log("hashchange:" + document.location);
    console.log("referer:" + document.referrer);
  });
</script>

苏南大叔:浏览器地址变换,popstate、pushstate以及replacestate事件 - state测试代码
浏览器地址变换,popstate、pushstate以及replacestate事件(图2-2)

这段代码运行后,其实还可以得出结论:

  • 就是这些pushstate/popstate/replacestate/hashchange,都不会影响来源页的变化【待议】,也就是说当前的页面确实还没有发生变化,只是变标没变本。
  • hashchange可能伴随popstate或者pushstate

事件监听

打完补丁后,核心监控代码就这样:

window.addEventListener("popstate", function (event) {
  console.log("popstate:" + document.location);
});
window.addEventListener("pushstate", function (event) {
  console.log("pushstate:" + document.location);
});
window.addEventListener("replacestate", function (event) {
  console.log("replacestate:" + document.location);
});

结束语

更多苏南大叔的经验文章,请参考:

如果本文对您有帮助,或者节约了您的时间,欢迎打赏瓶饮料,建立下友谊关系。
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。

 【福利】 腾讯云最新爆款活动!1核2G云服务器首年50元!

 【源码】本文代码片段及相关软件,请点此获取更多信息

 【绝密】秘籍文章入口,仅传授于有缘之人   js