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()(再处理失败队列),避免与主队列冲突。
- 延迟 500ms 执行
-
定时轮询
每 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)