如何监控前端路由变化
自从前端从多页面时代大规模向SPA
单页面转变,便出现了前端路由,前端路由本质上是根据hash
和history
的变化来执行对应的回调函数,比如vue-router
就是根据URL
的变化来渲染对应组件,那么history
和hash
有什么区别呢?
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
示例:
// 当点击一个链接或按钮时,你可能会调用这个函数
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
示例:
// 假设你想在不创建新历史记录的情况下,更新当前页面的 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');
pushState
和replaceState
函数都接收三个参数:
- state: 一个与指定的新历史记录条目相关联的状态对象。这个状态对象可以是任何可以被序列化的
JavaScript
对象。当用户导航到新的状态时,浏览器会触发一个popstate
事件,事件对象的state
属性包含了这个状态对象。 - title: 大多数浏览器目前将这个参数忽略。一些文档可能会建议给它一个空字符串,或者使用文档的标题。按规范,你可以为新的历史记录条目提供一个标题,但是这个标题并不会改变浏览器标签的标题。
- url (可选): 新历史记录条目的
URL
。浏览器将这个URL
显示在地址栏中。这个参数必须是与当前URL
同源的,或者至少必须保持路径(path)、查询参数(query)和片段(fragment)的相对改变。如果提供的URL
与当前URL
不兼容,则会抛出SecurityError
。
如:
值得注意的是,第三个参数可以接受一些不同形式的字符串参数:
- 绝对
URL
:
"https://www.example.com/path/page?search=query#hash"
但必须与当前页面的协议、域名和端口匹配,否则会抛出异常。
- 相对于当前路径的
URL
:
"/path/page?search=query#hash"
这将设置新的路径,并且保持与当前URL
相同的协议和域名。
- 只有路径的部分:
"/path/page"
这将改变路径,查询和哈希将会被去除。
- 查询字符串:
"?search=query"
这会更改当前URL
的查询部分,同时保留路径和哈希(如果存在的话)。
- 哈希部分:
"#hash"
这将修改URL
的哈希部分,但保持其他部分不变。
- 相对于当前
URL
的路径:
"page2" // 假设当前路径为 /path/page1
这会导致新路径变为/path/page2
。
另外,history
上面还有go
、back
、forward
几个api
,go
可以向前或向后跳转几步,back
和forward
相当于浏览器左上角的后退和前进按钮。
如何监听
监听history
window.addEventListener("popstate", function() {
console.log('popstate', e);
});
使用popstate
可以监听以下情况
- 在浏览器中手动修改
URL
(hash
模式时更改hash
) - 用户点击浏览器的后退和前进按钮
- 调用
history.go()
,history.back()
, 或history.forward()
方法时
现在直接把代码搬过来:
/**
* 重写对象上面的某个属性
* @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
}
}
}
然后直接调用
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
就可以
window.addEventListener("hashchange", function() {
console.log('Hash changed to: ' + window.location.hash);
});
处理回调函数
假设我们需要记录两个参数:
- referer:当前页面从哪个页面跳转过来
- newURL:当前页面的URL
- type:路由改变类型
我们新建一个普通的HTML
页面(上面出现过的代码不再复现),recordPvEvent
函数负责记录路由变化,若是在监控SDK
的话可以改成发送数据逻辑,页面初始化时执行一遍recordPvEvent
。
<!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>
这时可以分成几种情况:
- 初始化进入页面时,不会触发任何事件
- 手动刷新页面时,不会触发任何事件
- 触发
pushState
时,被拦截 - 触发
replaceState
时,被拦截 - 在
hash
模式时手动更改URL
,会先后触发popState
和hashChange
因为在手动更改URL
时可能先后触发popState
和hashChange
,所以定义一个isLastPopState
变量,当它为true
时,表示最后一次触发的事件为popState
,这时候如果触发hashChange
事件就不会被记录。
vue-router
和react-router
都是用hash
和history
去实现的,所以这一套监听在这些框架中也能使用,但是要根据不同router
在具体场景的触发事件做相应的调整,避免重复触发,这样就能使用这一套逻辑去监听所有web
页面的路由变化了。
总结
本文先从前端路由的hash
和history
原理出发,描述了两者的异同点,再根据两者的属性对前端路由进行监听。
欢迎点赞、收藏、转发~ 你的一个小小的赞是我最大的鼓励~
转载自:https://juejin.cn/post/7340998385304633380