
本文介绍使用 navigator.sendBeacon() 结合备用 fetch 方案,在页面卸载前异步持久化用户视频观看进度(如当前秒数),确保数据不丢失,兼容 React 函数组件与现代浏览器。
本文介绍使用 navigator.sendbeacon() 结合备用 fetch 方案,在页面卸载前异步持久化用户视频观看进度(如当前秒数),确保数据不丢失,兼容 react 函数组件与现代浏览器。
在 Web 应用中,记录用户视频观看进度(例如“已观看至第 217 秒”)并支持续播,是提升用户体验的关键功能。但直接在 beforeunload 或 unload 事件中调用 async/await 的 fetch 请求极易失败——因为浏览器会在事件处理函数返回后立即终止所有未完成的网络请求,导致数据丢失。
✅ 根本解法:优先使用 navigator.sendBeacon()sendBeacon() 是专为卸载场景设计的 API:它异步发送小体积(通常 ≤64KB)的 POST 请求,不阻塞页面关闭,且浏览器保证尽力送达(即使标签页已关闭,只要 Tab 进程尚未完全退出)。它不返回 Promise,但返回布尔值指示是否成功入队。
以下是推荐的封装方案——一个可复用的 React 自定义 Hook:
// hooks/useUnloadBeacon.ts
import { useEffect } from 'react';
interface UseUnloadBeaconOptions {
url: string;
data: Record<string, any>;
}
const useUnloadBeacon = ({ url, data }: UseUnloadBeaconOptions) => {
useEffect(() => {
const handleUnload = () => {
const jsonData = JSON.stringify(data);
const blob = new Blob([jsonData], { type: 'application/json' });
// ✅ 优先使用 sendBeacon(无阻塞、高可靠性)
const beaconSent = navigator.sendBeacon(url, blob);
// ❌ 回退方案:仅当 sendBeacon 不可用时尝试 fetch(注意:fetch 在 unload 中仍可能被中断)
if (!beaconSent) {
console.warn('sendBeacon not supported or failed; falling back to fetch (unreliable)');
fetch(url, {
method: 'POST',
body: jsonData,
headers: { 'Content-Type': 'application/json' },
keepalive: true, // 关键!允许 fetch 在页面卸载后继续运行(部分浏览器支持)
}).catch(console.error);
}
};
window.addEventListener('unload', handleUnload);
return () => window.removeEventListener('unload', handleUnload);
}, [url, JSON.stringify(data)]); // 注意:data 需稳定引用或序列化依赖
};
export default useUnloadBeacon;? 在组件中使用示例:
import useUnloadBeacon from './hooks/useUnloadBeacon';
function VideoPlayer({ videoId, timelineRef }) {
// 假设 timelineRef.current 存储当前播放秒数(number)
const saveData = {
video_id: videoId,
seconds: Math.floor(timelineRef.current || 0),
};
// ✅ 自动监听 unload 并发送进度
useUnloadBeacon({
url: '/api/save_video',
data: saveData,
});
return <video src={`...`} ref={/* ... */} />;
}⚠️ 关键注意事项:
-
不要使用
beforeunload处理数据保存:该事件主要用于提示用户“确认离开”,浏览器禁止其内执行异步操作;unload才是正确时机。 -
sendBeacon的限制:仅支持POST,且 payload 必须是ArrayBufferView、Blob、FormData或URLSearchParams;不能设置自定义 headers(服务端需接受默认Content-Type: text/plain,或用Blob指定类型)。 -
服务端适配:若使用
Blob发送 JSON,服务端需解析原始 body(如 Express 中用express.raw({ type: 'application/json' })中间件)。 -
React 依赖陷阱:
useEffect依赖项中避免直接传入timelineRef.current(它是 mutable 值),应通过JSON.stringify()或useMemo稳定化,或改用ref+useLayoutEffect同步更新。
? 进阶建议:
- 对高价值行为(如完成 90% 视频),可结合定时上报(如每 30 秒
debounced保存)+ 卸载兜底,实现双重保障。 - 前端保存后,服务端应做幂等处理(如基于
user_id + video_id唯一索引),避免重复写入。
通过 sendBeacon 主力保障 + fetch + keepalive 兜底的组合策略,你能在绝大多数现代浏览器中可靠捕获用户离开瞬间的观看状态,为无缝续播打下坚实基础。

















