
你有没有遇到过这种情况用户反馈页面点了没反应你远程一看接口请求全部挂掉最后才发现是用户那边断网了。产品经理问你能不能做个“友好提示”前端同学第一反应往往是——这不是后端的事吗实际上纯前端也能实现对网络状态的实时感知而且用到的API非常简单navigator.onLineonline/offline事件。这两个东西就是浏览器原生提供的网络状态通知机制不需要引入任何第三方库也不需要后端配合。这篇文章就把这个能力彻底讲透从浏览器底层是怎么通知前端断网和联网的到具体怎么监听、怎么结合业务做断网提示、请求失败重试、甚至做轻量级的数据同步占位再到开发中容易踩的坑比如iOS的兼容差异、localhost环境的误导最后给出一套可以直接复制到项目里用的完整封装方案。如果你是初级前端这属于面试常考的“网络状态合成”知识点如果你已经在写业务代码这套实战方案能直接提升你的交付质量。1. 网络状态检测的整体设计思路1.1 浏览器为什么需要网络状态检测先说一个直觉层面的问题浏览器本身是个客户端软件它每天都在收发HTTP请求按理说它应该是第一个知道“网络断了”的角色。但长期以来前端开发者几乎从不关心这个能力原因很简单——大家的认知里网络状态是后端和运维的事前端只负责把请求发出去失败了就报个错。但实际业务场景中前端对网络状态的感知能解决不少体验问题。举几个例子用户正在提交一个很长的表单写到一半断网了点提交按钮后请求一直转圈。如果前端能感知断网可以直接提示“网络已断开请检查网络连接”而不是让用户傻等超时。SPA应用需要维护登录态断网期间用户的token虽然还在但所有请求都会失败。此时如果前端能自动进入“离线模式”减少无效请求恢复联网后自动刷新数据体验会好很多。在协同编辑、在线文档、聊天工具这类实时性要求高的应用中断网瞬间给出提示、恢复联网后自动重连并补发消息这些都是核心体验的一部分。1.2 navigator.onLine 和 online/offline 事件的协作关系浏览器提供了两个东西来支撑前端感知网络状态一个是属性navigator.onLine一个是事件online和offline。前者是“当前快照”后者是“状态变更通知”。你可以这样类比navigator.onLine相当于你手机顶部的信号图标你随时切到桌面看一眼就知道现在有没有网而online/offline事件相当于运营商给你的短信通知——“您的网络已断开”“您的网络已恢复”。属性拿来被动查询事件拿来主动响应两者配合起来就能实现完整的状态管理。这里必须标注一个关键术语网络状态合成Network State Synthesis。这是浏览器层面做的一件事——它把操作系统层面的网络接口状态有线、Wi-Fi、蜂窝数据汇总成一个简单的布尔值。正常情况下只要系统有一个网络接口处于连接状态navigator.onLine就是true。但这个布尔值有一个很让前端头疼的“盲区”它只判断“有没有网络接口连接”不判断“能不能访问互联网”。你连着路由器但路由器没拨号navigator.onLine照样是true。这个特性后面的实战环节会专门处理。1.3 方案选型为什么首选原生API而不是第三方库市面上有一些第三方的网络状态检测库比如offline.js、netinfo之类的也有人自己用fetch轮询去探测服务器。我在项目里首选的方案是原生API原因有三个零依赖、零体积。navigator.onLine和事件监听是浏览器内置能力不需要额外引入任何包。前端项目动不动就是几百KB的依赖能减一个是一个。响应速度快。浏览器层面的网络接口状态变化能实时抛事件出来通常毫秒级。而自己写轮询探测最快也得几秒钟才能确认一次而且会产生大量无效请求。行为可预测。原生API的语义就是“网络接口连接状态”不掺入业务逻辑。第三方库往往加入了自己对“可用性”的判断反而容易和真实场景有偏差。但原生API也有短板最大的就是上面提到的“局域网在线但互联网不通”场景。这个盲区靠原生API解决不了必须在业务层配合一次“真实请求探测”来做二次确认。这也是我这套方案里比较核心的设计——事件驱动状态 请求探测兜底。2. 核心原理与兼容性细节解析2.1 浏览器的网络状态判定机制既然做实战就要先把原理搞透。浏览器怎么判断“网络接口连接状态”以 Chrome 为例它监听操作系统层的网络接口变化。当系统网络接口有线、Wi-Fi、蜂窝断开或连接时操作系统会发通知给浏览器浏览器再把状态映射到navigator.onLine属性上同时触发offline或online事件。这里有一个很重要的细节所谓“接口连接”指的是物理链路层和数据链路层通了比如你插了网线、连上了Wi-Fi、手机开了流量。它不探测 DNS 是否能解析、不探测服务器是否能响应。所以下面的场景里navigator.onLine一直是true路由器断网但设备连着路由器的 Wi-Fi。插着网线但宽带欠费停机。手机连着 Wi-Fi 网络但 Wi-Fi 本身没有外网。理解了这一点你就明白为什么单纯依靠navigator.onLine做业务判断是不够的。它的可靠性只在一种场景下是高的——网线被拔、Wi-Fi 断开、飞行模式开启这一类产生“接口级变化”的操作。2.2 online/offline 事件的浏览器兼容性差异这两个事件是 HTML5 标准的一部分现代浏览器支持度都很好但不同平台的表现还是有差异。这里整理一个我在实际项目中验证过的兼容性速查表平台/浏览器navigator.onLine初始值offline 事件触发时机备注ChromeWindows/macOS网络正常时为true网络接口断开时触发相对可靠但同样不探测互联网连通性Firefox网络正常时为true断网后延迟触发延迟时间不固定有时会滞后几秒SafarimacOS网络正常时为true行为不一致部分版本事件触发不稳定iOS Safari / WKWebView网络正常时为trueWi-Fi断开或蜂窝关闭时触发事件监听需要挂载在window上且navigator.onLine在部分版本上有延迟更新问题Android Chrome网络正常时为true网络切换时触发频繁从4G切到Wi-Fi时可能触发 offline 再 online微信内置浏览器iOS基本正常行为不一致依赖 iOS 系统 WebView同样有不稳定问题为了照顾面试场景我把兼容性这个知识点多说一句iOS Safari 在监听 online/offline 事件时在部分版本上存在事件不触发或者触发延迟的问题所以在移动端 WebView 里不能把原生事件作为唯一的数据源需要配合心跳探测或可见性变化兜底。这个是面试官比较爱追问的一个点。2.3 事件挂载位置window vs document vs navigator很多初学者会在这三个对象上纠结。按照标准online和offline事件挂在window上但navigator.onLine属性属于Navigator接口。实际开发中我在所有现代浏览器上都是监听window对象的online/offline都可以正常工作。但有一个容易踩的坑在部分旧版 Firefox 中监听document上的online/offline事件也能触发但标准推荐window。如果两者都挂载可能出现事件重复处理。所以统一规范就是——只监听window不做双挂载。这点我会在后面的封装模块里给出固定写法。3. 实战从零封装一个可靠的网络状态检测模块3.1 基础版事件监听 状态获取先写一个最基础的能力验证版本把事件监听跑通。这个版本适合新手理解事件机制也适合在控制台手动测试。// 获取当前网络状态 console.log(当前网络状态, navigator.onLine ? 在线 : 离线); // 监听离线事件 window.addEventListener(offline, () { console.log(网络已断开); }); // 监听在线事件 window.addEventListener(online, () { console.log(网络已恢复); });这段代码放到页面的任何 JavaScript 环境里都能跑。你打开浏览器控制台然后拔网线或者断Wi-Fi就能看到控制台输出。如果你在本地开发环境测试建议直接用 Chrome 开发者工具里的 Network 面板把网络切换成 Offline效果等同。这一步没什么难度但你要注意一个细节事件绑定之后如果页面在断网状态下加载navigator.onLine初始就是false或true事件不会回放历史状态。也就是说页面加载时就要先读取一次navigator.onLine作为初始状态然后再靠事件去更新。3.2 进阶版状态管理模块基础版只能证明“能监听”但真正用到项目里必须有状态管理。我的做法是封装一个networkStatus模块统一管理状态订阅和事件回调。下面是可直接参考的实现class NetworkStatus { constructor() { this.onLine navigator.onLine; this.listeners new Set(); this.init(); } init() { window.addEventListener(online, this.handleOnline); window.addEventListener(offline, this.handleOffline); document.addEventListener(visibilitychange, this.handleVisibilityChange); } handleOnline () { this.updateStatus(true); }; handleOffline () { this.updateStatus(false); }; // 页面从后台切回前台时重新确认一次网络状态 handleVisibilityChange () { if (!document.hidden) { this.checkNetwork(); } }; // 主动检查读取 navigator.onLine 并通知 checkNetwork () { this.updateStatus(navigator.onLine); }; updateStatus(status) { const changed this.onLine ! status; this.onLine status; if (changed) { this.emit(status); } } // 订阅状态变化 subscribe(callback) { this.listeners.add(callback); return () this.listeners.delete(callback); } emit(status) { this.listeners.forEach((callback) callback(status)); } destroy() { window.removeEventListener(online, this.handleOnline); window.removeEventListener(offline, this.handleOffline); document.removeEventListener(visibilitychange, this.handleVisibilityChange); this.listeners.clear(); } } // 使用示例 const networkStatus new NetworkStatus(); networkStatus.subscribe((onLine) { console.log(onLine ? 网络恢复 : 网络断开); });这个模块有几个设计点值得说明状态变更才通知。updateStatus里做了changed判断避免重复触发无意义的回调。事件回调统一处理。不管事件从哪来online/offline/visibilitychange最终都汇入updateStatus逻辑单一。visibilitychange兜底。移动端 WebView 里页面从后台切回前台时网络状态可能已经悄悄变了比如切了Wi-Fi所以切回来时主动读取一次navigator.onLine是最稳妥的。提供destroy方法。在组件卸载或页面卸载时移除事件监听防止内存泄漏。3.3 增强版结合请求探测解决“假在线”问题前面已经说到navigator.onLine在没有接口级断网时不会变false但业务上已经“不通了”。这种场景怎么处理我的方案是把事件驱动和请求探测结合。逻辑是——当navigator.onLine是true但某个关键接口请求失败时判定为“网络不可用”当navigator.onLine是false直接判定断网。同时在断网恢复后主动发一个探测请求确认网络真正可用后再通知业务层。核心思路可以用一句话概括事件告诉你“接口可能变了”请求告诉你“业务到底通不通”。两者都要各自负责各自能确定的部分。class ReliableNetworkStatus { constructor({ detectUrl /api/health, timeout 5000 } {}) { this.onLine navigator.onLine; this.detectUrl detectUrl; this.timeout timeout; this.listeners new Set(); this.pendingPromises new Set(); this.init(); } init() { window.addEventListener(online, this.handleOnline); window.addEventListener(offline, this.handleOffline); document.addEventListener(visibilitychange, this.handleVisibilityChange); } handleOnline () { // 浏览器说恢复了但未必能访问互联网发个探测请求确认 this.updateConnectivity(); }; handleOffline () { this.setStatus(false); }; handleVisibilityChange () { if (!document.hidden) { this.updateConnectivity(); } }; // 请求探测 async updateConnectivity() { this.setStatus(await this.probe()); } // 实际探测请求带超时成功返回 true失败返回 false probe() { return new Promise((resolve) { const controller new AbortController(); const timer setTimeout(() controller.abort(), this.timeout); fetch(this.detectUrl, { method: GET, cache: no-store, signal: controller.signal, }) .then((response) { clearTimeout(timer); resolve(response.ok); }) .catch(() { clearTimeout(timer); resolve(false); }); }); } // 业务层主动上报比如某个接口请求失败了 reportFailure() { if (this.onLine) { this.updateConnectivity(); } } setStatus(status) { const changed this.onLine ! status; this.onLine status; if (changed) { this.emit(status); } } subscribe(callback) { this.listeners.add(callback); return () this.listeners.delete(callback); } emit(status) { this.listeners.forEach((callback) callback(status)); } destroy() { window.removeEventListener(online, this.handleOnline); window.removeEventListener(offline, this.handleOffline); document.removeEventListener(visibilitychange, this.handleVisibilityChange); this.listeners.clear(); } }这段代码里有两个关键点要展开讲一下第一探测请求选择什么地址。最理想的是用一个不依赖业务逻辑的轻量级接口比如后端提供的健康检查接口/api/health数据量小、响应快。但如果你不想麻烦后端可以请求一个静态资源比如/favicon.ico或一张 1x1 的透明图片只要服务器能响应即可。注意cache: no-store这个参数要确保每次请求都真实发到服务器而不是被浏览器缓存直接命中——否则你探测的是缓存不是网络。第二用一个 Promise 来模拟超时而不是 setTimeout 暴力抛弃请求。这里用了AbortController来真正中断请求可以避免超时后请求还在后台挂起浪费连接资源。3.4 实战案例断网提示 请求重放封装好了底层模块接下来看怎么落地到业务里。我拿一个常见的“表单提交 列表刷新”场景来做说明。比如你的页面有一个提交按钮用户编辑完内容后点击提交。如果此时已经断网fetch请求会挂起很久才报错用户体验非常糟糕。有了网络状态模块可以这样处理const network new ReliableNetworkStatus(); // 提交按钮点击事件 async function handleSubmit(data) { // 先检查当前状态 if (!network.onLine) { ToastService.show(网络已断开请恢复网络后重试); return; } try { const response await fetch(/api/data, { method: POST, body: JSON.stringify(data), }); if (!response.ok) { throw new Error(提交失败); } ToastService.show(提交成功); } catch (error) { // 请求失败时主动上报给网络状态模块 network.reportFailure(); ToastService.show(网络异常请检查网络连接); } } // 断网提示 network.subscribe((onLine) { if (onLine) { ToastService.show(网络已恢复); refreshPageData(); } else { ToastService.show(网络已断开部分功能不可用); } });这个方案的核心价值是断网时提前拦截联网后自动恢复。用户不需要手动刷新页面从断网状态恢复后数据能自动刷新表单能重新提交。这在移动端弱网场景下尤其有用。3.5 扩展场景离线队列与数据同步再往前一步如果业务允许可以把断网期间用户的操作缓存下来恢复后自动补发。这在复杂的业务系统里是相对重量级的设计但基础思路并不复杂let offlineQueue []; // 断网时把操作塞进队列 network.subscribe((onLine) { if (onLine) { flushQueue(); } else { console.log(网络断开操作将缓存); } }); function enqueueOperation(operation) { if (!network.onLine) { offlineQueue.push(operation); // 这里可以把队列持久化到 localStorage localStorage.setItem(offline_queue, JSON.stringify(offlineQueue)); } } async function flushQueue() { if (offlineQueue.length 0) return; const pending JSON.parse(localStorage.getItem(offline_queue) || []); offlineQueue []; localStorage.removeItem(offline_queue); for (const op of pending) { try { await fetch(op.url, { method: op.method, body: op.body, }); } catch (error) { console.error(补发失败, op, error); // 补发失败塞回队列尾部等下一次恢复重试 offlineQueue.push(op); } } if (offlineQueue.length 0) { localStorage.setItem(offline_queue, JSON.stringify(offlineQueue)); } }这里要注意离线队列不能无限积压要在模块里加一个最大长度限制防止用户长时间离线导致本地存储爆掉。我一般限制在50~100条操作超过就直接丢弃并提示用户。4. 常见坑点与排查技巧实录4.1 本地开发环境里的误导这是新手最容易踩的坑。在 localhost 环境下调试时navigator.onLine几乎永远是true即使你把电脑的网线拔了。为什么你访问 localhost 时实际走的是回环地址即127.0.0.1。请求在操作系统内部直接完成不经过真实的网络接口。浏览器认为系统有网络接口连接Wi-Fi 或以太网即使这个接口连不上外网navigator.onLine仍为true。所以你在本地调试时想测断网效果直接断网线或关Wi-Fi是不够的建议用 Chrome DevTools 的 Network 面板里的 Offline 模式。但要注意DevTools 的 Offline 只是模拟“网络请求全失败”它不会真正触发offline事件。所以你要测online/offline事件必须用真实断网或者用系统级网络切换。这个矛盾怎么破我的经验是断网事件用“拔Wi-Fi/断网线”来测网络请求失败用 DevTools Offline 来测。两者分开测不容易被误导。4.2 iOS WebView 里的事件不触发在 iOS 的 WKWebView 环境里online/offline事件表现得相当不靠谱。我自己实测过在部分 iOS 版本中从 Wi-Fi 切换到蜂窝网络时offline事件偶尔不触发。用navigator.onLine读取状态有时会有几秒钟的延迟。在锁屏后解锁、页面从后台切回前台时navigator.onLine可能长时间保持旧值。所以移动端项目尤其是嵌入 App 的 H5 页面不能完全依赖原生事件。我给出的方案是用visibilitychange监听页面恢复并配合真实请求探测来纠正状态。上面的ReliableNetworkStatus已经做了这一步实测下来基本可以覆盖 iOS 上的状态偏差。4.3 探测请求自身的坑探测请求看起来简单但有很多细节容易被忽视超时时间设得太短。弱网环境下一个健康检查接口可能也要 2~3 秒才返回如果超时设 2 秒可能会把正常网络误判为断网。我一般设 5 秒。接口地址选错。如果你用第三方接口比如https://www.baidu.com做探测会触发跨域问题、DNS解析慢、甚至被运营商劫持。尽量用同源的轻量接口。请求缓存命中。如果探测接口的响应头带了强缓存浏览器可能直接返回缓存的响应导致即使断网了也能“成功”。所以探测请求里必须有cache: no-store。高频探测导致服务端压力。不要在断网期间每秒发一次探测请求要加节流。我的做法是断网状态下探测间隔至少10秒以上或者干脆等online事件触发后再探测。4.4 面试版问题速查表问题参考答案摘要navigator.onLine能判断真正能上外网吗不能它只判断网络接口是否连接不判断互联网是否可达offline事件什么时候触发系统网络接口断开时浏览器收到操作系统通知后触发如何判断用户真正连不上互联网用真实请求探测发一个轻量请求看是否成功online事件触发后能立刻发请求吗不一定事件只表示接口恢复DNS/路由/认证可能还没就绪建议延迟几百毫秒再探测online/offline事件挂在哪里标准是window对象事件触发顺序是什么通常先改navigator.onLine的值再触发事件但不保证所有浏览器都如此5. 进阶网络状态合成与工程化实践5.1 什么是网络状态合成标题热搜词里出现了“网络状态合成”顺带讲一下这个概念。所谓网络状态合成就是浏览器把多种网络相关的信号源汇总成一个统一的、供开发者使用的状态。目前浏览器里能拿到的信号源有navigator.onLine网络接口是否连接。online/offline事件接口状态变化通知。navigator.connectionNetwork Information API提供网络类型wifi、cellular之类、下行速度、RTT等信息。fetch/XHR的实际请求结果。把这些信号整合成一个“业务可用的网络状态”的过程就是网络状态合成。业务层不关心底层是 Wi-Fi 还是 4G只需要知道“现在能不能发请求”“请求失败了是网络问题还是接口问题”。我在生产项目里做过一款比较完整的合成逻辑大致是这样的状态机在线navigator.onLine true且最近一次探测请求成功。离线navigator.onLine false。弱网/未知navigator.onLine true且有请求失败记录但探测请求未完成。恢复中从离线状态收到online事件正在探测中。这四个状态分别对应不同的 UI 提示和业务逻辑。比如“弱网/未知”状态只是降低并发请求不阻断操作“离线”状态则直接禁用提交按钮“恢复中”状态显示加载动画。5.2 工程化全局的状态管理与组件库封装如果是在大项目里用我建议把网络状态模块放到状态管理库里Vuex/Pinia/Redux让所有组件都能响应。以 Vue 3 Pinia 为例可以这样组织// stores/network.js import { defineStore } from pinia; import { ReliableNetworkStatus } from /utils/networkStatus; export const useNetworkStore defineStore(network, { state: () ({ onLine: navigator.onLine, status: unknown, // online | offline | recovering }), actions: { init() { this.network new ReliableNetworkStatus({ detectUrl: /api/health, }); this.network.subscribe((onLine) { this.onLine onLine; this.status onLine ? online : offline; }); }, reportFailure() { this.network.reportFailure(); }, }, });组件里就能直接响应式地处理状态script setup import { storeToRefs } from pinia; import { useNetworkStore } from /stores/network; const networkStore useNetworkStore(); const { onLine } storeToRefs(networkStore); /script template div v-if!onLine classoffline-banner 网络已断开当前操作将被缓存恢复后自动重试 /div /template这个设计的好处是网络状态变成了全局的响应式数据任何组件都能感知不需要到处传回调。5.3 用异常上报监控真实断网率最后提一个偏工程化的小技巧把断网事件上报到监控系统。很多团队在产品上线后根本不知道用户端的断网情况有多严重直到看到大量请求失败告警才后知后觉。有了offline事件和数据上报你可以统计出每天有多少用户经历断网。断网平均持续时间。哪个页面断网率最高可以给页面标签加context信息。移动端和桌面端的断网率差异。上报的方式很简单把断网开始和结束的时间戳发给后端或埋点平台let offlineStartTime null; network.subscribe((onLine) { if (onLine) { if (offlineStartTime) { const duration Date.now() - offlineStartTime; trackEvent(network_recovered, { duration }); offlineStartTime null; } } else { offlineStartTime Date.now(); trackEvent(network_offline, { page: window.location.pathname }); } });这些数据对产品决策很有价值比如如果你的应用在移动端断网率特别高产品团队可能会考虑多做一些离线缓存而不只是加个提示。6. 收尾我做这套方案时踩过的坑前面技术细节讲了不少最后分享几个我在实际项目中真实的体会。第一不要在断网恢复后立刻发重试请求。我第一次做自动重试时监听online事件后立刻去发待处理队列里的请求结果一半都失败。原因是网络接口刚恢复时DHCP 分配 IP、DNS 解析、代理握手这些动作都还没完成这时候发请求大概率还是失败。后来我调整逻辑online事件触发后先等 1~2 秒再做一次探测请求探测成功后才开始重放队列成功率一下子提升到了接近 100%。这个 1~2 秒的等待对用户体验几乎没有影响但换来的可靠性提升非常明显。第二排查问题时要看清是“事件没触发”还是“状态没更新”。有段时间我发现一个页面断网后 UI 提示不出现排查了很久最后发现是事件触发了但navigator.onLine的值还是旧的。这类问题尤其在 iOS 上出现过。所以调试时不要只看事件有没有触发也要定时检查navigator.onLine的实时值两者配合才能定位问题。第三别让网络状态模块承担太多职责。有些人会把“所有请求失败的错误处理”都塞进网络状态模块里导致模块越来越重最后连业务接口的404、500错误都当成断网来处理逻辑乱成一团。我的原则是网络状态模块只负责“判定网络是否可用”和“通知状态变化”具体的错误重试、UI切换、业务补偿都让业务层去响应。模块保持单一职责扩展性就好得多。这套方案我在移动端H5和PC端管理后台里都落地过代码量不大但产出的体验提升非常明显。如果你正在做类似的功能可以先把基础版跑通再按业务需要逐步叠加请求探测、离线队列和状态上报。如果有测试环境建议多试几种真实的断网方式拔网线、断Wi-Fi、开飞行模式、拔路由器WAN口线你会发现不同场景下事件触发的时机差异很大看完这篇再动手踩坑会少很多。