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同步前做冲突判断
队列一直重试没有限制 retryretryCount超过次数标记 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”解释为什么产生冲突
字段差异高亮不同字段降低选择成本
操作按钮保留本机、使用云端、手动合并给明确出口
后续状态已同步或仍需处理避免重复冲突

十、离线同步相关官方资料

  1. 华为开发者文档:应用数据持久化
    https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-persistence-overview
  2. 华为开发者文档:应用文件访问
    https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/app-file-access
  3. 华为开发者文档:网络管理
    https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/net-connection-overview

十一、让离线模式可控而不是碰运气

离线模式的核心是记录“用户在离线期间做了什么”。本地实体保存版本,同步任务保存操作,队列控制执行,冲突判断保护远端数据,冲突解决把选择权交给用户。页面上也要让用户看到哪些内容待同步,哪些内容同步失败,哪些内容需要人工处理。

落地时不要一开始覆盖所有业务。可以先选择一个风险较低但有代表性的对象,例如路线备注或反馈草稿,跑通本地保存、断网修改、恢复同步、冲突处理四条路径。小对象跑稳后,再扩展到订单、附件、多人协作内容这类复杂数据。

离线链路问题推荐处理方式
离线写入怎么记录生成 SyncTask
怎么防止覆盖远端比较远端版本
失败怎么处理有限重试并标记状态
冲突怎么办保留本地、远端或合并
用户怎么看状态展示 pending、failed、conflict
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐