开篇:地铁里没网,用户点个赞按钮直接转圈圈

很多应用的体验极差,就是因为它是“网络优先”的。

用户在电梯里、地铁上,点个收藏,或者发个评论,按钮直接开始转圈,转了几秒钟弹个 Toast:“网络异常”。用户刚才的编辑内容全丢了,或者操作直接失败。

在这个网络无处不在又随时可能断开的时代,“离线优先(Offline First)” 架构才是留住用户的王道。

用户不管有没有网,点按钮立刻成功(UI 立刻更新),数据先存本地。等网络恢复了,再悄悄在后台把数据同步到服务器。这就是所谓的“乐观更新(Optimistic UI)”和“本地队列”。

https://placeholder-01.jpg/

一、架构设计:本地数据库是“唯一真相”

在这套架构里,UI 渲染的数据永远来自本地数据库(RDB),而不是网络请求的直接返回。

核心流程:

  1. 用户操作 -> 写入本地 RDB -> 状态标记为 PENDING(待同步)。

  2. UI 监听 RDB,数据一变,界面立刻刷新。

  3. 网络恢复(或定时器触发) -> 后台读取 PENDING 的数据 -> 发送网络请求。

  4. 请求成功 -> 更新 RDB 状态为 SYNCED(已同步)。

二、建表与状态设计

我们需要一个表来存业务数据,以及一个表来存操作队列。

typescript

// 1. 业务表
const SQL_TODO = `
  CREATE TABLE IF NOT EXISTS todo (
    id TEXT PRIMARY KEY,
    title TEXT,
    isCompleted INTEGER,
    syncStatus INTEGER -- 0: 待同步, 1: 已同步
  )
`;

// 2. 操作队列表(处理复杂的增删改顺序)
const SQL_QUEUE = `
  CREATE TABLE IF NOT EXISTS sync_queue (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    actionType TEXT, -- 'INSERT', 'UPDATE', 'DELETE'
    entityId TEXT,
    payload TEXT, -- JSON数据
    retryCount INTEGER DEFAULT 0
  )
`;

三、写入与触发同步

当用户添加一条待办:

typescript

async function addTodo(title: string) {
  const id = generateUUID();
  // 1. 写业务表
  await rdbStore.insert('todo', { id, title, isCompleted: 0, syncStatus: 0 });
  
  // 2. 写同步队列
  await rdbStore.insert('sync_queue', {
    actionType: 'INSERT',
    entityId: id,
    payload: JSON.stringify({ id, title })
  });

  // 3. 通知 UI 刷新(如果没用 DataObserver,可以用 EventHub)
  eventHub.emit('TODO_CHANGED');
  
  // 4. 尝试触发同步
  triggerSync();
}

triggerSync 是核心。你不能同步队列里有一万条数据就发一万个请求,得批量。

typescript

import networkObserver from '@ohos.net.connection';

let isSyncing = false;

async function triggerSync() {
  if (isSyncing) return;
  // 检查网络
  if (!hasNetwork()) return;

  isSyncing = true;
  try {
    // 从队列中取一批(比如 10 条)
    const queueItems = await getQueueItems(10);
    if (queueItems.length === 0) return;

    // 组装成批量接口发给服务端
    const response = await api.syncBatch(queueItems);
    
    if (response.success) {
      // 删掉已成功的队列数据
      await removeQueueItems(queueItems.map(i => i.id));
      // 更新业务表状态
      await updateTodoStatusToSynced(queueItems);
    }
  } catch (err) {
    // 失败处理:增加 retryCount
    await incrementRetry(queueItems);
  } finally {
    isSyncing = false;
    // 如果还有数据,继续同步
    triggerSync();
  }
}

注意那个 if (isSyncing) return;。 如果你不加这个锁,用户疯狂点按钮,会触发几百次同步,服务端直接把你 IP 封了。

四、冲突解决与重试机制

坑一:网络断了,同步队列一直发。
监听系统的网络状态变化(@ohos.net.connection)。如果没网,停止发请求,等网络恢复了(on('netAvailable'))再触发。

坑二:服务器返回 400(数据不合法)怎么办?
如果一条数据服务端死活返回 400,你无限重试会把队列卡死。必须在队列里设置 retryCount,如果超过 3 次,说明这条数据有严重问题,把它移到“死信队列”,或者打个日志放弃同步,别影响其他正常数据。

坑三:多端同步冲突。
用户手机离线加了条数据(版本 v1),平板在线也改了这个数据(版本 v2)。手机上来同步时,服务端得能判断版本。这就是大名鼎鼎的乐观锁。服务端要检查客户端传来的版本号,如果比服务端旧,直接拒绝,让客户端拉取最新数据合并。

https://placeholder-02.png/

五、总结

离线优先架构,是提升应用健壮性的核武器。核心记住三点:

  1. UI 永远只信本地库,操作立刻落库,给用户最快反馈。

  2. 维护一个同步队列,网络恢复后批量推送,并加锁防止并发重试。

  3. 做好最坏的打算,设置重试上限,处理版本冲突,防止队列死锁。

小任务: 用 RDB 写一个最简单的离线点赞功能。点击时往数据库里加一条记录,界面上数字立刻 +1。然后开个定时器,每 10 秒检查有没有网络,有网络就把未同步的记录推送到一个虚拟 API。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐