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

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

需求

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

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

1、拦截浏览器前进后退

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

首先给大家明确一点,出于安全问题,浏览器并不支持通过js拦截浏览器的前进后退操作,但是可以使用障眼法。

具体思路就是我们可以在页面加载的时候,使用 history.pushState 这个API给页面添加一个当前页面的历史记录(不会导致页面刷新),此时最近的两条历史记录都是当前页面,当用户点击后退的时候,浏览器会退到上一个记录(还是当前页面),这时会触发 popstate事件 ,回退的时候再往历史记录里添加一条当前页面的记录(为了下次拦截使用),同时我们使用弹窗提示用户一些信息,如果用户确定要回退,我们再使用 history.go(-2) 跳过这两条当前页面的记录,返回到真正的上个页面,这样我们就成功模拟了回退操作的拦截。

代码实现:

bash 复制代码
import { onUnmounted } from 'vue'

interface IBrowserInterceptEvents {
  popstate?: (next: () => void) => void // 监听浏览器前进后退
}

// 作用:添加一个历史记录,以便后续模拟拦截后退
function addStopHistory() {
  const state = { id: 'stopBack' }
  if (history.state.id === 'stopBack') return
  history.pushState(state, '', window.location.href)
}

const useBrowserInterceptor = (events: IBrowserInterceptEvents) => {
  const { popstate } = events
  let popstateCallback: EventListener | undefined

  let isHistoryBack = false
  // 拦截浏览器后退
  if (popstate) {
    addStopHistory()
    popstateCallback = () => {
      addStopHistory()
      popstate(() => {
        isHistoryBack = true
        history.go(-2)
      })
    }
    window.addEventListener('popstate', popstateCallback)
  }

  // 销毁事件
  onUnmounted(() => {
    // 不是历史后退触发的,仅仅是组件卸载,才需要清除模拟拦截后退时添加的历史记录
    if (popstate && !isHistoryBack) {
      history.go(-1)
    }
    popstateCallback && window.removeEventListener('popstate', popstateCallback)
  })
}

export default useBrowserInterceptor

使用

bash 复制代码
// 使用拦截
useBrowserInterceptor({
  popstate: showWarnModal,
})

// 弹窗提示
const showWarnModal = (next: any) => {
  const { pending, uploading, failed } = taskStatusMap.value
  if (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 变量做判断区分。

刷新页面:

关闭页面:

代码实现

bash 复制代码
import { onUnmounted } from 'vue'

interface IBrowserInterceptEvents {
  popstate?: (next: () => void) => void // 监听浏览器前进后退
  beforeunload?: EventListener // 监听标签页刷新和关闭
}

// addStopHistory ...

const useBrowserInterceptor = (events: IBrowserInterceptEvents) => {
  const { popstate, beforeunload } = events
  let popstateCallback: EventListener | undefined
  let beforeunloadCallback: EventListener | undefined

  let 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

使用

bash 复制代码
useBrowserInterceptor({
  popstate: showWarnModal,
  beforeunload: (e) => {
    const { pending, uploading, failed } = taskStatusMap.value
    if (pending + uploading + failed > 0) {
      e.preventDefault()
      e.returnValue = false
    }
  }
})

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

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

历史回退也有可能触发导航守卫,也要使用 isHistoryBack 做判断区分。

最后我们还要处理一下事件的销毁,组件卸载时销毁事件,这里有个注意点:我们不仅要移除注册的事件,当组件卸载不是历史后退(isHistoryBack)也不是路由跳转(isRouter)触发的,仅仅是组件卸载(比如v-if),这个时候还需要清除模拟拦截后退时添加的历史记录,否则会造成页面回退异常。

代码实现(完整版)

bash 复制代码
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') return
  history.pushState(state, '', window.location.href)
}

const useBrowserInterceptor = (events: IBrowserInterceptEvents) => {
  const { popstate, beforeunload, beforeRouteLeave } = events
  let popstateCallback: EventListener | undefined
  let beforeunloadCallback: EventListener | undefined

  let isHistoryBack = false
  let isRouter = false
  // 拦截浏览器后退
  if (popstate) {
    addStopHistory()
    popstateCallback = () => {
      addStopHistory()
      popstate(() => {
        isHistoryBack = true
        history.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 = true
        next()
      })
    })

  // 销毁事件
  onUnmounted(() => {
    // 不是后退且不是导航守卫触发的,仅仅是组件卸载,才需要清除模拟拦截后退时添加的历史记录
    if (popstate && !isHistoryBack && !isRouter) {
      history.go(-1)
    }
    popstateCallback && window.removeEventListener('popstate', popstateCallback)
    beforeunloadCallback && window.removeEventListener('beforeunload', beforeunloadCallback)
  })
}

export default useBrowserInterceptor

使用

bash 复制代码
// 使用拦截
useBrowserInterceptor({
  beforeRouteLeave: showWarnModal,
  popstate: showWarnModal,
  beforeunload: (e) => {
    const { pending, uploading, failed } = taskStatusMap.value
    if (pending + uploading + failed > 0) {
      e.preventDefault()
      e.returnValue = false
    }
  }
})

// 弹窗提示
const showWarnModal = (next: any) => {
  const { pending, uploading, failed } = taskStatusMap.value
  if (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()
  }
}

总结

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

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

相关推荐
sheji34161 分钟前
【开题答辩全过程】以 基于web的图书借阅系统的设计与实现为例,包含答辩的问题和答案
前端
●VON2 分钟前
Flutter组件深度解析:从基础到高级的完整指南
android·javascript·flutter·harmonyos·von
CodeSheep6 分钟前
两位大佬相继离世,AI时代我们活得太着急了
前端·后端·程序员
xuankuxiaoyao8 分钟前
VUE.JS 实践 第三章
前端·javascript·vue.js
放下华子我只抽RuiKe521 分钟前
NLP自然语言处理硬核实战笔记
前端·人工智能·机器学习·自然语言处理·开源·集成学习·easyui
PieroPc21 分钟前
电脑DIY组装报价系统 用MiMo V2 Pro 写html ,再用opencode(选MiMo 作模型) 当录入口
前端·html
工程师老罗24 分钟前
lvgl有哪些布局?
前端·javascript·html
好家伙VCC26 分钟前
# 发散创新:用Selenium实现自动化测试的智能断言与异常处理策略在现代Web应用开发中,*
java·前端·python·selenium
木子清billy36 分钟前
物联网浏览器(IoTBrowser)-js开发人脸识别
开发语言·javascript·物联网
关中老四38 分钟前
【原生JS甘特图MZGantt 】如何给父任务设置独立进度条
前端·javascript·甘特图