网络状态监听与模拟

离线优先应用的核心假设是“网络随时可能中断”,因此应用必须能够实时感知网络状态变化并做出正确响应。本章聚焦前端侧的网络状态监听机制:navigator.onLine 属性、Network Information API 以及 online/offline 事件,并说明如何在集成测试中模拟这些状态变化。

4.1 核心原理

浏览器提供三种互补的网络状态感知方式:

机制 类型 能力 局限性
navigator.onLine 同步属性 布尔值,表示当前是否连接网络 仅反映“是否有连接”,不区分网络质量;Android Chrome 中可能不准确
online / offline 事件 事件驱动 在连接状态变化时触发,可在 window 或 document 上监听 事件触发时机可能延迟;无法获取当前网络类型
Network Information API
(navigator.connection)
异步 + 事件 提供 effectiveType、downlink、rtt、saveData,并派发 change 事件 并非所有浏览器支持(Safari 不支持);需要处理 polyfill

关键原则:不应依赖 navigator.onLine 做单次检查,而应组合使用事件监听与 Network Information API 实现全面覆盖。

4.2 实现流程

  1. 初始化检测:在应用启动时读取 navigator.onLine 确定初始状态,同时注册 online/offline 事件监听。
  2. 事件响应:在事件回调中更新全局状态(如 Redux store、Context),触发 UI 重新渲染。
  3. 网络质量感知:若浏览器支持 navigator.connection,读取 effectiveType('slow-2g'、'2g'、'3g'、'4g')并监听 change 事件,动态调整资源加载策略。
  4. 测试模拟:在集成测试中,使用 Chrome DevTools 或 Playwright / Puppeteer 的 CDP 命令模拟离线、慢速网络。
  5. 清理:在组件卸载或页面切换时移除事件监听,防止内存泄漏。

4.3 代码实现示例

基础网络状态监听器

// networkStatus.js
let isOnline = navigator.onLine;
let effectiveType = 'unknown';

function getConnection() {
  return navigator.connection || navigator.mozConnection || navigator.webkitConnection;
}

function updateNetworkStatus() {
  isOnline = navigator.onLine;
  const conn = getConnection();
  if (conn) {
    effectiveType = conn.effectiveType; // 'slow-2g','2g','3g','4g'
  }
  console.log(`Network status: ${isOnline ? 'online' : 'offline'}, type: ${effectiveType}`);
  // 触发自定义事件或更新状态管理
  window.dispatchEvent(new CustomEvent('networkchange', {
    detail: { isOnline, effectiveType }
  }));
}

// 初始化
updateNetworkStatus();

// 监听浏览器事件
window.addEventListener('online', updateNetworkStatus);
window.addEventListener('offline', updateNetworkStatus);

// 监听 Network Information API 变化
const conn = getConnection();
if (conn) {
  conn.addEventListener('change', updateNetworkStatus);
}

// 导出供测试清理
export function cleanup() {
  window.removeEventListener('online', updateNetworkStatus);
  window.removeEventListener('offline', updateNetworkStatus);
  if (conn) conn.removeEventListener('change', updateNetworkStatus);
}
测试提示:在 Jest / Vitest 单元测试中,可使用 Object.defineProperty(navigator, 'onLine', { value: false }) 模拟离线,但更推荐在集成测试中使用浏览器原生模拟工具。

4.4 调试与模拟

Chrome DevTools 提供最直接的离线模拟:打开 DevTools > Network 标签 > 勾选 Offline。也可通过下拉菜单选择预设网络条件(Slow 3G、Fast 3G 等):

// 使用 Puppeteer 编程模拟
await page.evaluate(() => {
  // 用 CDP 模拟网络 (实际使用 page.emulateNetworkConditions)
});
// Playwright 示例
await page.context().setOffline(true);
// 或使用 CDP 会话自定义延迟
const cdp = await page.context().newCDPSession(page);
await cdp.send('Network.emulateNetworkConditions', {
  offline: false,
  latency: 300,
  downloadThroughput: 500 * 1024, // 500Kbps
  uploadThroughput: 100 * 1024
});

常用模拟场景列表:

  • 完全离线
  • 不可靠网络(间歇断开)
  • 慢速 2G (50Kbps)
  • 慢速 3G (750Kbps)
  • 固定延迟(如 300ms)
  • 丢包率模拟(需通过代理工具如 Charles)

4.5 常见错误与陷阱

错误 后果 解决方案
仅依赖 navigator.onLine 同步判断 在某些系统(如 macOS Chrome)离线时仍返回 true,导致缓存逻辑失效 结合事件监听,并定期发送心跳请求确认可达性
事件监听重复注册 触发多次回调,引发重复状态更新或死循环 使用单例模式;在 React 中通过 useEffect 的清理函数移除监听
未处理 Safari 的兼容性 在 iOS 上无法获取连接类型,无 change 事件 检查 navigator.connection 是否存在,并提供 fallback
测试中模拟离线后未恢复 后续测试用例仍处于离线状态,导致误报失败 在 beforeEach / afterEach 中重置网络条件

4.6 性能与稳定性优化

  • 节流高频事件:部分浏览器可能在网络抖动时频繁触发 change 或 online/offline 事件,建议使用 requestAnimationFrame 或 setTimeout(fn, 500) 合并响应。
  • 延迟初始化:如果 navigator.connection 的读取开销较大(极少情况),可延迟到应用首次需要网络质量信息时再绑定。
  • 跨页面保持状态:对于 SPA,全局状态(如 Redux isOnline)应在路由切换时不重置;对于 MPA,可用 ServiceWorker 的 MessageChannel 传递状态。
  • 自动化测试稳定性:使用 Playwright 的 page.waitForEvent('offline') 确保事件已触发后再断言 UI 变化,避免竞态条件。

4.7 网络状态监听流程

网络状态监听与事件触发流程 navigator.onLine online/offline事件 Network Information API DevTools / CDP 模拟 状态更新 (React/Vue store、自定义事件) UI / 缓存策略调整 集成测试通过模拟网络条件 + 监听事件,验证从离线到在线或反之的完整链路
稳定性警告:部分低端 Android 设备在硬件网络切换(WiFi → 4G)时可能不触发 offline 事件,建议应用层加入心跳探活(如每 30s 发 HEAD 请求)作为补充。