离线队列功能说明

一、功能概述

实现 「请求已加入离线队列,将在网络恢复后自动提交」 的完整能力:当设备无网络或请求失败时,将请求写入本地队列;待网络恢复后自动按顺序重放队列中的请求,并支持请求间的依赖与结果注入(如先上传图片再提交表单时,把上传返回的 URL 注入到表单中)。

适用于 uni-app Android 场景下的工单提交、巡检完成、施工交底等需要离线可用的业务。


二、引入与使用

2.1 应用入口引入(必须)

main.js 中引入并执行 install(),应用启动后会注册网络监听与定时重放,离线队列才会在「网络恢复后自动提交」。

// main.js
import App from './App'

// 引入离线功能:注册网络监听、冷启动/网络恢复/定时重放
import { install } from '@/util/offline.js'
install()

// ... 其余应用初始化

说明:只需引入一次,无需在页面或组件中重复引入 install


2.2 业务层按需引入 API

业务代码中在文件顶部静态 import,而是在需要发请求时动态按需引入,避免首包体积和未使用时的加载:

// 在提交/上传等逻辑里按需引入
const { $request, $depend } = await import('@/util/offline.js')
// 或仅用依赖请求时
const { $depend } = await import('@/util/offline')

可选:需要手动触发「重试失败队列」时,可引入 $retryFailed

const { $retryFailed } = await import('@/util/offline.js')
await $retryFailed()

2.3 使用方式一:普通请求 $request

适用于单次请求(无依赖),例如上传文件、提交单条接口。

const { $request } = await import('@/util/offline.js')

const result = await $request({
  id: 'unique-id',           // 必填,全局唯一,便于队列去重与依赖
  url: '/api/xxx',
  method: 'POST',
  data: { key: 'value' }
})

if (result.__offline_queued) {
  uni.showToast({ title: '网络不佳,数据已保存,将在网络恢复后自动提交', icon: 'none' })
  return
}
if (!result.success) {
  uni.showToast({ title: result.error || '请求失败', icon: 'none' })
  return
}
// 成功:使用 result.data

上传文件示例(项目内 pages/construction/start.vue):

const { $request } = await import('@/util/offline.js')

const upRes = await $request({
  id: `upload-avatar-${workerId}`,
  url: '/api/files/upload',
  filePath: tempPath,
  name: 'file',
  headers: { 'content-type': 'multipart/form-data' }
})

if (upRes.__offline_queued) {
  // 已加入队列,网络恢复后会自动上传
  return
}
const fileUrl = upRes?.data?.data?.fileUrl || upRes?.data?.fileUrl

2.4 使用方式二:依赖请求 $depend

适用于「先 A 后 B,且 B 需要 A 的返回结果」的场景(如先上传再提交表单,表单里带上传返回的 URL)。

const { $depend } = await import('@/util/offline')

// 参数:当前请求 id, 依赖的父请求 id 数组, 注入配置, 请求 config
const result = await $depend(
  'my-submit-id',
  ['upload-id-1'],
  { 'upload-id-1': 'data.photoUrl=data.data.fileUrl' },  // 把上传结果的 data.data.fileUrl 注入到当前 data.photoUrl
  {
    url: '/api/order/submit',
    method: 'POST',
    data: { title: 'xxx', photoUrl: '' }
  }
)

if (result.__offline_queued) {
  uni.showToast({ title: '网络不佳,数据已保存,将在网络恢复后自动提交', icon: 'none', duration: 3000 })
} else if (result.success) {
  uni.showToast({ title: '提交成功', icon: 'success' })
} else {
  uni.showToast({ title: result.error || '提交失败', icon: 'none' })
}

无依赖的提交(仅用 $depend 统一写法、带唯一 id):

const { $depend } = await import('@/util/offline')

const result = await $depend(
  `night-patrol-batch-upload-${taskId.value}`,
  [],   // 无依赖
  {},   // 无注入
  {
    url: '/api/mobile/night-patrol/batch-upload',
    method: 'POST',
    data: submitData
  }
)

2.5 项目内实际示例汇总

文件 用途 使用 API
main.js 应用启动时注册离线重放与网络监听 import { install } from '@/util/offline.js'; install()
pages/smartDetail/index.vue 智慧巡查任务完成(单接口提交) $depend(id, [], {}, config)
pages/detail/index.vue 检查点记录:先上传多图 → 提交记录 → 任务完成 多次 $depend,带 dependsinject
pages/construction/start.vue 工人头像:先上传图片 → 更新工人信息(注入 fileUrl) $request 上传 + $depend 更新

检查点多步依赖示例pages/detail/index.vue 节选):

const { $depend } = await import('@/util/offline')

// 1)先提交所有上传请求(无依赖)
for (const uploadRequest of fileUploadRequests) {
  await $depend(
    uploadRequest.id,
    [],
    {},
    { url: uploadRequest.url, method: uploadRequest.method, filePath: uploadRequest.filePath, name: uploadRequest.name }
  )
}

// 2)提交检查点记录,依赖所有上传,并把上传返回的 fileUrl 注入到 data
const result = await $depend(
  `submit-checkpoint-records-${taskInfo.value.patrolTaskId}`,
  fileUploadRequestIds,
  injectConfig,
  { url: '/api/equipment/inspection/checkpoint-records/batch', method: 'POST', data: submitData }
)

// 3)任务完成请求依赖「检查点记录提交」
await $depend(
  `complete-task-${taskInfo.value.patrolTaskId}`,
  [`submit-checkpoint-records-${taskInfo.value.patrolTaskId}`],
  {},
  { url: `/api/equipment/inspection/tasks/${taskInfo.value.patrolTaskId}/complete`, method: 'POST', data: completeData }
)

if (result.__offline_queued) {
  uni.showToast({ title: '网络不佳,数据已保存,将在网络恢复后自动提交', icon: 'none', duration: 3000 })
}

三、核心概念

3.1 主队列(离线队列)

项目 说明
存储 Key OFFLINE_QUEUEuni.setStorageSync
作用 存放因离线或网络异常而未能发出的请求
结构 数组,每项为请求配置对象(见下文「队列项结构」)

用户在网络不可用时发起提交 → 请求不会直接发往服务端,而是被 push 到主队列并持久化到本地。业务层可据此提示:「网络不佳,数据已保存,将在网络恢复后自动提交」。

3.2 失败队列

项目 说明
存储 Key OFFLINE_FAILED_QUEUE
作用 存放重试后仍失败的请求(若业务有写入)
重试 通过 $retryFailed()install() 中网络恢复后的 retryFailed() 统一重试

当前 replay() 逻辑中,主队列里重试超过次数仍失败的请求会被直接删除,不会写入失败队列;失败队列由其他需要「保留失败请求」的逻辑写入并供 retryFailed() 使用。

3.3 队列项结构(主队列 / 失败队列)

每个队列项为普通请求或带依赖的请求,例如:

{
  id: 'unique-request-id',           // 必填,唯一标识,用于去重与依赖
  url: '/api/xxx',
  method: 'POST',
  data: { ... },                     // 可选,请求体
  depends: ['parent-id-1'],          // 可选,依赖的请求 id 列表
  inject: { 'parent-id-1': 'data.photoUrl=data.data.fileUrl' },  // 可选,从依赖结果注入到当前 data
  headers: {},                       // 可选
  filePath: '',                      // 可选,上传时使用
  name: 'file',
  formData: {}
}
  • id:必填,全局唯一;重放时按依赖关系拓扑排序,再按顺序执行。
  • depends / inject:用于「先上传后提交」等场景,见下文「依赖与注入」。

3.4 TTL 缓存

项目 说明
存储 Key 前缀 TTL_ + 请求 id
作用 缓存某次请求的响应结果,供依赖该请求的后续请求做 inject
过期时间 默认 5 分钟(cacheGet(key, ttlMin)

重放时,先执行完的请求会把响应写入内存 cache 和 TTL 缓存;后面依赖它的请求通过 inject() 从缓存中取父结果并写入当前请求的 dataurl


四、整体流程概览

                    ┌─────────────────┐
                    │  业务调用       │
                    │  $request()     │
                    │  $depend()      │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │ online() 检查   │
                    │ 网络是否可用    │
                    └────────┬────────┘
                             │
              ┌──────────────┴──────────────┐
              │ 在线                        │ 离线
              ▼                             ▼
    ┌──────────────────┐          ┌──────────────────┐
    │ 若有 depends     │          │ push(config)      │
    │ 先 cacheGet 注入 │          │ 写入主队列        │
    │ 再 request()     │          │ 持久化到本地      │
    │ 成功则 cacheSet  │          └────────┬─────────┘
    └──────────────────┘                   │
              │                            │ 返回
              │                            │ { __offline_queued: true }
              ▼                            │
    返回真实响应 / 错误对象                 ▼
                                   业务提示:「将在网络恢复后自动提交」

网络恢复后的自动提交由 replay()retryFailed() 完成,见第四节。


五、「网络恢复后自动提交」的实现

5.1 何时触发重放

以下三种情况会尝试把队列中的请求发到服务端:

  1. 应用冷启动
    install()main.js 里被调用,约 1 秒后执行一次 replay(),处理上次未发完的队列。

  2. 网络状态从无到有
    install() 里注册了 uni.onNetworkStatusChange,当 isConnected === true 时:

    • 延迟 500ms 执行 replay()(先处理主队列);
    • 再延迟 1s 执行 retryFailed()(再处理失败队列),避免与主队列冲突。
  3. 定时轮询
    每 30 秒检查一次网络,若在线则同样先 replay(),再延迟 1s 执行 retryFailed()

对应代码(install):

// util/offline.js 第 517–538 行
export function install() {
  setTimeout(() => { replay(); }, 1000);

  uni.onNetworkStatusChange(({ isConnected }) => {
    if (isConnected) {
      setTimeout(() => {
        replay();
        setTimeout(() => { retryFailed(); }, 1000);
      }, 500);
    }
  });

  setInterval(async () => {
    if (await online()) {
      replay();
      setTimeout(() => { retryFailed(); }, 1000);
    }
  }, 30000);
}

因此,用户看到「请求已加入离线队列,将在网络恢复后自动提交」后,无需额外操作,只要网络恢复(或下次打开 App、或 30 秒内轮询到网络),就会自动重放。

5.2 主队列重放:replay()

作用:把主队列(OFFLINE_QUEUE)里的请求按依赖关系依次发到服务端,成功则从队列中移除,失败则按当前策略处理(见下)。

步骤简述

  1. 网络检查
    if (!(await online())) return;

  2. 加载并过滤队列

    • 从本地读取主队列;
    • 若某请求 id 已在失败队列中,则从本次重放列表中剔除,避免重复处理;
    • 写回过滤后的队列。
  3. 拓扑排序
    使用 topoSort(list)depends 关系排序,保证被依赖的请求先执行(无循环依赖)。

  4. 逐条执行

    • 对每条请求先 inject(item, cache),把依赖的父请求结果注入到当前 item.dataurl
    • 若有 complete-task- 前缀的 id,会先等待 3 秒再发(避免服务端压力);
    • 最多重试 MAX_RETRY(3)次,间隔 RETRY_DELAY(800ms);
    • 成功:从主队列 removeItem(item.id),并把响应写入内存 cache 和 TTL 缓存;
    • 失败(含 TOKEN_EMPTY):从主队列移除,写入失败队列;其它失败也移除并计为 failedCount。
  5. 用户提示

    • 若重放后主队列仍有剩余,toast:「已处理 N 个请求,剩余 M 个」;
    • 若主队列已空,toast:「所有离线请求已处理完成」;
    • 若失败队列中仍有项,toast:「有 N 个请求失败,请检查网络后重试」。

关键代码(顺序执行 + 重试 + 移除):

// util/offline.js 第 378–419 行(节选)
for (const item of list) {
  try { inject(item, cache); } catch (e) { failedCount++; continue; }

  if (item.id && item.id.startsWith('complete-task-')) {
    await new Promise(r => setTimeout(r, 3000));
  }

  for (let i = 0; i < MAX_RETRY; i++) {
    try {
      const res = await request(item);
      // 解析 responseData,写入 cache 和 cacheSet(item.id, responseData)
      removeItem(item.id);
      successCount++;
      break;
    } catch (e) {
      if (e.type === 'TOKEN_EMPTY') { ... removeItem(item.id); break; }
      if (i < MAX_RETRY - 1) await new Promise(r => setTimeout(r, RETRY_DELAY));
    }
  }
  if (!ok) {
    removeItem(item.id);  // 重试耗尽后直接删除,不进入失败队列
    failedCount++;
  }
}

5.3 失败队列重试:retryFailed()

作用:专门处理 OFFLINE_FAILED_QUEUE 中的请求(需要业务侧或其它逻辑把失败请求 push 到失败队列)。
流程与 replay 类似:先 online(),再加载失败队列,过滤掉已在主队列中的 id,然后逐条 inject + request,成功则 removeFailedItem(item.id),并给出成功/失败个数的 toast。


六、依赖与注入($depend)

6.1 为什么需要依赖

典型场景:先上传图片,再提交表单,表单里要带上传返回的 URL
若两个请求都进离线队列,重放时必须保证:
1)先执行上传;
2)再执行提交,且提交时的 data 里要包含上传接口返回的 fileUrl
依赖(depends)和注入(inject)就是用来描述顺序和数据传递的。

6.2 拓扑排序(topoSort)

队列项带有 depends: ['id-a', 'id-b'] 表示当前请求依赖 id-a、id-b 的结果。
topoSort(list) 按依赖关系做 DAG 排序,使:

  • 被依赖的请求排在前面;
  • 若存在循环依赖会抛错。
    这样在 replay() 中按顺序执行时,父请求一定先于子请求执行,且结果已写入 cache 和 TTL。

6.3 注入(inject)

对当前请求项 item,若配置了 inject,则会从 cache(以及 TTL)中取父请求的响应,按配置写入 item.dataitem.url

inject 配置格式(在 item.inject 中,key 为父请求 id):

  • 字符串
    • url/ 前缀:用父结果替换 item.url 中的 {{id}}
    • 目标路径=源路径:从父结果按源路径取值,写入 item.data 的目标路径,例如 data.photoUrl=data.data.fileUrl
    • 仅源路径:等价于目标路径与源路径相同。
  • 对象
    { targetPath: sourcePath },可配置多组。

路径取值/写值通过 getPath(obj, path)setPath(obj, path, val) 实现(点号分隔)。

6.4 $depend 封装

业务层不需要手写 depends/inject 结构,可直接用:

const result = await $depend(requestId, parentIds, injectMap, config);
  • requestId:当前请求 id;
  • parentIds:依赖的父请求 id 数组;
  • injectMap:父 id → 注入配置(字符串或对象);
  • config{ url, method, data, ... },与 $request 的 config 一致。

内部会设置 config.id = requestIdconfig.depends = parentIdsconfig.inject = injectMap,然后调用 $request(config)
因此「加入离线队列」和「网络恢复后自动提交」的行为与 $request 一致,只是多了依赖顺序和注入。


七、业务入口:$request 与离线分支

7.1 $request(config)

  • 在线
    • 若有 depends,先从 TTL 缓存 cacheGet(depId) 取父结果,注入到 config,再 request(config)
    • 成功则 cacheSet(config.id, responseData)
    • 失败返回 { success: false, error, statusCode, data, id },不抛错。
  • 离线
    • 不发起真实请求,执行 push(config) 写入主队列并持久化;
    • 返回 { __offline_queued: true, id, message: 'OFFLINE_QUEUED' }

业务层可根据 result.__offline_queued 提示「请求已加入离线队列,将在网络恢复后自动提交」。

7.2 网络判断

online() 通过 uni.getNetworkType 判断当前是否为 'none',非 'none' 视为在线。
因此「网络恢复」即指下一次 online() 为 true 的时机(网络状态变化、定时轮询或冷启动后再检一次)。


八、业务层使用示例

8.1 无依赖:智慧巡查任务完成(smartDetail)

// pages/smartDetail/index.vue
const { $depend } = await import('@/util/offline');

const requestId = `night-patrol-batch-upload-${taskId.value}`;
const result = await $depend(
  requestId,
  [],   // 无依赖
  {},   // 无注入
  {
    url: '/api/mobile/night-patrol/batch-upload',
    method: 'POST',
    data: submitData
  }
);

if (result.__offline_queued || result.success) {
  // 本地更新任务状态、缓存等
  if (result.__offline_queued) {
    uni.showToast({
      title: '网络不佳,数据已保存,将在网络恢复后自动提交',
      icon: 'none',
      duration: 3000
    });
  } else {
    uni.showToast({ title: '任务已完成', icon: 'success' });
  }
  setTimeout(() => { uni.navigateBack(); }, 1500);
} else {
  uni.showToast({ title: result.error || '提交失败,请重试', icon: 'none' });
}

这里 $depend(id, [], {}, config) 等价于「带 id 的 $request」:离线时请求进入主队列,网络恢复后由 replay() 自动 POST 到 /api/mobile/night-patrol/batch-upload

8.2 有依赖:先上传再提交(detail 检查点)

// pages/detail/index.vue
const fileUploadRequestIds = fileUploadRequests.map(req => req.id);

// 1)先提交上传请求(无依赖)
for (const uploadRequest of fileUploadRequests) {
  await $depend(
    uploadRequest.id,
    [],
    {},
    { url: uploadRequest.url, method: uploadRequest.method, filePath: uploadRequest.filePath, name: uploadRequest.name }
  );
}

// 2)提交检查点记录,依赖所有上传结果,并把上传返回的 fileUrl 注入到 data
const result = await $depend(
  `submit-checkpoint-records-${taskInfo.value.patrolTaskId}`,
  fileUploadRequestIds,
  injectConfig,  // 例如 { 'upload-id-1': 'data.photoUrl=data.data.fileUrl' }
  { url: '/api/equipment/inspection/checkpoint-records/batch', method: 'POST', data: submitData }
);

// 3)任务完成请求依赖「检查点记录提交」
await $depend(
  `complete-task-${taskInfo.value.patrolTaskId}`,
  [`submit-checkpoint-records-${taskInfo.value.patrolTaskId}`],
  {},
  { url: `/api/equipment/inspection/tasks/${taskInfo.value.patrolTaskId}/complete`, method: 'POST', data: completeData }
);

if (result.__offline_queued) {
  uni.showToast({ title: '网络不佳,数据已保存,将在网络恢复后自动提交', icon: 'none', duration: 3000 });
}

重放时顺序为:上传 1…N → 提交检查点记录(注入各上传的 fileUrl)→ 任务完成。
用户侧同样只需看到一次「将在网络恢复后自动提交」的提示即可。


九、关键常量与配置

常量 / 配置 说明
QUEUE_KEY 'OFFLINE_QUEUE' 主队列存储 key
FAILED_QUEUE_KEY 'OFFLINE_FAILED_QUEUE' 失败队列存储 key
MAX_RETRY 3 单请求重放时最大重试次数
RETRY_DELAY 800 ms 重试间隔
TTL_KEY 'TTL_' 响应缓存 key 前缀
缓存 TTL 5 分钟 cacheGet 默认过期时间
冷启动延迟 1 s install() 后首次 replay() 延迟
网络恢复延迟 500 ms + 1 s 先 replay,1s 后 retryFailed
轮询间隔 30 s 定时检查网络并重放

十、小结

  • 「请求已加入离线队列,将在网络恢复后自动提交」
    请求在离线或失败时被 push 到主队列并持久化,业务通过 result.__offline_queued 提示用户;无需用户再点一次,由 replay() 在网络恢复(或冷启动/定时)时自动重放。

  • 网络恢复的触发
    冷启动约 1s 后、uni.onNetworkStatusChange 检测到已连接、以及每 30 秒的轮询,都会在 online() 为 true 时执行 replay(),必要时再执行 retryFailed()

  • 依赖与顺序
    通过 $depend(id, parents, injectMap, config) 声明依赖和注入,重放前经 topoSort 排序,保证先执行被依赖请求并把结果注入到后续请求的 data/url,实现「先上传后提交」等流程的离线与自动重试。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐