欢迎光临
我们一直在努力

uniapp离线队列功能

离线队列功能说明

一、功能概述

实现 「请求已加入离线队列,将在网络恢复后自动提交」 的完整能力:当设备无网络或请求失败时,将请求写入本地队列;待网络恢复后自动按顺序重放队列中的请求,并支持请求间的依赖与结果注入(如先上传图片再提交表单时,把上传返回的 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,带 depends 与 inject
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_QUEUE(uni.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() 从缓存中取父结果并写入当前请求的 data 或 url。


四、整体流程概览

┌─────────────────┐
│ 业务调用 │
│ $request() │
│ $depend() │
└────────┬────────┘

┌────────▼────────┐
│ online() 检查 │
│ 网络是否可用 │
└────────┬────────┘

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

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


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

5.1 何时触发重放

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

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

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

    • 延迟 500ms 执行 replay()(先处理主队列);
    • 再延迟 1s 执行 retryFailed()(再处理失败队列),避免与主队列冲突。
  • 定时轮询
    每 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)里的请求按依赖关系依次发到服务端,成功则从队列中移除,失败则按当前策略处理(见下)。

    步骤简述:

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

  • 加载并过滤队列

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

  • 逐条执行

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

    • 若重放后主队列仍有剩余,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.data 或 item.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 = requestId、config.depends = parentIds、config.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,实现「先上传后提交」等流程的离线与自动重试。

    赞(0)
    未经允许不得转载:171主机测评 » uniapp离线队列功能
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址