HarmonyOS 离线模式实战:本地缓存、同步队列与冲突解决
HarmonyOS 离线模式实战:本地缓存、同步队列与冲突解决
离线模式不是断网时展示缓存这么简单。用户离线新增、编辑、删除的数据,恢复网络后要同步到服务端;如果服务端也被其他设备修改,就会产生冲突。没有同步队列和冲突策略,离线功能很容易变成“看起来能用,恢复网络后丢数据”。

一、离线能力先分读和写
只读离线和可写离线是两种复杂度。读缓存只要过期策略;写离线需要同步队列和冲突处理。
| 能力 | 典型场景 | 关键风险 |
|---|---|---|
| 只读缓存 | 查看最近路线、文章草稿 | 数据过期 |
| 离线新增 | 新建笔记、反馈草稿 | 重复提交 |
| 离线编辑 | 修改资料、路线备注 | 与远端冲突 |
| 离线删除 | 删除收藏、移除附件 | 恢复后顺序错误 |

二、资料与版本边界:本文写应用层离线同步
本文示例面向 HarmonyOS NEXT / ArkTS 工程,重点在本地缓存、离线变更记录、同步队列、冲突判断和失败重试。真实项目需结合本地数据库、文件存储、后端版本字段和账号体系。

接入离线模式前先限定业务对象
离线能力不要一开始覆盖所有数据。建议选择一个体积小、冲突可解释、失败可恢复的对象先跑通,例如路线备注、收藏标签、反馈草稿。订单支付、多人协同文档、附件批量上传这类场景复杂很多,不适合做第一版。
| 候选对象 | 是否适合第一版 | 原因 |
|---|---|---|
| 路线备注 | 适合 | 字段少,冲突容易解释 |
| 收藏状态 | 适合 | 操作简单,可重试 |
| 反馈草稿 | 适合 | 本地保存价值高 |
| 订单支付 | 不建议 | 幂等、资金、状态机复杂 |
| 大附件上传 | 不建议 | 断点、空间、网络成本高 |
业务对象选小一点,才能把本地表、同步队列、失败重试、冲突解决讲清楚。第一版离线模式的目标不是“什么都能离线”,而是让一条链路可靠。
本地表建议同时保存实体和任务
只保存业务实体不够。用户离线期间做了什么操作,必须通过任务表记录下来。否则恢复网络时只能猜测应该同步新增、更新还是删除。
export interface OfflineStorageSchema {
entityTable: string;
taskTable: string;
conflictTable: string;
maxRetryCount: number;
}
export function routeNoteOfflineSchema(): OfflineStorageSchema {
return {
entityTable: 'route_note',
taskTable: 'route_note_sync_task',
conflictTable: 'route_note_conflict',
maxRetryCount: 3
};
}
这段 schema 描述的是离线存储结构。实体表保存当前页面展示的数据,任务表保存待同步动作,冲突表保存需要用户选择的版本。
三、本地实体:必须带版本和同步状态
离线数据不能只保存业务字段。要带本地版本、远端版本和同步状态。
export type SyncState = 'synced' | 'pending' | 'conflict' | 'failed';
export interface OfflineRouteNote {
localId: string;
remoteId: string;
title: string;
content: string;
localVersion: number;
remoteVersion: number;
syncState: SyncState;
updatedAt: number;
}
export function markNotePending(note: OfflineRouteNote): OfflineRouteNote {
return {
...note,
localVersion: note.localVersion + 1,
syncState: 'pending',
updatedAt: Date.now()
};
}
这段模型描述本地数据的同步事实。localVersion 表示本机改过几次,remoteVersion 用来和服务端判断冲突。
四、同步任务:把离线操作排队
每次离线新增、编辑、删除,都应该生成同步任务,而不是只改本地表。
export type SyncOperation = 'create' | 'update' | 'delete';
export interface SyncTask {
taskId: string;
localId: string;
operation: SyncOperation;
baseRemoteVersion: number;
retryCount: number;
createdAt: number;
}
export function createSyncTask(localId: string, operation: SyncOperation, baseRemoteVersion: number): SyncTask {
return {
taskId: `${operation}_${localId}_${Date.now()}`,
localId,
operation,
baseRemoteVersion,
retryCount: 0,
createdAt: Date.now()
};
}
同步任务记录了操作类型和操作基于哪个远端版本。恢复网络后,队列按顺序执行。
五、队列执行:失败可重试,不能无限重试
同步失败可能是网络问题,也可能是业务冲突。重试要有限制。
export interface SyncQueueState {
tasks: SyncTask[];
running: boolean;
}
export function nextSyncTask(state: SyncQueueState): SyncTask | undefined {
if (state.running) {
return undefined;
}
return state.tasks.find(task => task.retryCount < 3);
}
export function increaseRetry(task: SyncTask): SyncTask {
return { ...task, retryCount: task.retryCount + 1 };
}
队列控制的是节奏,不直接处理业务冲突。连续失败三次后应标记为需要用户处理或等待下一次网络恢复。
六、冲突判断:远端版本变了就不要覆盖
离线编辑恢复后,如果服务端版本已经变化,不能直接覆盖。
export interface RemoteVersionInfo {
remoteId: string;
version: number;
updatedBy: string;
}
export interface ConflictCheckResult {
conflict: boolean;
reason: string;
}
export function checkSyncConflict(task: SyncTask, remote: RemoteVersionInfo): ConflictCheckResult {
if (task.baseRemoteVersion !== remote.version) {
return { conflict: true, reason: '远端数据已被其他设备修改' };
}
return { conflict: false, reason: '可以同步' };
}
这段代码把冲突判断显式化。它预防的是离线修改恢复后覆盖其他设备最新数据。
七、冲突解决:让用户选择保留哪一份
冲突不是错误,它需要决策。可以让用户选择保留本地、保留远端或合并。
export type ConflictResolveAction = 'keepLocal' | 'keepRemote' | 'merge';
export interface ConflictResolvePlan {
action: ConflictResolveAction;
message: string;
}
export function buildConflictResolvePlan(action: ConflictResolveAction): ConflictResolvePlan {
if (action === 'keepLocal') {
return { action, message: '使用本机离线修改覆盖远端内容' };
}
if (action === 'keepRemote') {
return { action, message: '放弃本机离线修改,使用远端最新内容' };
}
return { action, message: '对比两份内容后手动合并' };
}
合并策略要根据业务复杂度选择。简单备注可以用户选择,复杂文档建议做差异对比。
八、离线模式问题排查表
| 离线同步表现 | 优先怀疑的同步环节 | 排查方式 | 修复方向 |
|---|---|---|---|
| 恢复网络后数据丢失 | 没有同步任务 | 查 SyncTask | 离线写入时入队 |
| 本地覆盖远端新数据 | 没做版本比较 | 检查 baseRemoteVersion | 同步前做冲突判断 |
| 队列一直重试 | 没有限制 retry | 查 retryCount | 超过次数标记 failed |
| 删除和编辑顺序错乱 | 队列未按时间执行 | 查看 createdAt | 按任务创建顺序同步 |
| 用户不知道有冲突 | 冲突被当作失败 | 查看 ConflictCheckResult | 展示冲突处理页 |
| 缓存过期仍展示为最新 | 没有同步状态 | 查 syncState | 页面展示待同步状态 |
九、离线上线前验收表
| 离线验收场景 | 通过结果 |
|---|---|
| 只读缓存 | 断网可查看最近数据,并提示更新时间 |
| 离线新增 | 恢复网络后能同步且不重复 |
| 离线编辑 | 远端未变化时正常同步 |
| 冲突处理 | 远端变化时不直接覆盖 |
| 失败重试 | 弱网失败可重试但不无限循环 |
| 状态展示 | pending、failed、conflict 对用户可见 |
离线验收要用两台设备模拟冲突:设备 A 断网编辑,设备 B 在线编辑同一条数据,然后让设备 A 恢复网络。只有这条路径能验证版本比较和冲突解决是否真的有效。
同步失败不要无限重试
离线队列如果不限制重试次数,弱网或服务端错误会让任务一直占用资源。更稳的做法是区分可重试失败和不可重试失败,超过次数后把状态交给用户处理。
export interface SyncRetryDecision {
shouldRetry: boolean;
nextDelayMs: number;
reason: string;
}
export function decideSyncRetry(retryCount: number, httpStatus: number): SyncRetryDecision {
if (httpStatus >= 400 && httpStatus < 500) {
return { shouldRetry: false, nextDelayMs: 0, reason: '客户端请求不可自动重试' };
}
if (retryCount >= 3) {
return { shouldRetry: false, nextDelayMs: 0, reason: '超过最大重试次数' };
}
return { shouldRetry: true, nextDelayMs: (retryCount + 1) * 3000, reason: '等待网络恢复后重试' };
}
这段决策避免队列无限循环。4xx 更适合让用户修正或重新登录,5xx 和网络异常可以有限重试,超过次数后要在页面显示失败状态。
离线链路可以分四步上线
第一步只做只读缓存。断网时能看到最近数据,并明确告诉用户更新时间和可能不是最新。
第二步做离线草稿。用户断网输入内容后,本地保存,不急着自动同步复杂业务对象。
第三步接同步队列。每次新增、编辑、删除都生成任务,恢复网络后按顺序执行。任务执行结果要能回写实体状态。
第四步处理冲突。远端版本变化时不要直接覆盖,要把本地版本和远端版本都展示出来,让用户选择保留哪一份或合并。
这个顺序的好处是每一阶段都有清楚边界。读者可以先让产品看到只读缓存和草稿价值,再逐步扩大到可写离线。
冲突页面要让用户看得懂
冲突解决不能只给“同步失败”四个字。用户需要知道本机改了什么、远端改了什么、两个版本分别来自什么时候。
| 冲突信息 | 页面展示建议 | 目的 |
|---|---|---|
| 本地修改时间 | 显示“本机修改于 xx:xx” | 帮用户判断哪份更新 |
| 远端修改时间 | 显示“云端修改于 xx:xx” | 解释为什么产生冲突 |
| 字段差异 | 高亮不同字段 | 降低选择成本 |
| 操作按钮 | 保留本机、使用云端、手动合并 | 给明确出口 |
| 后续状态 | 已同步或仍需处理 | 避免重复冲突 |
十、离线同步相关官方资料
- 华为开发者文档:应用数据持久化
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-persistence-overview - 华为开发者文档:应用文件访问
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/app-file-access - 华为开发者文档:网络管理
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/net-connection-overview
十一、让离线模式可控而不是碰运气
离线模式的核心是记录“用户在离线期间做了什么”。本地实体保存版本,同步任务保存操作,队列控制执行,冲突判断保护远端数据,冲突解决把选择权交给用户。页面上也要让用户看到哪些内容待同步,哪些内容同步失败,哪些内容需要人工处理。
落地时不要一开始覆盖所有业务。可以先选择一个风险较低但有代表性的对象,例如路线备注或反馈草稿,跑通本地保存、断网修改、恢复同步、冲突处理四条路径。小对象跑稳后,再扩展到订单、附件、多人协作内容这类复杂数据。
| 离线链路问题 | 推荐处理方式 |
|---|---|
| 离线写入怎么记录 | 生成 SyncTask |
| 怎么防止覆盖远端 | 比较远端版本 |
| 失败怎么处理 | 有限重试并标记状态 |
| 冲突怎么办 | 保留本地、远端或合并 |
| 用户怎么看状态 | 展示 pending、failed、conflict |
更多推荐



所有评论(0)