在线状态检测的前端实现方案与实时性优化实践
我的写法,亲测靠谱
在线状态(online/offline)这玩意儿看着简单,但真用到项目里,我踩过至少三轮坑——第一次是以为 navigator.onLine 能实时反映网络状态,结果用户断网后页面还显示“在线”,点了保存直接丢数据;第二次是监听 online/offline 事件,但没做防抖,切Wi-Fi时触发了七八次;第三次是用了第三方库,结果它偷偷 poll 一个 1×1 的 gif,安卓 WebView 里被拦截了,状态永远不更新。
现在我自己的项目(一个内部协作工具),在线状态逻辑就这几十行,跑了一年多没出过问题。核心就一条:别信 navigator.onLine 的实时性,它只管浏览器是否“认为自己连着网”,和真实网络通不通没关系。真正的判断,得靠「探测 + 缓存 + 降级」三板斧。
下面是我现在固定用的 hook(React):
import { useState, useEffect, useCallback } from 'react';
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
// 探测函数,超时 3s,失败即视为离线
const checkNetwork = useCallback(async () => {
try {
// 用 HEAD 请求避免传输多余数据,目标是轻量、快、不带业务耦合
const res = await fetch('https://jztheme.com/api/health', {
method: 'HEAD',
cache: 'no-store',
headers: { 'X-Check': 'online' },
});
return res.ok;
} catch (e) {
return false;
}
}, []);
// 初始化探测(页面加载时)
useEffect(() => {
const initCheck = async () => {
const status = await checkNetwork();
setIsOnline(status);
};
initCheck();
}, [checkNetwork]);
// 监听原生事件(快速响应系统级切换)
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
// 每 15s 主动探测一次(兜底,防止原生事件漏发或卡住)
useEffect(() => {
const timer = setInterval(async () => {
if (!navigator.onLine) return; // 系统已报告离线,跳过探测
const status = await checkNetwork();
setIsOnline(status);
}, 15000);
return () => clearInterval(timer);
}, [checkNetwork]);
return isOnline;
}
为什么这么写?第一,探测地址必须独立、无副作用。我用 /api/health 是个纯返回 200 的 endpoint,不查数据库、不打日志、不鉴权,哪怕后端崩了它也得尽量返回 200 —— 因为它的唯一使命就是告诉前端“我还能响”。之前试过用业务接口探测,结果某天订单服务挂了,整个应用误判成离线,用户疯狂点刷新还以为自己网坏了。
第二,不依赖单一信号源。原生 online 事件响应快但不准(比如拔网线后要等几秒才触发);主动探测准但有延迟。两者结合,既快又稳。15s 间隔是我压测过的平衡点:太短(5s)增加无效请求;太长(60s)用户断网后长时间没反馈。
第三,不写死重试逻辑。很多教程教你在离线时自动重发请求,但我现在一律交给上层组件处理。因为「要不要重发」「重发几次」「失败后提示什么」,每个业务场景都不一样。这个 hook 只回答一个问题:此刻,我能连上服务器吗?干净,不越界。
这几种错误写法,别再踩坑了
来,列几个我亲手写过、上线后被 PM 打电话问“为啥用户说他明明连着网却显示离线”的反面案例:
- 只监听
online/offline,不做探测:iOS Safari 在后台标签页里会暂停事件,切回去时状态已经滞后;某些 Android 定制 ROM 会直接屏蔽这些事件。我曾经在一台华为 Mate30 上复现过:Wi-Fi 切换瞬间,事件根本没触发。 - 用
fetch('/')探测:首页可能带 SSR、重定向、或者被 CDN 缓存,返回 200 不代表后端活着。更糟的是,有些公司内网环境首页是登录页,未登录时返回 302,你的探测就变成“离线”了。 - 在
useEffect里直接await fetch()且没加 loading 状态:导致组件首次渲染时isOnline是undefined,UI 闪一下空白或报错。后来我改成初始化先读navigator.onLine,再异步覆盖,体验顺多了。 - 探测时没设 timeout:有一次测试环境 DNS 配置错了,fetch 卡了 45 秒才失败,整个页面假死。现在所有探测都加
AbortController(上面代码省略了,实际我写了,但怕篇幅太长没贴全)。
实际项目中的坑
我们有个离线草稿功能,用户断网时输入的内容存在 localStorage,恢复后自动同步。听起来很美,但上线第一天就被吐槽:“我明明开着热点,怎么老弹‘已离线’?”
查了半天,发现是 Chrome 在某些移动热点环境下,navigator.onLine 返回 true,但实际 TCP 连接被运营商劫持或限速,fetch 超时。这时候如果只看 navigator.onLine 就会误判。
解决方案?很简单:把探测请求的 timeout 设成 2.5s,比常规业务请求(8s)短一半。只要探测失败,就立刻标为离线,哪怕 navigator.onLine === true —— 宁可错杀,不可放过。用户感知是“稍等,正在检测网络”,而不是“你已离线,请检查网络”,体验反而更好。
另外提一嘴:PWA 场景下,Service Worker 可能拦截 /api/health 并返回缓存。所以我在 SW 里加了白名单规则:if (request.url.includes('/api/health')) return fetch(request),绝不缓存探测请求。
还有个小细节:微信内置浏览器里,online 事件有时不触发。我们的解法是,在 visibilitychange 事件里补一次探测 —— 用户切回页面时,大概率网络状态也变了,顺手查一下,零成本兜底。
以上是我总结的最佳实践,有更好的方案欢迎评论区交流
这个方案不是完美的。比如在极端弱网(2G+高丢包)下,探测可能频繁失败,导致状态来回跳变。但我们评估过,这种场景下用户本来操作就卡,状态跳变影响远小于数据丢失。所以宁可“保守判定离线”,也不冒险“乐观判定在线”。
如果你的项目对实时性要求极高(比如在线协作文档),那得上 WebSocket 心跳 + 多端探测;如果只是表单提交类应用,这套够用了。没有银弹,只有权衡。
代码里那个 https://jztheme.com/api/health 你替换成自己后端的轻量健康检查接口就行,不用额外开发,一行 Nginx 配置就能搞定。
以上是我踩坑后的总结,希望对你有帮助。

暂无评论