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

前言

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

  • 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'
})

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

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

相关推荐
GHUIJS4 分钟前
【vue3】vue3.5
前端·javascript·vue.js
-seventy-14 分钟前
对 JavaScript 原型的理解
javascript·原型
&白帝&32 分钟前
uniapp中使用picker-view选择时间
前端·uni-app
谢尔登38 分钟前
Babel
前端·react.js·node.js
ling1s39 分钟前
C#基础(13)结构体
前端·c#
卸任1 小时前
使用高阶组件封装路由拦截逻辑
前端·react.js
lxcw1 小时前
npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED
前端·npm·node.js
秋沐1 小时前
vue中的slot插槽,彻底搞懂及使用
前端·javascript·vue.js
这个需求建议不做1 小时前
vue3打包配置 vite、router、nginx配置
前端·nginx·vue
QGC二次开发1 小时前
Vue3 : Pinia的性质与作用
前端·javascript·vue.js·typescript·前端框架·vue