🧑?💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
需求
首先列舉一下需要攔截的行為,接下來我們逐個實現。
- 瀏覽器前進后退
- 標簽頁刷新和關閉
- 路由跳轉
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()}
}
總結
我們實現了對 用戶刷新、關閉標簽頁、瀏覽器歷史回退、路由跳轉 等操作的攔截,可以在某些特殊場景下給用戶一些友好的提示,提升用戶體驗。
如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。