如何监控前端路由变化

自从前端从多页面时代大规模向SPA单页面转变,便出现了前端路由,前端路由本质上是根据hashhistory的变化来执行对应的回调函数,比如vue-router就是根据URL的变化来渲染对应组件,那么historyhash有什么区别呢?

history和hash模式

这两者的更改都不会让页面重新加载,所以才适用于SPA页面的导航。

history模式出现之前,都是用hash去实现前端路由。在hash是根据URL后面的#来表示,比如www.example.com/#header可以根据window.location.hash = "newHash"window.location.replace("#newValue")来更改其值,改变hash值并不会让页面重新响应。

history是在浏览器中维护了一个关于path的栈,可以使用pushState往里面新增path,这时候会这个新增的path会替换当前的URL;使用replaceState会将当前的历史记录条目,也就是对当前页面的URL进行修改。这两个api都会触发当前URL的替换,如果你的项目中有针对history的监听,就可以触发对应的回调函数做一些事情。 值得注意的是,出于隐私和安全的原因,Web开发者不能直接通过JavaScript查看或访问当前浏览器会话的完整历史记录队列,window.history对象不提供任何方法或属性来查看历史记录中的实际URL,开发者只能通过histoty.length来获取当前会话历史的长度,即包含的历史记录条目数。

pushState示例:

javascript 复制代码
// 当点击一个链接或按钮时,你可能会调用这个函数
function navigateToPage(page) {
  // 这里的 `pageInfo` 是一个包含了新页面信息的对象
  const pageInfo = {
    title: page.title,
    url: page.url,
  };

  // 在历史记录中添加一个新条目
  history.pushState(pageInfo, pageInfo.title, pageInfo.url);

  // 可以选择在这里更新页面内容,比如通过 AJAX 加载新页面的数据
  // 更新页面标题
  document.title = pageInfo.title;

  // 记得更新实际的内容,这里只是一个例子
  // document.getElementById("content").innerHTML = ajaxContent;
}

// 例如,使用上面的函数导航到一个新页面,不会触发页面重新加载
navigateToPage({ title: 'New Page', url: '/newpage' });

replaceState示例:

javascript 复制代码
// 假设你想在不创建新历史记录的情况下,更新当前页面的 URL 参数
function updateURLParameter(param, value) {
  const url = new URL(window.location);
  url.searchParams.set(param, value);

  // 替换当前历史记录
  history.replaceState({ title: document.title }, document.title, url);

  // 你可能还会更新页面内容以反映新的参数
}

// 比如说,更新 URL 的参数而不改变历史记录
updateURLParameter('search', 'query');

pushStatereplaceState函数都接收三个参数:

  1. state: 一个与指定的新历史记录条目相关联的状态对象。这个状态对象可以是任何可以被序列化的 JavaScript 对象。当用户导航到新的状态时,浏览器会触发一个 popstate 事件,事件对象的 state 属性包含了这个状态对象。
  2. title: 大多数浏览器目前将这个参数忽略。一些文档可能会建议给它一个空字符串,或者使用文档的标题。按规范,你可以为新的历史记录条目提供一个标题,但是这个标题并不会改变浏览器标签的标题。
  3. url (可选): 新历史记录条目的 URL。浏览器将这个URL显示在地址栏中。这个参数必须是与当前 URL同源的,或者至少必须保持路径(path)、查询参数(query)和片段(fragment)的相对改变。如果提供的 URL 与当前 URL 不兼容,则会抛出SecurityError

如: 值得注意的是,第三个参数可以接受一些不同形式的字符串参数:

  1. 绝对URL:
javascript 复制代码
"https://www.example.com/path/page?search=query#hash"

但必须与当前页面的协议、域名和端口匹配,否则会抛出异常。

  1. 相对于当前路径的URL:
javascript 复制代码
"/path/page?search=query#hash"

这将设置新的路径,并且保持与当前URL相同的协议和域名。

  1. 只有路径的部分:
javascript 复制代码
"/path/page"

这将改变路径,查询和哈希将会被去除。

  1. 查询字符串:
javascript 复制代码
"?search=query"

这会更改当前URL的查询部分,同时保留路径和哈希(如果存在的话)。

  1. 哈希部分:
javascript 复制代码
"#hash"

这将修改URL的哈希部分,但保持其他部分不变。

  1. 相对于当前URL的路径:
javascript 复制代码
"page2" // 假设当前路径为 /path/page1

这会导致新路径变为/path/page2

另外,history上面还有gobackforward几个apigo可以向前或向后跳转几步,backforward相当于浏览器左上角的后退和前进按钮。

如何监听

监听history

javascript 复制代码
window.addEventListener("popstate", function() {
     console.log('popstate', e);
});

使用popstate可以监听以下情况

  1. 在浏览器中手动修改URLhash模式时更改hash
  2. 用户点击浏览器的后退和前进按钮
  3. 调用 history.go(), history.back(), 或 history.forward()方法时

当我们使用pushStatereplaceStateURL会改变,但是却无法被任何事件监听到,这时我们可以重写history上的pushStatereplaceState方法,使用到上篇文章提到的AOP编程思想,不清楚的同学可以复习一下:在JavaScript中使用AOP编程思想监听HTTP请求 - 掘金

现在直接把代码搬过来:

javascript 复制代码
/**
 * 重写对象上面的某个属性
 * @param targetObject 需要被重写的对象
 * @param propertyName 需要被重写对象的 key
 * @param newImplementation 以原有的函数作为参数,执行并重写原有函数
 */
function overrideProperty(
  targetObject,
  propertyName,
  newImplementation,
) {
  if (targetObject === undefined) return // 若当前对象不存在
  if (propertyName in targetObject) {  // 若当前对象存在当前属性
    const originalFunction = targetObject[propertyName]
    const modifiedFunction = newImplementation(originalFunction) // 把原本的函数传入
    if (typeof modifiedFunction == 'function') {
      targetObject[propertyName] = modifiedFunction
    }
  }
}

然后直接调用

javascript 复制代码
overrideProperty(history, 'pushState', originalFetch => {
  return function (...args) {
    // 在触发pushState前做些什么
    pushStateCallback()
    return originalFetch.apply(this, args)
  }
})
overrideProperty(history, 'replaceState', originalFetch => {
  return function (...args) {
    // 在触发replaceState前做些什么
    replaceStateCallback()
    return originalFetch.apply(this, args)
  }
})

这样当我们使用这两个api时就会被监听到。

监听hash

全局监听hashchange就可以

javascript 复制代码
window.addEventListener("hashchange", function() {
    console.log('Hash changed to: ' + window.location.hash);
});

处理回调函数

假设我们需要记录两个参数:

  1. referer:当前页面从哪个页面跳转过来
  2. newURL:当前页面的URL
  3. type:路由改变类型

我们新建一个普通的HTML页面(上面出现过的代码不再复现),recordPvEvent函数负责记录路由变化,若是在监控SDK的话可以改成发送数据逻辑,页面初始化时执行一遍recordPvEvent

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <button onclick="triggerPushState()">
    pushState
  </button>
  <button onclick="triggerReplaceState()">
    pushState
  </button>
</body>
<script>
  let hisURL = document.location.href;
  let isLastPopState = false;
  // 触发pushState
  function triggerPushState() {
    history.pushState({ page: 1 }, null, 'testPUSH');
  }
  // 触发replaceState
  function triggerReplaceState() {
    history.replaceState({ page: 1 }, null, 'testREP');
  }
  // 页面初始化
  recordPvEvent({
    type: 'init',
    referer: document.referrer,
    newURL: document.location.href
  })
  // 记录路由变化(或上报数据)
  function recordPvEvent(event) {
    const { referer = hisURL, type } = event
    console.log('event: ', {
      type,
      referer,
      newURL: document.location.href
    });
    hisURL = document.location.href; // 更新hisURL
  }
  window.addEventListener('popstate', (e) => {
    isLastPopState = true;
    recordPvEvent({
      type: 'popstate'
    })
  })
  window.addEventListener('hashchange', function () {
    if(!isLastPopState) {
      recordPvEvent({
        type: 'hashchange'
      })
    }
    isLastPopState = false;
    
  });
  // 上面button触发的
  function pushStateCallback() {
    isLastPopState = false;
    recordPvEvent({
      type: 'pushState'
    })
  }
  // 上面button触发的
  function replaceStateCallback() {
    isLastPopState = false;
    recordPvEvent({
      type: 'replaceState'
    })
  }
</script>

</html>

这时可以分成几种情况:

  1. 初始化进入页面时,不会触发任何事件
  2. 手动刷新页面时,不会触发任何事件
  3. 触发pushState时,被拦截
  4. 触发replaceState时,被拦截
  5. hash模式时手动更改URL,会先后触发popStatehashChange

因为在手动更改URL时可能先后触发popStatehashChange,所以定义一个isLastPopState变量,当它为true时,表示最后一次触发的事件为popState,这时候如果触发hashChange事件就不会被记录。

vue-routerreact-router都是用hashhistory去实现的,所以这一套监听在这些框架中也能使用,但是要根据不同router在具体场景的触发事件做相应的调整,避免重复触发,这样就能使用这一套逻辑去监听所有web页面的路由变化了。

总结

本文先从前端路由的hashhistory原理出发,描述了两者的异同点,再根据两者的属性对前端路由进行监听。

欢迎点赞、收藏、转发~ 你的一个小小的赞是我最大的鼓励~

相关推荐
世俗ˊ20 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92120 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_25 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人34 分钟前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css
清汤饺子1 小时前
实践指南之网页转PDF
前端·javascript·react.js
蒟蒻的贤1 小时前
Web APIs 第二天
开发语言·前端·javascript