優化用戶體驗:攔截瀏覽器前進后退、刷新、關閉、路由跳轉等用戶行為并彈窗提示
????? 寫在開頭
點贊 + 收藏 === 學會??????
需求
首先列舉一下需要攔截的行為,接下來我們逐個實現。
- 瀏覽器前進后退
- 標簽頁刷新和關閉
- 路由跳轉
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') 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
使用
// 使用攔截
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 變量做判斷區分。
刷新頁面:

關閉頁面:

代碼實現
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
使用
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),這個時候還需要清除模擬攔截后退時添加的歷史記錄,否則會造成頁面回退異常。

代碼實現(完整版)
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
使用
// 使用攔截
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()
}
}
總結
我們實現了對 用戶刷新、關閉標簽頁、瀏覽器歷史回退、路由跳轉 等操作的攔截,可以在某些特殊場景下給用戶一些友好的提示,提升用戶體驗。
如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。


浙公網安備 33010602011771號