现代 Web 开发中检测用户离开页面的完整方案(附 Vue 实现)

在现代 Web 应用开发中,准确判断用户是否停留在当前页面是一个重要需求。这不仅关系到用户体验优化,还影响着数据分析准确性和系统资源利用率。本文将详细介绍前端检测用户离开页面的各种场景及解决方案,并提供 Vue 框架下的实现示例。

一、用户"离开页面"的场景细分

首先我们需要明确,"离开页面"并非单一行为,它包含多种场景:

  • 切换到其他浏览器标签页或应用(页面不可见但未关闭)
  • 最小化浏览器窗口(页面不可见)
  • 关闭浏览器标签页或整个浏览器
  • 在当前标签页导航到新 URL
  • 移动设备上切换到其他 App 或返回主屏幕
  • 使用浏览器"前进/后退"按钮导航

不同场景需要不同的检测方案,我们需要根据实际业务需求选择合适的技术组合。

二、核心检测技术与 API

1. Page Visibility API(页面可见性 API)- 现代首选

这是处理"页面是否对用户可见"的标准方案,专门用于检测页面隐藏/显示状态,适合处理切换标签页、最小化窗口等场景。

核心属性与事件

  • document.hidden:只读属性,页面不可见时返回 true
  • visibilitychange:当页面可见性状态变化时触发

适用场景

  • 暂停/恢复视频、音频播放
  • 停止/启动动画或轮播
  • 暂停后台数据轮询,恢复可见时继续
  • 切换主题色或通知状态

Vue 实现示例


<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const myVideo = ref(null)
const handleVisibilityChange = () => {
  if (document.hidden) {
    // 页面不可见时暂停视频
    myVideo.value?.pause()
    console.log('用户切换到其他标签页或最小化窗口')
  } else {
    // 页面恢复可见时继续播放
    myVideo.value?.play()
    console.log('用户回到当前页面')
  }
}
onMounted(() => {
  document.addEventListener('visibilitychange', handleVisibilityChange)
})
onUnmounted(() => {
  document.removeEventListener('visibilitychange', handleVisibilityChange)
})
</script>

优点

  • W3C 标准,所有现代浏览器支持
  • 性能友好,专为可见性检测设计
  • 直接反映页面"可见"状态

局限性

  • 无法区分"切换标签页"和"关闭标签页"
  • 不能检测页面真正卸载的情况

2. beforeunload 事件 - 防止数据丢失

该事件在页面即将卸载时触发,可用于询问用户是否确定离开,主要用于防止用户意外丢失未保存数据。

Vue 实现示例

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// 标记是否有未保存的数据
const hasUnsavedChanges = ref(false)
const handleBeforeUnload = (e) => {
  if (hasUnsavedChanges.value) {
    // 现代浏览器会忽略自定义文本,使用内置提示
    e.preventDefault()
    // Chrome 需要设置 returnValue
    e.returnValue = ''
    return ''
  }
}
onMounted(() => {
  window.addEventListener('beforeunload', handleBeforeUnload)
})
onUnmounted(() => {
  window.removeEventListener('beforeunload', handleBeforeUnload)
})
</script>

注意事项

  • 现代浏览器不允许自定义提示文本,只会显示标准化提示
  • 过度使用会影响用户体验,仅在有未保存数据时使用
  • 不能依赖此事件执行数据保存操作

3. navigator.sendBeacon() - 可靠数据上报

为解决页面卸载时异步请求不可靠的问题,sendBeacon() 允许异步发送少量数据,浏览器会保证在后台完成发送。

适用场景

  • 用户离开时上报访问时长、操作日志等分析数据
  • 记录用户最后操作状态

Vue 实现示例

<script setup>
import { onMounted, onUnmounted } from 'vue'
// 收集需要上报的分析数据
const getAnalyticsData = () => {
  return JSON.stringify({
    page: window.location.pathname,
    stayTime: Date.now() - window.pageLoadTime,
    lastAction: window.lastUserAction
  })
}
const handlePageHide = () => {
  // 发送数据到服务器,确保页面卸载时能成功提交
  navigator.sendBeacon('/api/user-leave', getAnalyticsData())
}
onMounted(() => {
  // 记录页面加载时间
  window.pageLoadTime = Date.now()
  window.addEventListener('pagehide', handlePageHide)
})
onUnmounted(() => {
  window.removeEventListener('pagehide', handlePageHide)
})
</script>

优点

  • 异步非阻塞,不影响页面卸载
  • 浏览器保证数据发送完成
  • 不会延迟页面切换或关闭

4. pagehide 和 pageshow 事件 - 处理往返缓存

现代浏览器(尤其是移动端)使用"往返缓存"(bfcache)优化后退/前进导航,此时 unload 事件可能不触发,pagehide 则更可靠。

核心特性

  • pagehide:导航离开页面时触发,无论是否存入 bfcache
  • event.persisted:判断页面是否被存入 bfcache
  • pageshow:从缓存恢复页面时触发

Vue 实现示例

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const handlePageHide = (event) => {
  if (event.persisted) {
    console.log('页面进入 bfcache,可能会被缓存')
    // 保存需要恢复的状态
    sessionStorage.setItem('pageState', JSON.stringify({
      scrollPosition: window.scrollY
    }))
  } else {
    console.log('页面正常卸载')
  }
  // 无论哪种情况都发送统计数据
  navigator.sendBeacon('/api/page-leave', JSON.stringify({
    path: window.location.pathname,
    isPersisted: event.persisted
  }))
}
const handlePageShow = (event) => {
  if (event.persisted) {
    console.log('从 bfcache 恢复页面')
    // 恢复之前保存的状态
    const savedState = sessionStorage.getItem('pageState')
    if (savedState) {
      const { scrollPosition } = JSON.parse(savedState)
      window.scrollTo(0, scrollPosition)
    }
  }
}
onMounted(() => {
  window.addEventListener('pagehide', handlePageHide)
  window.addEventListener('pageshow', handlePageShow)
})
onUnmounted(() => {
  window.removeEventListener('pagehide', handlePageHide)
  window.removeEventListener('pageshow', handlePageShow)
})
</script>

三、综合方案与最佳实践

根据不同业务场景,推荐以下组合方案:

  1. 页面可见性监测

    • 优先使用 Page Visibility API
    • 适用于暂停/恢复操作、资源优化
  2. 数据上报

    • 使用 navigator.sendBeacon() + pagehide 事件
    • 替代不可靠的 unload 事件
  3. 防止数据丢失

    • 必要时使用 beforeunload
    • 配合本地存储自动保存草稿
  4. 处理缓存与恢复

    • pagehidepageshow 处理 bfcache 场景
    • 保存/恢复页面关键状态

Vue 项目中的统一封装示例


<script setup>
import { onMounted, onUnmounted } from 'vue'
export default function usePageLeave(options) {
  const {
    onVisibilityChange,
    onPageHide,
    onBeforeUnload
  } = options
  // 可见性变化处理
  const handleVisibilityChange = () => {
    onVisibilityChange?.(document.hidden)
  }
  // 页面隐藏处理
  const handlePageHide = (event) => {
    onPageHide?.(event)
  }
  // 页面卸载前处理
  const handleBeforeUnload = (event) => {
    onBeforeUnload?.(event)
  }
  onMounted(() => {
    document.addEventListener('visibilitychange', handleVisibilityChange)
    window.addEventListener('pagehide', handlePageHide)
    if (onBeforeUnload) {
      window.addEventListener('beforeunload', handleBeforeUnload)
    }
  })
  onUnmounted(() => {
    document.removeEventListener('visibilitychange', handleVisibilityChange)
    window.removeEventListener('pagehide', handlePageHide)
    if (onBeforeUnload) {
      window.removeEventListener('beforeunload', handleBeforeUnload)
    }
  })
}
</script>

使用方式

<script setup>
import usePageLeave from './usePageLeave'
usePageLeave({
  onVisibilityChange: (isHidden) => {
    if (isHidden) {
      console.log('页面不可见')
      // 暂停视频、动画等
    } else {
      console.log('页面可见')
      // 恢复播放
    }
  },
  onPageHide: (event) => {
    console.log('页面即将离开', event.persisted)
    // 发送统计数据
    navigator.sendBeacon('/api/log', JSON.stringify({
      action: 'leave',
      path: window.location.pathname
    }))
  },
  onBeforeUnload: (event) => {
    // 检查是否有未保存数据
    if (hasUnsavedChanges.value) {
      event.preventDefault()
      event.returnValue = ''
      return ''
    }
  }
})
</script>

四、总结

检测用户离开页面是一个涉及多种场景的复杂需求,没有单一解决方案能覆盖所有情况。通过合理组合 Page Visibility API、pagehide 事件和 sendBeacon() 等现代 API,我们可以构建可靠的检测系统。

在 Vue 项目中,建议将这些逻辑封装为组合式函数,以便在多个组件中复用。同时要注意不同浏览器的兼容性差异,特别是移动端对 bfcache 的处理。

通过准确检测用户行为并做出相应处理,我们可以优化资源利用、提升用户体验,并收集更准确的分析数据,为产品迭代提供有力支持。