当前位置: 首页 > ops >正文

优化用户体验:拦截浏览器前进后退、刷新、关闭、路由跳转等用户行为并弹窗提示

🧑‍💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣

需求

首先列举一下需要拦截的行为,接下来我们逐个实现。

  1. 浏览器前进后退
  2. 标签页刷新和关闭
  3. 路由跳转

1、拦截浏览器前进后退

这里的实现是核心,涉及到大量 History API 的理解,如果不太了解可以先看一下这两个文章:
拦截浏览器后退方法附带独家干货知识点
浏览器的History、Location对象,及使用js控制网页的前进后退和加载,刷新当前页面总结!

首先给大家明确一点,出于安全问题,浏览器并不支持通过js拦截浏览器的前进后退操作,但是可以使用障眼法。
具体思路就是我们可以在页面加载的时候,使用 history.pushState 这个API给页面添加一个当前页面的历史记录(不会导致页面刷新),此时最近的两条历史记录都是当前页面,当用户点击后退的时候,浏览器会退到上一个记录(还是当前页面),这时会触发 popstate事件 ,回退的时候再往历史记录里添加一条当前页面的记录(为了下次拦截使用),同时我们使用弹窗提示用户一些信息,如果用户确定要回退,我们再使用 history.go(-2) 跳过这两条当前页面的记录,返回到真正的上个页面,这样我们就成功模拟了回退操作的拦截。
在这里插入图片描述

代码实现:

import { onUnmounted } from 'vue'interface IBrowserInterceptEvents {popstate?: (next: () => void) => void // 监听浏览器前进后退
}// 作用:添加一个历史记录,以便后续模拟拦截后退
function addStopHistory() {const state = { id: 'stopBack' }if (history.state.id === 'stopBack') returnhistory.pushState(state, '', window.location.href)
}const useBrowserInterceptor = (events: IBrowserInterceptEvents) => {const { popstate } = eventslet popstateCallback: EventListener | undefinedlet isHistoryBack = false// 拦截浏览器后退if (popstate) {addStopHistory()popstateCallback = () => {addStopHistory()popstate(() => {isHistoryBack = truehistory.go(-2)})}window.addEventListener('popstate', popstateCallback)}// 销毁事件onUnmounted(() => {// 不是历史后退触发的,仅仅是组件卸载,才需要清除模拟拦截后退时添加的历史记录if (popstate && !isHistoryBack) {history.go(-1)}popstateCallback && window.removeEventListener('popstate', popstateCallback)})
}export default useBrowserInterceptor

使用

// 使用拦截
useBrowserInterceptor({popstate: showWarnModal,
})// 弹窗提示
const showWarnModal = (next: any) => {const { pending, uploading, failed } = taskStatusMap.valueif (pending + uploading + failed > 0) {Modal.confirm({title: h('h3', '当前页面有未完成的任务!'),width: 500,content: h('div', null, [taskStatusMap.value.pending? h(Tag, { color: 'default' }, `待上传:${taskStatusMap.value.pending}`): null,taskStatusMap.value.uploading? h(Tag, { color: 'processing' }, `上传中:${taskStatusMap.value.uploading}`): null,taskStatusMap.value.failed? h(Tag, { color: 'error' }, `上传失败:${taskStatusMap.value.failed}`): null,h('div',{ style: { marginTop: '10px' } },'此操作会导致未完成上传的视频数据丢失,确定要继续吗?')]),onOk() {next()}})} else {next()}
}

2、拦截标签页刷新和关闭

这个比较简单,我们只需要监听 beforeunload 事件,阻止默认行为即可。但是这里要注意:出于浏览器安全问题,我们只能使用浏览器默认弹窗提示(如下图),无法自定义提示内容。

历史回退也有可能导致触发 beforeunload 事件,所以要添加一个 isHistoryBack 变量做判断区分。

刷新页面:

在这里插入图片描述

关闭页面:

在这里插入图片描述

代码实现

import { onUnmounted } from 'vue'interface IBrowserInterceptEvents {popstate?: (next: () => void) => void // 监听浏览器前进后退beforeunload?: EventListener // 监听标签页刷新和关闭
}// addStopHistory ...const useBrowserInterceptor = (events: IBrowserInterceptEvents) => {const { popstate, beforeunload } = eventslet popstateCallback: EventListener | undefinedlet beforeunloadCallback: EventListener | undefinedlet isHistoryBack = false// 拦截浏览器后退 ...// 拦截标签页关闭和刷新if (beforeunload) {beforeunloadCallback = (event) => {if (!isHistoryBack) beforeunload(event)}window.addEventListener('beforeunload', beforeunloadCallback)}// 销毁事件onUnmounted(() => {// 不是后退且不是导航守卫触发的,仅仅是组件卸载,才需要清除模拟拦截后退时添加的历史记录if (popstate && !isHistoryBack) {history.go(-1)}popstateCallback && window.removeEventListener('popstate', popstateCallback)beforeunloadCallback && window.removeEventListener('beforeunload', beforeunloadCallback)})
}export default useBrowserInterceptor

使用

useBrowserInterceptor({popstate: showWarnModal,beforeunload: (e) => {const { pending, uploading, failed } = taskStatusMap.valueif (pending + uploading + failed > 0) {e.preventDefault()e.returnValue = false}}
})

3、拦截路由跳转(完整版)

这里我们可以使用 vue-router 提供的 onBeforeRouteLeave 钩子函数在组件内注册一个导航守卫,当用户跳转路由的时候进行弹窗提示。

历史回退也有可能触发导航守卫,也要使用 isHistoryBack 做判断区分。
最后我们还要处理一下事件的销毁,组件卸载时销毁事件,这里有个注意点:我们不仅要移除注册的事件,当组件卸载不是历史后退(isHistoryBack)也不是路由跳转(isRouter)触发的,仅仅是组件卸载(比如v-if),这个时候还需要清除模拟拦截后退时添加的历史记录,否则会造成页面回退异常。
在这里插入图片描述

代码实现(完整版)

import { onUnmounted } from 'vue'
import { type NavigationGuardNext, onBeforeRouteLeave } from 'vue-router'interface IBrowserInterceptEvents {popstate?: (next: () => void) => void // 监听浏览器前进后退beforeunload?: EventListener // 监听标签页刷新和关闭beforeRouteLeave?: (next: NavigationGuardNext) => void // 导航守卫
}// 作用:添加一个历史记录,以便后续模拟拦截后退
function addStopHistory() {const state = { id: 'stopBack' }if (history.state.id === 'stopBack') returnhistory.pushState(state, '', window.location.href)
}const useBrowserInterceptor = (events: IBrowserInterceptEvents) => {const { popstate, beforeunload, beforeRouteLeave } = eventslet popstateCallback: EventListener | undefinedlet beforeunloadCallback: EventListener | undefinedlet isHistoryBack = falselet isRouter = false// 拦截浏览器后退if (popstate) {addStopHistory()popstateCallback = () => {addStopHistory()popstate(() => {isHistoryBack = truehistory.go(-2)})}window.addEventListener('popstate', popstateCallback)}// 拦截标签页关闭和刷新if (beforeunload) {beforeunloadCallback = (event) => {if (!isHistoryBack) beforeunload(event)}window.addEventListener('beforeunload', beforeunloadCallback)}// 导航守卫beforeRouteLeave &&onBeforeRouteLeave((_to, _from, next) => {if (isHistoryBack) {next()return}beforeRouteLeave(() => {isRouter = truenext()})})// 销毁事件onUnmounted(() => {// 不是后退且不是导航守卫触发的,仅仅是组件卸载,才需要清除模拟拦截后退时添加的历史记录if (popstate && !isHistoryBack && !isRouter) {history.go(-1)}popstateCallback && window.removeEventListener('popstate', popstateCallback)beforeunloadCallback && window.removeEventListener('beforeunload', beforeunloadCallback)})
}export default useBrowserInterceptor

使用

// 使用拦截
useBrowserInterceptor({beforeRouteLeave: showWarnModal,popstate: showWarnModal,beforeunload: (e) => {const { pending, uploading, failed } = taskStatusMap.valueif (pending + uploading + failed > 0) {e.preventDefault()e.returnValue = false}}
})// 弹窗提示
const showWarnModal = (next: any) => {const { pending, uploading, failed } = taskStatusMap.valueif (pending + uploading + failed > 0) {Modal.confirm({title: h('h3', '当前页面有未完成的任务!'),width: 500,content: h('div', null, [taskStatusMap.value.pending? h(Tag, { color: 'default' }, `待上传:${taskStatusMap.value.pending}`): null,taskStatusMap.value.uploading? h(Tag, { color: 'processing' }, `上传中:${taskStatusMap.value.uploading}`): null,taskStatusMap.value.failed? h(Tag, { color: 'error' }, `上传失败:${taskStatusMap.value.failed}`): null,h('div',{ style: { marginTop: '10px' } },'此操作会导致未完成上传的视频数据丢失,确定要继续吗?')]),onOk() {next()}})} else {next()}
}

总结

我们实现了对 用户刷新、关闭标签页、浏览器历史回退、路由跳转 等操作的拦截,可以在某些特殊场景下给用户一些友好的提示,提升用户体验。

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

在这里插入图片描述

http://www.xdnf.cn/news/8037.html

相关文章:

  • 西门子 S1500 博途软件舞台威亚 3D 控制方案
  • SQL:窗口函数(Window Functions)
  • 基于ITcpServer/IHttpServer框架的HTTP服务器
  • 关于大语言模型的问答?
  • 后端开发实习生-抖音生活服务
  • Centos系统资源镜像配置
  • Java集合框架深度剖析:结构、并发与设计模式全解析
  • 生物化学笔记: 药物 论文阅读 赖氨酸用于预防和治疗皮肤单纯疱疹感染 基础信息药理学临床试验
  • 笔试模拟 day12
  • 小白刷题 之 如何高效计算二进制数组中最大连续 1 的个数
  • jQuery Mobile 表单输入详解
  • Linux shell 正则表达式高效使用
  • 配置gem5环境:Dockerfile使用
  • Netty学习专栏(二):Netty快速入门及重要组件详解(EventLoop、Channel、ChannelPipeline)
  • 计算机网络 第三章:运输层(三)
  • AI|Java开发 IntelliJ IDEA中接入本地部署的deepseek方法
  • IDEA启动报错:Cannot invoke “org.flowable.common.engine.impl.persistence.ent
  • LESS基础用法详解
  • 智能制造:基于AI制造企业解决方案架构设计【附全文阅读】
  • Redis实战篇Day01(短信登录篇)
  • 《C++ list详解》
  • 金仓数据库主备切换故障解析,一次由相对路径引发的失败与切换流程解读
  • 抛弃传统P2P技术,EasyRTC音视频基于WebRTC打造教育/会议/远程巡检等场景实时通信解决方案
  • 数据库blog5_数据库软件架构介绍(以Mysql为例)
  • 大队项目流程
  • 流程引擎选型指南
  • VSCode推出开源Github Copilot:AI编程新纪元
  • 实战:Dify智能体+Java=自动化运营工具!
  • C++ 中的 **常变量** 与 **宏变量** 比较
  • 【TI MSP430与SD NAND:心电监测的长续航解决方案】