SuperAgent 浏览器端网络状态检测:离线时的请求队列处理

SuperAgent 浏览器端网络状态检测:离线时的请求队列处理

【免费下载链接】superagent Ajax for Node.js and browsers (JS HTTP client). Maintained for @forwardemail, @ladjs, @spamscanner, @breejs, @cabinjs, and @lassjs. 【免费下载链接】superagent 项目地址: https://gitcode.com/gh_mirrors/su/superagent

在现代 Web 应用开发中,用户对网络稳定性的依赖与日俱增。然而,网络连接的不稳定性(如地铁、弱信号区域等场景的信号中断)常导致请求失败,影响用户体验。SuperAgent 作为一款轻量级的 JavaScript HTTP 客户端(HTTP Client),虽未内置完整的离线请求队列功能,但通过其灵活的 API 和事件机制,可构建可靠的离线请求处理系统。本文将详细介绍如何基于 SuperAgent 实现浏览器端网络状态检测、请求队列管理及离线数据同步,解决网络波动带来的数据一致性问题。

网络状态检测机制

浏览器提供了原生的网络状态 API,可实时监测网络连接变化。结合 SuperAgent 的错误处理机制,能精准识别离线场景并触发相应处理逻辑。

浏览器网络 API 应用

使用 navigator.onLine 属性和 online/offline 事件可实现基础网络状态监听:

// 初始网络状态检查
const isOnline = () => navigator.onLine;

// 网络状态变化监听
window.addEventListener('online', () => {
  console.log('网络已恢复,同步离线请求队列');
  syncOfflineQueue();
});

window.addEventListener('offline', () => {
  console.log('网络已断开,启用离线请求队列');
});

SuperAgent 错误类型识别

SuperAgent 在网络错误时会抛出特定异常,可通过错误信息判断是否为离线状态。在 src/client.js 中定义了跨域错误处理逻辑:

// 源自 SuperAgent 源码:src/client.js 第 655 行
Request.prototype.crossDomainError = function () {
  const error = new Error(
    'Request has been terminated\nPossible causes: the network is offline, Origin is not allowed by Access-Control-Allow-Origin, the page is being unloaded, etc.'
  );
  error.crossDomain = true;
  this.callback(error);
};

通过错误信息中的 "network is offline" 关键字,可识别离线场景:

request.get('/api/data')
  .end((err, res) => {
    if (err && err.message.includes('network is offline')) {
      // 处理离线错误
      queueRequest({
        method: 'GET',
        url: '/api/data',
        callback: (res) => console.log('恢复后的数据:', res)
      });
    }
  });

请求队列核心设计

离线请求队列需解决三个关键问题:请求存储、优先级管理和恢复策略。采用 IndexedDB 进行持久化存储,结合队列数据结构实现高效管理。

队列数据结构设计

class RequestQueue {
  constructor() {
    this.queue = [];
    this.priorityMap = { 'GET': 1, 'POST': 2, 'PUT': 3, 'DELETE': 4 };
  }

  // 添加请求到队列,按方法优先级排序
  enqueue(request) {
    request.timestamp = Date.now();
    this.queue.push(request);
    this.queue.sort((a, b) => {
      // 优先级高的请求先执行
      if (this.priorityMap[a.method] !== this.priorityMap[b.method]) {
        return this.priorityMap[b.method] - this.priorityMap[a.method];
      }
      // 相同优先级按时间排序
      return a.timestamp - b.timestamp;
    });
    this.persistQueue(); // 持久化到 IndexedDB
  }

  // 获取下一个待执行请求
  dequeue() {
    const request = this.queue.shift();
    this.persistQueue();
    return request;
  }

  // 清空队列
  clear() {
    this.queue = [];
    this.persistQueue();
  }

  // 持久化队列到 IndexedDB
  persistQueue() {
    // 实现 IndexedDB 存储逻辑
  }
}

离线请求存储方案

使用 IndexedDB 而非 localStorage 存储请求,原因如下:

  • 支持更大存储容量(通常为 50MB 以上)
  • 提供事务支持,确保数据一致性
  • 可存储复杂对象,无需手动序列化
// IndexedDB 存储实现示例
class OfflineStorage {
  constructor() {
    this.dbName = 'superagent-offline-queue';
    this.storeName = 'requests';
  }

  // 打开数据库连接
  open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1);
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName, { keyPath: 'id', autoIncrement: true });
        }
      };

      request.onsuccess = (event) => {
        this.db = event.target.result;
        resolve(this.db);
      };

      request.onerror = (event) => reject(event.target.error);
    });
  }

  // 保存请求到数据库
  saveRequest(request) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], 'readwrite');
      const store = transaction.objectStore(this.storeName);
      const request = store.add({
        method: request.method,
        url: request.url,
        data: request.data,
        timestamp: Date.now(),
        retries: 0
      });

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}

SuperAgent 请求拦截与封装

通过封装 SuperAgent 的请求方法,实现请求自动入队、网络状态检测和错误恢复的无缝集成。

请求拦截器实现

// 增强版 SuperAgent 请求函数
const createOfflineRequest = (superagent, queue) => {
  return async (method, url, data) => {
    // 检查网络状态
    if (!isOnline()) {
      console.log(`网络离线,将 ${method} ${url} 加入队列`);
      return new Promise((resolve) => {
        queue.enqueue({
          method,
          url,
          data,
          resolve,
          reject: (err) => console.error('请求失败:', err)
        });
      });
    }

    // 网络正常,直接发送请求
    return new Promise((resolve, reject) => {
      superagent[method.toLowerCase()](url)
        .send(data)
        .end((err, res) => {
          if (err && isNetworkError(err)) {
            // 网络错误,加入队列
            queue.enqueue({ method, url, data, resolve, reject });
          } else if (err) {
            reject(err);
          } else {
            resolve(res);
          }
        });
    });
  };
};

// 网络错误判断函数
const isNetworkError = (err) => {
  // 基于 SuperAgent 错误信息判断网络错误
  return err.message.includes('network is offline') || 
         err.message.includes('Failed to fetch') ||
         err.status === 0; // 状态码 0 通常表示网络错误
};

完整请求流程控制

// 初始化组件
const initOfflineSystem = async () => {
  const queue = new RequestQueue();
  const storage = new OfflineStorage();
  await storage.open();
  
  // 从数据库加载历史队列
  const storedRequests = await storage.getRequests();
  storedRequests.forEach(req => queue.enqueue(req));
  
  // 创建增强版请求函数
  const request = createOfflineRequest(superagent, queue);
  
  // 暴露全局 API
  window.offlineRequest = request;
  window.syncOfflineQueue = () => syncQueue(queue, storage, request);
  
  return request;
};

// 同步离线队列
const syncQueue = async (queue, storage, request) => {
  while (queue.length > 0 && isOnline()) {
    const req = queue.dequeue();
    try {
      console.log(`同步离线请求: ${req.method} ${req.url}`);
      const res = await request(req.method, req.url, req.data);
      req.resolve(res);
      await storage.deleteRequest(req.id); // 从数据库删除已完成请求
    } catch (err) {
      req.retries++;
      if (req.retries < 3) {
        queue.enqueue(req); // 重试次数未满,重新入队
      } else {
        req.reject(err); // 超过重试次数,触发错误回调
      }
    }
  }
};

离线同步策略

离线请求恢复并非简单的按序重发,需考虑请求依赖、幂等性和数据一致性问题,制定合理的同步策略。

同步优先级规则

  1. 方法优先级:DELETE > PUT > POST > GET(确保数据修改操作优先同步)
  2. 时间戳排序:相同方法按请求发起时间排序
  3. 依赖处理:通过请求 ID 建立依赖关系,支持链式同步
// 带依赖关系的请求同步
const syncWithDependencies = async (queue) => {
  const independentRequests = queue.filter(req => !req.dependsOn);
  
  // 先处理无依赖的请求
  for (const req of independentRequests) {
    await executeRequest(req);
  }
  
  // 处理有依赖的请求
  const dependentRequests = queue.filter(req => req.dependsOn);
  for (const req of dependentRequests) {
    const dependency = queue.find(r => r.id === req.dependsOn);
    if (!dependency || dependency.status === 'completed') {
      await executeRequest(req);
    } else {
      // 依赖未完成,放回队列尾部
      queue.enqueue(req);
    }
  }
};

冲突解决机制

当离线状态下对同一资源进行多次修改,恢复网络后可能产生数据冲突。可采用以下策略解决:

  1. 版本号控制:为每个资源添加版本字段,服务端验证版本一致性
  2. 时间戳策略:以最后修改时间为准,覆盖旧数据
  3. 合并策略:对可合并的字段(如数组、计数器)进行增量合并
// 请求冲突处理示例
const handleRequestConflict = async (req, conflictError) => {
  // 获取服务端最新数据
  const serverData = await fetchLatestData(req.url);
  
  // 根据冲突类型选择解决策略
  if (conflictError.type === 'version_conflict') {
    // 使用最新版本号重试请求
    req.data.version = serverData.version;
    return req;
  } else if (conflictError.type === 'data_conflict') {
    // 合并本地修改与服务端数据
    const mergedData = mergeData(req.data, serverData);
    req.data = mergedData;
    return req;
  }
  
  // 无法自动解决,触发用户干预
  throw new Error('需要用户手动解决数据冲突');
};

完整实现案例

整合上述组件,构建一个完整的离线请求处理系统,包含请求队列、网络监听、数据持久化和同步恢复功能。

系统架构图

mermaid

核心代码集成

// 初始化离线请求系统
const init = async () => {
  // 创建组件实例
  const networkMonitor = new NetworkMonitor();
  const storage = new OfflineStorage();
  await storage.open();
  const queue = new RequestQueue(storage);
  await queue.restore(); // 从存储恢复队列
  
  // 创建增强版 SuperAgent
  const request = SuperAgentEnhancer.create(superagent, queue, networkMonitor);
  
  // 创建同步管理器
  const syncManager = new SyncManager(queue, storage, request);
  
  // 绑定网络事件
  networkMonitor.onOnline(() => syncManager.sync());
  
  // 暴露全局 API
  window.offlineRequest = request;
  
  console.log('离线请求系统初始化完成');
  return request;
};

// 应用示例
init().then(request => {
  // 使用增强版请求函数
  request('POST', '/api/tasks', { title: '离线任务' })
    .then(res => console.log('任务创建成功:', res.body))
    .catch(err => console.error('任务创建失败:', err));
});

性能优化建议

  1. 批量同步:网络恢复时,合并多个小请求为批量请求
  2. 请求压缩:对队列中的请求数据进行压缩存储
  3. 后台同步:使用 Service Worker 在页面关闭后继续同步
  4. 优先级调度:根据用户行为动态调整同步优先级
// 批量请求合并示例
const batchRequests = (requests) => {
  // 按 URL 分组
  const grouped = {};
  requests.forEach(req => {
    if (!grouped[req.url]) grouped[req.url] = [];
    grouped[req.url].push(req);
  });
  
  // 生成批量请求
  const batchRequests = [];
  Object.keys(grouped).forEach(url => {
    const requests = grouped[url];
    if (requests.length > 1 && requests[0].method === 'POST') {
      // 合并多个 POST 请求为批量操作
      batchRequests.push({
        method: 'POST',
        url: `${url}/batch`,
        data: { operations: requests.map(r => ({ id: r.id, data: r.data })) }
      });
    } else {
      // 非批量请求直接添加
      batchRequests.push(...requests);
    }
  });
  
  return batchRequests;
};

总结与扩展

基于 SuperAgent 构建的离线请求处理系统,通过网络状态监测、请求队列管理和智能同步策略,有效解决了网络波动导致的数据一致性问题。该方案具有以下优势:

  1. 侵入性低:通过封装而非修改 SuperAgent 源码实现功能增强
  2. 可扩展性强:模块化设计支持自定义存储、同步和冲突解决策略
  3. 兼容性好:基于标准 Web API,支持主流浏览器

潜在扩展方向

  1. 与 Service Worker 集成:实现后台同步和推送通知
  2. 请求优先级动态调整:结合用户行为数据优化同步顺序
  3. 离线数据分析:统计离线时长、请求失败率等指标
  4. 用户可控同步:提供 UI 界面让用户手动触发和解决冲突

官方文档:README.md

通过本文介绍的方法,开发者可显著提升 Web 应用在弱网环境下的稳定性和用户体验,使应用具备接近原生应用的离线工作能力。SuperAgent 的灵活性使其成为构建此类系统的理想选择,而合理的架构设计则确保了系统的可靠性和可维护性。

【免费下载链接】superagent Ajax for Node.js and browsers (JS HTTP client). Maintained for @forwardemail, @ladjs, @spamscanner, @breejs, @cabinjs, and @lassjs. 【免费下载链接】superagent 项目地址: https://gitcode.com/gh_mirrors/su/superagent

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值