跳到主要内容

断线重连与离线处理

场景

用户在弱网或无网络环境下使用应用(如 IM 聊天、在线文档),如何保证核心功能可用并在网络恢复时自动恢复?

方案设计

1. 网络状态检测

network-monitor.ts
class NetworkMonitor {
private listeners = new Set<(online: boolean) => void>();

constructor() {
window.addEventListener('online', () => this.notify(true));
window.addEventListener('offline', () => this.notify(false));
}

get isOnline(): boolean {
return navigator.onLine;
}

onChange(callback: (online: boolean) => void): () => void {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}

private notify(online: boolean) {
this.listeners.forEach((cb) => cb(online));
}
}

export const networkMonitor = new NetworkMonitor();

2. WebSocket 断线重连

reconnect-ws.ts
class ReconnectWebSocket {
private ws: WebSocket | null = null;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private attempt = 0;
private maxAttempts = 10;
private baseDelay = 1000;

constructor(private url: string) {
this.connect();
}

private connect() {
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
console.log('WebSocket connected');
this.attempt = 0; // 重置重连计数
};

this.ws.onclose = (event) => {
if (!event.wasClean) {
this.scheduleReconnect();
}
};

this.ws.onerror = () => {
this.ws?.close();
};
}

private scheduleReconnect() {
if (this.attempt >= this.maxAttempts) {
console.error('Max reconnection attempts reached');
return;
}
// 指数退避 + 抖动
const delay = Math.min(
this.baseDelay * 2 ** this.attempt + Math.random() * 1000,
30000
);
this.attempt++;
console.log(`Reconnecting in ${delay}ms (attempt ${this.attempt})`);
this.reconnectTimer = setTimeout(() => this.connect(), delay);
}

// 网络恢复时立即重连
onNetworkRestore() {
if (this.ws?.readyState !== WebSocket.OPEN) {
this.attempt = 0;
this.connect();
}
}

destroy() {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
this.ws?.close();
}
}

3. 离线请求队列

offline-queue.ts
interface QueueItem {
url: string;
init: RequestInit;
resolve: (value: Response) => void;
reject: (reason: unknown) => void;
}

class OfflineQueue {
private queue: QueueItem[] = [];

async fetch(url: string, init?: RequestInit): Promise<Response> {
if (navigator.onLine) {
return fetch(url, init);
}

// 离线时放入队列
return new Promise<Response>((resolve, reject) => {
this.queue.push({ url, init: init ?? {}, resolve, reject });
});
}

// 网络恢复时批量发送
async flush() {
const items = [...this.queue];
this.queue = [];

for (const item of items) {
try {
const response = await fetch(item.url, item.init);
item.resolve(response);
} catch (error) {
item.reject(error);
}
}
}
}

// 监听网络恢复
const offlineQueue = new OfflineQueue();
window.addEventListener('online', () => offlineQueue.flush());

4. 离线数据持久化

offline-storage.ts
// 用 IndexedDB 存储离线数据
async function saveOfflineAction(action: { type: string; payload: unknown }) {
const db = await openDB('offline-actions', 1, {
upgrade(db) {
db.createObjectStore('actions', { keyPath: 'id', autoIncrement: true });
},
});
await db.add('actions', { ...action, timestamp: Date.now() });
}

// 网络恢复后同步
async function syncOfflineActions() {
const db = await openDB('offline-actions', 1);
const actions = await db.getAll('actions');
for (const action of actions) {
await sendToServer(action);
await db.delete('actions', action.id);
}
}

常见面试问题

Q1: WebSocket 断线重连需要注意什么?

答案

  1. 指数退避:避免频繁重连导致服务端压力
  2. 最大次数限制:超过后停止重连并提示用户
  3. 心跳机制:定期发送 ping 检测连接是否真正存活
  4. 恢复状态:重连后需要重新订阅/同步数据(如拉取离线消息)
  5. visibilitychange:后台 Tab 可暂停重连,回到前台再恢复

Q2: navigator.onLine 可靠吗?

答案

不完全可靠navigator.onLine 只检测设备是否连接了网络,不能检测网络是否真正可达。可能出现"有 WiFi 但无法上网"的情况。

更可靠的方案:

  • 定期向服务端发心跳 ping
  • 使用 fetch 并设超时来探测实际连通性
  • navigator.onLine 作为快速判断的第一道防线

相关链接