前几天周末,我在一个偏远郊区的农家乐团建。去卫生间的时候,顺手在咱们自己的 App 里发了条带图的动态。

结果提示“网络错误”,图片没发出去。更气人的是,我辛辛苦苦敲的百来字文案,因为页面退出,直接没了。

回来之后我就跟开发组拍桌子:“用户环境千变万化,谁规定发动态的时候一定得满格 5G?必须做一个离线优先的架构,发送按钮点下去,不管有没有网,立刻给我变成‘已发送’。”

这就是“乐观更新(Optimistic UI)”和“离线队列”的由来。这篇记录下我是怎么在鸿蒙里用 RDB 构建一条可靠的本地消息队列的。

https://placeholder-01.jpg/

为什么不能用网络优先?

我之前的垃圾写法是这样的:

typescript

// 错误示范:网络请求成功后才更新 UI
async function postComment(text: string) {
  this.isLoading = true;
  try {
    await api.postComment(text); // 没网就卡在这里,最后报错
    this.commentList.push(text);
  } catch (err) {
    promptAction.showToast({ message: '网络异常' });
  } finally {
    this.isLoading = false;
  }
}

这种模式在没网的时候,会让用户感到极度的挫败,而且一旦发生异常,用户的心血(输入的文字)全白费了。

正确的思路是:把本地关系型数据库(RDB)当做唯一的数据真相来源(Single Source of Truth)。 UI 只认数据库,网络请求只是后台的同步动作。

设计本地消息队列表

我们需要一张表来记录用户的每一次“发送”动作。

typescript

import relationalStore from '@ohos.data.relationalStore';

const SQL_CREATE_QUEUE = `
  CREATE TABLE IF NOT EXISTS message_queue (
    id TEXT PRIMARY KEY,       -- 客户端生成的唯一 ID
    content TEXT,              -- 消息内容
    status INTEGER,            -- 状态:0=待发送, 1=发送中, 2=发送成功, 3=发送失败
    create_time INTEGER        -- 创建时间
  )
`;

当用户点击“发送”时,我们根本不发网络请求,直接往数据库里插一条 status = 0 的记录。

typescript

async function onSendClick(content: string) {
  const id = generateUUID(); 
  
  // 1. 写入数据库,状态为待发送 (0)
  await rdbStore.insert('message_queue', {
    id: id,
    content: content,
    status: 0,
    create_time: Date.now()
  });

  // 2. 界面上立刻显示这条消息,打上一个“转圈圈”的 Loading 图标
  // 因为 UI 是双向绑定或监听 RDB 的,所以这一步非常快
  
  // 3. 触发后台同步任务
  triggerSync();
}

同步引擎与网络监听

接下来,要写一个后台同步引擎。它不断地从队列里拿 status = 0 的数据去发请求。

typescript

let isSyncing = false;

async function triggerSync() {
  // 加锁,防止用户狂点导致多个同步任务并发执行
  if (isSyncing) return;
  
  // 检查当前网络状态,如果是离线状态,直接返回
  if (!isNetworkAvailable()) return;

  isSyncing = true;
  try {
    // 取出所有待发送的数据
    const predicates = new relationalStore.RdbPredicates('message_queue');
    predicates.equalTo('status', 0);
    const resultSet = await rdbStore.query(predicates, ['id', 'content']);
    
    while (resultSet.goToNextRow()) {
      const id = resultSet.getString(0);
      const content = resultSet.getString(1);
      
      // 更新状态为“发送中”(可选)
      await updateStatus(id, 1);
      
      try {
        // 真正发网络请求
        await api.postMessage(id, content); 
        // 成功了,把状态改成 2
        await updateStatus(id, 2);
      } catch (err) {
        // 失败了,把状态改成 3,或者根据重试次数决定是否放弃
        await updateStatus(id, 3);
      }
    }
    resultSet.close();
  } finally {
    isSyncing = false;
  }
}

网络恢复时怎么办?

如果用户发送时没网,消息存在了本地。等用户走到有 Wi-Fi 的地方,我们必须自动触发同步。这就需要监听系统的网络变化:

typescript

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

// 监听网络可用事件
const netConnection = connection.createNetConnection();
netConnection.on('netAvailable', (data) => {
  console.info('网络恢复了,开始执行积压的消息队列');
  triggerSync();
});

https://placeholder-02.png/

处理极端情况:幂等性

这套架构虽然牛,但有个巨大的隐患:如果请求已经到达了服务器,但服务器返回响应的途中网断了,客户端会把状态标记为失败,下次重试时会重复发送。

解决这个问题的关键在于:幂等性(Idempotency)。
我们在客户端生成的唯一 id,必须作为请求的一部分传给服务器。服务器收到这个 id 时,先查一下数据库,如果这个 id 已经处理过了,直接返回成功;没处理过才落库。

离线优先的架构,会让用户觉得你的应用“快得不可思议”。即使是在高铁隧道里,他们点发送按钮的反应也是瞬间的。

忠告:

  1. 本地 RDB 是绝对的核心,千万别再搞什么网络成功后才更新界面的老套路了。

  2. 队列必须要有重试上限。如果一条消息一直报 500 错误,重试个 3 次就该放弃了,别让一条死数据堵死整个队列。

Logo

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

更多推荐