断线重连与离线处理
场景
用户在弱网或无网络环境下使用应用(如 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 断线重连需要注意什么?
答案:
- 指数退避:避免频繁重连导致服务端压力
- 最大次数限制:超过后停止重连并提示用户
- 心跳机制:定期发送 ping 检测连接是否真正存活
- 恢复状态:重连后需要重新订阅/同步数据(如拉取离线消息)
- visibilitychange:后台 Tab 可暂停重连,回到前台再恢复
Q2: navigator.onLine 可靠吗?
答案:
不完全可靠。navigator.onLine 只检测设备是否连接了网络,不能检测网络是否真正可达。可能出现"有 WiFi 但无法上网"的情况。
更可靠的方案:
- 定期向服务端发心跳 ping
- 使用 fetch 并设超时来探测实际连通性
navigator.onLine作为快速判断的第一道防线