拦截浏览器后退方法附带独家干货知识点

前言

本次主题,大家都能在网上搜到许多解决方案的文章,本篇我的文章所给你带来不一样的地方是:

  • pushState和popState的关系许多细节干货介绍,网上都没有细说的细节
  • 完整的方案介绍,虽然思路跟其他文章差不多,但是实际写起来的细节点更为详细,值得留意
  • 网上大多数介绍的popState方案,也有文章提及到hash的方案,但是并无代码提供,基本上都是说思路一样,但是其实真的写起来,需要注意点会略有不同
  • 发现一些浏览器差异问题,存在一些怪异现象,供大家了解
  • 文末提供工具函数,方便大家直接拿来即用

背景介绍

需要实现一个效果,在用户进行了url后退操作,提供一个二次弹窗确认是否需要离开(或有些需求直接不给后退),一般都是处于有表单编辑输入的页面,以防客户输入了一堆内容,不小心触发了回退操作,导致内容丢失了。

所以关键点就在于,需要监听到回退的事件,然后做出自己的处理逻辑。而要实现上述的效果,有两个方法:

  • 监听popstate
  • 监听hashchange

思路

首先我们要知道,前端领域里浏览器的后退行为是无法阻止的,即你点击浏览器上的后退按钮,不论怎样,浏览器就是会后退一个访问记录。所以我们要实现拦截阻止这个后退行为,得使用障眼法。

希望在不刷新影响当前界面的情况下,插入一条新的url访问记录,那么这样当我们点击返回操作的时候,就会仍然回到当前页面,但是仍然希望不会发生刷新重载的结果。

要达到上述目的的关键是,url的变化不会影响到页面的变化,这么一说,是不是很熟悉,不知道各位是否脑海中立即呈现一个东西,我们很常见的就能达到这个效果。

没错,就是hash,当然,除了hash外,其实还有一个方法,就是pushSate

所以下文就是围绕着这两种方法来说明解决方案

监听popstate

使用该方案,我希望你能基本知道两个js的api------pushState & popstate

当你看完官方文档后,你可能会觉得你读懂了,理解了。那么我向你提问几个问题,如果你能清晰回答出来,那就证明,你真的懂了:

  1. 当使用pushSate时传入了一个新的url地址,此时界面变化是怎样,浏览器地址是怎样,会重新加载新的url吗?
  2. 当你在某个页面上监听了popstate,从别的url上切回到这个页面(不论是前进还是后退),会不会触发popstate
  3. 多次url地址的切换,是所有页面都是触发到popstate事件吗?
  4. 多个页面添加了popstate,实际上到底哪个会被触发?
  5. popstatepushSate的关系是怎样的?
  6. 还有什么其他方式会导致popstate的发生

当你心中尚存上述问题的疑问,那么请继续阅读下面我通过实践总结出来的一些细节点,把一些官方资料没说得很清楚的或者漏说的一些细节点补充一下。

当然你不是很清晰也没关系,对于你要解决拦截阻止后退这个需求不受到影响,这里只是提供一些干活给大家。

pushState 和 popstate的理解

  1. pushState不会触发页面跳转,仅仅是url地址发生变动,但是不会加载真实url对应的页面
  2. 通过pushState方法添加的历史会话记录,在添加的会话记录A与被添加的会话记录B中来回前进与后退(包括浏览器上的前进与后退按钮,以及js执行historyback/forward/go方法),都会触发绑定在对应页面页面上的popstate事件;也不会发生页面的改变(除非你监听popstate事件主动做出的页面改变等),即不会去加载真实url对应的页面: 若在A真实页面上,与pushSate出来的B进行一个会话记录的访问变化,就会触发的是A页面上绑定的popstate事件。若在B真实页面上,就会触发的B上绑定的popsate事件。反正要牢记,popstate事件只在A和B这有pushstate建立联系的会话记录上变化才会触发,以当前真实页面的popstate为准。
  3. 若离开了A页面(包括url显示B地址,但是还是在A页面上的情况),绑定在A上的popstate事件就会销毁了。所以当你从别的页面回退到A页面,不会触发A上的popstate事件,因为回退的时候该事件才重新开始建立监听绑定,不是之前的那次监听绑定了。或者是回退到因A通过pushState产生的B页面上时,此时时真实的对应url上的B页面,这样更加不会触发A上的popstate事件,都不是一个页面,此时window.onpopstate是null,除非B页面自己也重新建立了绑定监听。
  4. 尽管发生了B的的跳转,但是通过访问历史会话记录的方式重新回到B,history.state始终都是当时会话记录的state情况,即pushStatestate的值。重新通过地址访问B的地址,此时是一个新的会话记录,所以这种情况下history.state不是当时在A上pushStatestate的值。

资料上写着的 【每当用户导航到新的 state,都会触发 popstate 事件】,一开始咋一看,以为是要导航到的state不一样,才会触发popstate事件,实际上并不是的,不用管state的值的情况。

上述例子是两个会话历史的说明,多条亦是如此。重点在于是否由pushState产生的。

hash的变化,如通过location.hash来设置hash值,则也会触发popstate,跟是不是pushState产生的记录无关,只要改变了hash就会触发了。

实现

就算你真的没有深入了解过pushStatepopstate,大概知道啥作用,其实也不太影响你对接下来解决方案的理解那接下来,正式说解决方案。

  1. 在进入需要拦截阻止浏览器后退行为的页面后,马上使用pushState插入一条记录,以及添加popstate事件的监听。
  2. 当用户触发了后退行为,就会触发了popstate事件的监听函数,函数里需要使用pushState再次插入一条记录,目的是为了当用户再次点击后退按钮,仍然会触发后退效果。
  3. 若你的需求需要弹出弹窗给用户选择是否继续后退,则函数里就需要触发一个弹窗,当用户选择确定后退,则调用histroy.go(-2)触发后退两次记录,因为此时有两条记录是代表当前页的,一条是页面自身的记录,一条是pushState插入的记录,所以你的目的记录是两条前的记录。

是不是很简单,那么用代码展示下:

js 复制代码
// 第一个入参state信息若无特别需要,则可以随便写
function addStopHistory () {
    history.pushState({
        id: 'stopBack'
    }, '', window.location.href)
}

addStopHistory()

// 若有需要,则需区分是否是hash的变动引起的popstate
window.addEventlistener('popstate', () => {
    addStopHistory()
    // 有如果需要弹窗提示
    // ...
    // 需要确定后退则 histroy.go(-2)
})

需要注意的是,当你需要解绑添加的popstate事件时,你需要主动执行一次histroy.go(-2),目的是为了消灭主动插入的新的记录,不然当真的需要点击后退时,第一次后退是不会成功的,得点两次

监听hashchange

做法同样很简单:

  1. 在进入需要拦截阻止浏览器后退行为的页面后,马上使用location.hash插入一条记录,以及添加hashchange事件的监听。
  2. 当用户触发了后退行为,就会触发了hashchange事件的监听函数,函数里需要使用location.hash再次插入一条记录,目的是为了当用户再次点击后退按钮,仍然会触发后退效果。当然这里不同于pushState,主动添加hash也是会引起hashchange的,所以需要区分是后退行为引起的还是主动插入的。
  3. 若你的需求需要弹出弹窗给用户选择是否继续后退,则函数里就需要触发一个弹窗,当用户选择确定后退,则调用histroy.go(-2)触发后退两次记录,因为此时有两条记录是代表当前页的,一条是页面自身的记录,一条是location.hash插入的记录,所以你的目的记录是两条前的记录。

那么用代码展示下:

js 复制代码
// 这个变量是用来标识此时是hash变化是主动插入记录所用的,则不要执行hashchange的内容了
let hashAdding = false

function addStopHistory () {
    location.hash = 'stopback'
}
// 注意不要立马页面执行改变hash,不然浏览器会认为这是一个历史记录,得延迟一些执行
hashAdding = true
addStopHistory()

window.addEventlistener('hashchange', () => {
    // 主动插入记录引起的hashchange就不用执行真正的阻止拦截行为了
    if (hashAdding) {
        hashAdding = false
        return
    }
    hashAdding = true
    addStopHistory()
    // 有如果需要弹窗提示
    // ...
    // 需要确定后退则 histroy.go(-2)
})

需要注意的是,当你需要解绑添加的popstate事件时,你需要主动执行一次histroy.go(-2),目的是为了消灭主动插入的新的记录,不然当真的需要点击后退时,第一次后退是不会成功的,得点两次。

怪异现象

这里发现有两个问题:

  • 有些页面会报错Use of history.pushState in a trivial session history context, which maintains only one session history entry, is treated as history.replaceState.
  • 如果不对界面进行任何操作(随便点击一下界面也好呀,就是让页面聚焦),则不会触发popstatehashchange

经过测试发现,上述的怪异问题表现,不同浏览器表现不同的,像FireFox就压根没有上述问题。Chrome自己进行了优化,因此上述表现更为明显,它把上述场景视为一个是非必要的情况,因此给优化了。像Edge360等浏览器,也有不同程度的优化。

那么接下来简单了解下这里的两个问题。

问题一

bash 复制代码
Use of history.pushState in a trivial session history context, which maintains only one session history entry, is treated as history.replaceState.

这个问题的产生是使用了pushState,网上搜索了一番,没找到对应的解释,看来较少人遇到或使用。

那么这个问题产生的影响结果是:无法插入一条新的历史记录。

因为没有对应的解释,只能看提示推测了,英文翻译过来的意思就是

scss 复制代码
使用history.pushState在一个简单的会话历史上下文中(它只维护一个会话历史条目)被视为history.replacestate

提示得很隐晦,具体是个啥也得靠猜。

既然这样,尝试去解决它,从解决手段来反推敲原因,也不失一个好办法。

经过多种测试:

  • 进入页面后,设置定时器延时执行第一次的pushState。 √
  • 在页面的load事件里执行第一次的pushState。 ×
  • 进入页面后先replaceState赋予一个state,再用pushState。×

最终还是使用setTimeout生效,而且时间还不能太短,我这边测试700ms OK。从这个表现来看,是浏览器自身做的优化,它把极短时间内二次变动的地址信息,把这个添加行为视为原本url想要的最终效果,故合并在一起了。

当然因为我是简单的demo测试,没有类似异步之类的各种复杂渲染过程,在实际项目中,你正常调用pushState本身的时机可能就是延迟后的

这个跟通过location.hash改变url的hash很相似,如果你进入页面的立马改变hash,也是同样会把当前url和改变hash后的url合并成一个历史记录。

经过测试,发现因为简单的一个页面demo测试,会发生上述问题,当放到几个实际的项目中,其实较少发生上述情况,概率上来讲较少发生。

问题二

尽管通过pushState或改变hash能插入一条新的历史记录了,如果不对界面进行任何操作,则不会触发popstatehashchange。同样该问题也是存在浏览器差异,也是浏览器自身的一个优化,只不过此时对我们来讲是一个"负优化"而已。

还是火狐浏览器坚挺,其他浏览器多少都会有这个优化,如chrome、360、edge

这个暂时没有解决办法,但是正常使用来讲,从实际产品使用来讲,本身你进入了一个界面,点都没点下,就点击后退了,不阻止也是正常的。而但凡你对页面点击一下,证明你关心过当前界面,也就能成功拦截阻止了。你说它这样人性化嘛,也的确挺智能的。。。

注意上述问题一和二,可能随着浏览器的版本迭代而变化,毕竟这本身只是一个浏览器自身优化问题。

工具函数

上述思路,两种方法都介绍完毕了,并且细节需要留意的点也都提及了。我这里已经封装好了一个工具函数了,大家可以直接引用来使用,一键开启关闭功能。

安装

csharp 复制代码
npm i uiueutils

// or

pnpm add uiueutils

// or

yarn add uiueutils

使用

js 复制代码
/**
 * 方法入参说明
 * @param {String} type - 实现拦截的方式,分为使用pushState和添加hash两种,值对应为 state 和 hash
 * @param {Function} callback - 监听到回退的实践函数,例如添加二次确认弹窗等
 * @param {OBject} options - { timer, msg } timer:延迟添加拦截的秒数;msg:添加的state值或hash值
 * @returns {Function} 用于取消拦截的函数,调用一次即取消了拦截
 */

import { backWatcher } from 'uiueutils'

// 添加拦截
backWatcher.add('state')
// 取消拦截,例如在你页面关闭时调用
backWatcher.remove()

// 添加拦截回调函数
const remove = backWatcher.add('state', () => {
    console.info('已拦截')
})
// 取消拦截,跟backWatcher.remove()一样
remove()

// 添加拦截,并延迟执行,且添加的hash值是stop
backWatcher.add('hash', null, {
    timer: 700,
    msg: 'stop'
})

如果本篇文章对你有所帮助,欢迎点个赞吧~感谢!

文章为个人原创,未经允许请勿私自转载

相关推荐
careybobo12 分钟前
海康摄像头通过Web插件进行预览播放和控制
前端
TDengine (老段)38 分钟前
TDengine 中的关联查询
大数据·javascript·网络·物联网·时序数据库·tdengine·iotdb
杉之2 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端2 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡2 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木3 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!3 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
難釋懷4 小时前
JavaScript基础-移动端常见特效
开发语言·前端·javascript
还是鼠鼠4 小时前
Node.js全局生效的中间件
javascript·vscode·中间件·node.js·json·express
自动花钱机4 小时前
WebUI问题总结
前端·javascript·bootstrap·css3·html5