于怀初一 發表於 2025-5-22 16:23:00

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

<h1 data-id="heading-0">🧑‍💻 写在开头</h1>
<p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<h2 data-id="heading-1">需求</h2>
<p>首先列举一下需要拦截的行为,接下来我们逐个实现。</p>
<ol>
<li>浏览器前进后退</li>
<li>标签页刷新和关闭</li>
<li>路由跳转</li>
</ol>
<h2 data-id="heading-2">1、拦截浏览器前进后退</h2>
<blockquote>
<p>这里的实现是核心,涉及到大量 History API 的理解,如果不太了解可以先看一下这两个文章:<br>
拦截浏览器后退方法附带独家干货知识点<br>
浏览器的History、Location对象,及使用js控制网页的前进后退和加载,刷新当前页面总结!</p>


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

</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202505/2149129-20250522162037806-2086904861.png" alt="" loading="lazy"></p>
<h3 data-id="heading-3">代码实现:</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:bash;gutter:true;">import { onUnmounted } from 'vue'

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

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

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

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

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

export default useBrowserInterceptor</pre>
</div>
<h3 data-id="heading-4">使用</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:bash;gutter:true;">// 使用拦截
useBrowserInterceptor({
popstate: showWarnModal,
})

// 弹窗提示
const showWarnModal = (next: any) =&gt; {
const { pending, uploading, failed } = taskStatusMap.value
if (pending + uploading + failed &gt; 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()
}
}</pre>
</div>
<div>
<div>
<h2 data-id="heading-5">2、拦截标签页刷新和关闭</h2>
<p>这个比较简单,我们只需要监听 beforeunload 事件,阻止默认行为即可。但是这里要注意:出于浏览器安全问题,我们只能使用浏览器默认弹窗提示(如下图),无法自定义提示内容。</p>
<blockquote>
<p>历史回退也有可能导致触发 <strong>beforeunload</strong> 事件,所以要添加一个 <strong>isHistoryBack</strong> 变量做判断区分。</p>
</blockquote>
<p><strong>刷新页面:</strong></p>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202505/2149129-20250522162119197-842112702.png" alt="" loading="lazy"></p>
<p><strong>&nbsp;关闭页面:</strong></p>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202505/2149129-20250522162131190-151075668.png" alt="" loading="lazy"></p>
<p>&nbsp;</p>
<h3 data-id="heading-6">代码实现</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:bash;gutter:true;">import { onUnmounted } from 'vue'

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

// addStopHistory ...

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

let isHistoryBack = false
// 拦截浏览器后退 ...

// 拦截标签页关闭和刷新
if (beforeunload) {
    beforeunloadCallback = (event) =&gt; {
      if (!isHistoryBack) beforeunload(event)
    }
    window.addEventListener('beforeunload', beforeunloadCallback)
}

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

export default useBrowserInterceptor</pre>
</div>
<h3 data-id="heading-7">使用</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:bash;gutter:true;">useBrowserInterceptor({
popstate: showWarnModal,
beforeunload: (e) =&gt; {
    const { pending, uploading, failed } = taskStatusMap.value
    if (pending + uploading + failed &gt; 0) {
      e.preventDefault()
      e.returnValue = false
    }
}
})</pre>
</div>
<div>
<div>
<h2 data-id="heading-8">3、拦截路由跳转(完整版)</h2>
<p>这里我们可以使用 <strong>vue-router</strong> 提供的 onBeforeRouteLeave 钩子函数在组件内注册一个导航守卫,当用户跳转路由的时候进行弹窗提示。</p>
<blockquote>
<p>历史回退也有可能触发导航守卫,也要使用 <strong>isHistoryBack</strong> 做判断区分。</p>
<p><strong>最后我们还要处理一下事件的销毁,组件卸载时销毁事件,这里有个注意点:我们不仅要移除注册的事件,当组件卸载不是历史后退(isHistoryBack)也不是路由跳转(isRouter)触发的,仅仅是组件卸载(比如v-if),这个时候还需要清除模拟拦截后退时添加的历史记录,否则会造成页面回退异常。</strong></p>
</blockquote>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202505/2149129-20250522162208462-897193543.png" alt="" loading="lazy"></p>
<p>&nbsp;</p>
<h3 data-id="heading-9">代码实现(完整版)</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:bash;gutter:true;">import { onUnmounted } from 'vue'
import { type NavigationGuardNext, onBeforeRouteLeave } from 'vue-router'

interface IBrowserInterceptEvents {
popstate?: (next: () =&gt; void) =&gt; void // 监听浏览器前进后退
beforeunload?: EventListener // 监听标签页刷新和关闭
beforeRouteLeave?: (next: NavigationGuardNext) =&gt; void // 导航守卫
}

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

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

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

// 拦截标签页关闭和刷新
if (beforeunload) {
    beforeunloadCallback = (event) =&gt; {
      if (!isHistoryBack) beforeunload(event)
    }
    window.addEventListener('beforeunload', beforeunloadCallback)
}

// 导航守卫
beforeRouteLeave &amp;&amp;
    onBeforeRouteLeave((_to, _from, next) =&gt; {
      if (isHistoryBack) {
      next()
      return
      }
      beforeRouteLeave(() =&gt; {
      isRouter = true
      next()
      })
    })

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

export default useBrowserInterceptor</pre>
</div>
<h3 data-id="heading-10">使用</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:bash;gutter:true;">// 使用拦截
useBrowserInterceptor({
beforeRouteLeave: showWarnModal,
popstate: showWarnModal,
beforeunload: (e) =&gt; {
    const { pending, uploading, failed } = taskStatusMap.value
    if (pending + uploading + failed &gt; 0) {
      e.preventDefault()
      e.returnValue = false
    }
}
})

// 弹窗提示
const showWarnModal = (next: any) =&gt; {
const { pending, uploading, failed } = taskStatusMap.value
if (pending + uploading + failed &gt; 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()
}
}</pre>
</div>
<h2 data-id="heading-11">总结</h2>
<p>我们实现了对&nbsp;用户刷新、关闭标签页、浏览器历史回退、路由跳转&nbsp;等操作的拦截,可以在某些特殊场景下给用户一些友好的提示,提升用户体验。</p>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></p>
</div>
</div>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/18891534
頁: [1]
查看完整版本: 优化用户体验:拦截浏览器前进后退、刷新、关闭、路由跳转等用户行为并弹窗提示