两台设备都收到了数据,却在不同字段上各说各话

我在做一个跨设备清单展示的纸面方案时,先遇到的不是近场发现,也不是传输速度,而是 JSON 字段改名。旧版应用把清单内容放在 assets,新版改成了 itemIds。双方单独运行都没问题;一旦业务数据从另一端进来,新版页面读取不到数组,却可能照常显示“接收成功”。如果后面还有批量操作,空数组甚至会被误当成用户主动提交了空清单。

这类问题跟通信通道没有必然关系。即便把内容通过系统分享、二维码、调试文本框或后台接口传进来,接收端仍要解释同一段业务数据。我决定先把“数据合同版本”抽成可以离线复现的验证器,再谈实际碰一碰入口的设备和能力约束。这能避免 Demo 为了表现设备之间的交互,跳过最难追查的字段兼容边界。

img

不应该把版本号当成界面装饰

Demo 约定两个被支持的业务版本。v1 的清单字段叫 assets,v2 改叫 itemIds;两者都要求有非空 packetId 与 requestId。接收端统一输出 v2 形式的标准对象,后续 UI 只认识 itemIds,不会在每个组件里写一遍 assets ?? itemIds。v1 输入通过校验后记为 DOWNGRADE,意思是经过兼容映射,不是把系统或手机真的降了版本;v2 输入成功记为 ACCEPT。

v3 在本轮尚未定义,所以直接 HOLD_VERSION。缺少清单、清单为空、ID 出现重复或字段类型错误都归为 HOLD_REQUIRED。这里没有为未知版本做“尽力猜测”,因为字段相同不代表语义相同。例如 v3 可能把 itemIds 变成加密引用,勉强当 v2 解析反而容易显示错误对象。

四份构造样本的目标写得很死:PK-41 是 v2、包含两条记录,应 ACCEPT;PK-42 是 v1、包含三条 assets,应 DOWNGRADE;PK-43 声明 v3,应 HOLD_VERSION;PK-44 是 v2 但列表为空,应 HOLD_REQUIRED。最终合计 ACCEPT 1 / DOWNGRADE 1 / HOLD 2。这些名字是虚构的业务数据包编号,不对应官方碰一碰协议帧,也没有给 NFC 或系统回调编造接口。

先把输入变成“不可信 JSON”处理

接收文本并不能证明内容正确。第一步是检查包体是否为对象、版本是否在白名单,再按版本提取数组。很多看起来无害的错误都应该在这里明确返回原因:schemaVersion 是字符串 '2' 而不是数字 2;itemIds 是对象或逗号分隔的字符串;数组里有空 ID;同一个 ID 出现两次;单次清单过长。让 UI 继续执行并指望后面的页面兜底,最终只会把错误扩散到更多状态里。

核心验证器只依赖 Node.js 标准库,判断成功后返回一个新的规范化对象,而不是就地修改收到的原始数据。下面是随包脚本的简化摘录,主要强调先验版本,再验字段,最后做映射的顺序。

const validString = (value) => typeof value === 'string' &&
  value.length > 0 && value.length <= 40;
function inspect(packet) {
  if (!packet || typeof packet !== 'object' || !validString(packet.packetId)) {
    return { packetId: 'UNKNOWN', status: 'HOLD_REQUIRED' };
  }
  if (packet.schemaVersion !== 1 && packet.schemaVersion !== 2) {
    return { packetId: packet.packetId, status: 'HOLD_VERSION' };
  }
  const ids = packet.schemaVersion === 1 ? packet.assets : packet.itemIds;
  if (!validString(packet.requestId) || !Array.isArray(ids) || !ids.length ||
      ids.length > 100 || !ids.every(validString) || new Set(ids).size !== ids.length) {
    return { packetId: packet.packetId, status: 'HOLD_REQUIRED' };
  }
  return { packetId: packet.packetId,
    status: packet.schemaVersion === 1 ? 'DOWNGRADE' : 'ACCEPT',
    normalized: { requestId: packet.requestId, itemIds: ids.slice() } };
}

这里没有把 requestId 做成一个防重票据,也没有 TTL、签名验证和加密承诺。前面第三组已经讨论过重复提交和过期回执,本篇故意限定在字段版本兼容与必填结构,避免用换一个名称重新写同一个问题。真正接入设备间传输时,应在规范化之前增加通道信任判断,并由业务方单独完成身份确认、权限和防重校验。

把失败原因留在输入边缘

开发阶段最怕一个笼统的“格式不正确”。我让报告区分 UNSUPPORTED_SCHEMA 和 ITEM_LIST_INVALID,这样用户能知道是双方版本没有协商成功,还是清单内容本身就有问题。线上产品不必把所有内部字段暴露给用户,但日志与排查工具应保留这些稳定原因码,不要只留下一个红色感叹号。

为了把四个分支落成可回归的测试,随包的 fixtures/packets.json 保存原始数据,tools/schema-check.mjs 读取后输出 reports/schema-report.json。测试时先检查语法,再运行脚本,并验证总数及每个包的状态。报告还保留 normalized,方便后续调试页确认 v1 的 assets 确实转换为了 v2 itemIds。

const results = packets.map(inspect);
const summary = {
  ACCEPT: results.filter((r) => r.status === 'ACCEPT').length,
  DOWNGRADE: results.filter((r) => r.status === 'DOWNGRADE').length,
  HOLD: results.filter((r) => r.status.startsWith('HOLD')).length
};
for (const r of results) console.log(`SC-404 ${r.packetId} ${r.status}`);
console.log(`SC-404 summary ACCEPT=${summary.ACCEPT} ` +
  `DOWNGRADE=${summary.DOWNGRADE} HOLD=${summary.HOLD}`);

这一步并不需要设备、蓝牙或 Wi-Fi。我更愿意先在电脑上把负例补齐,因为真正连接设备后,传输日志、生命周期事件和业务校验日志交织在一起,很难分清是哪一层造成了空结果。Node.js 的这段检查没有依赖任何 HarmonyOS 私有 API,执行环境也不会因设备类型不同改变校验答案。

ArkUI 只消费清洗后的状态

在应用侧,我使用 TextInput 作为“粘贴构造 JSON 进行检查”的调试入口,而不是宣称 TextInput 就是系统碰一碰接收接口。真实入口可以替换成官方能力允许的分享数据回调,再把拿到的文本交给业务解析器。ArkUI 页面只展示三类业务态:可接受、需兼容映射、需拦截。它不应该直接把原始 JSON 插入页面对象图。

下面片段展示调试页的输入和按钮行为;checkLocalSchema 代表移植后的 ArkTS 业务校验函数,需要由工程本身实现并配套测试,不是华为官方 SDK 方法。这里没有声称该片段已在 DevEco Studio 编译通过。

@Entry
@Component
struct Index {
  @State rawJson: string = '';
  @State resultText: string = '等待检查';

  build() {
    Column({ space: 16 }) {
      Text('碰一碰清单兼容预检 SC-404').fontSize(20);
      TextInput({ placeholder: '粘贴构造的业务JSON', text: this.rawJson })
        .onChange((text: string) => { this.rawJson = text; });
      Button('检查清单').onClick(() => {
        try {
          const value: Record<string, Object> = JSON.parse(this.rawJson);
          this.resultText = this.checkLocalSchema(value);
        } catch (err) {
          this.resultText = 'HOLD_REQUIRED:JSON解析失败';
        }
      });
      Text(this.resultText);
    }.padding(16)
  }

  private checkLocalSchema(value: Record<string, Object>): string {
    // 此处接入由业务工程移植并测试的校验逻辑。
    return value ? '待业务校验' : 'HOLD_REQUIRED';
  }
}

img

为了保持演示范围清楚,我没有提供模拟“触碰成功”的系统弹窗,也没有用假通知表示手机已经识别另一台设备。页面中的四个结果是加载同一套 JSON 构造样本后的预览。将来接官方系统入口时,先核对目标设备、API Level、授权和分享数据格式,再把接入逻辑单独放进适配层;适配层的输出才进入这里的校验函数。

失败时不要把旧清单留在画面上

假设先检查 PK-41 成功,页面展示两条清单;随后粘贴 PK-43,版本不受支持。如果只改顶部状态为 HOLD_VERSION,底部仍保留上一个规范化数组,用户容易误以为这两条记录来自 PK-43。更稳妥的处理是每次输入都开启新的本地检查批次,先清理待呈现的旧数据,确认当前批次校验成功后再展示本次规范化结果。

此外,还应明确内存边界。脚本对数组长度限制为 100,对每个 ID 的长度限制为 40,这是演示项目自定上限,不是官方单次碰一碰传输限额。真实工程还要限制原始文本字节数、JSON 嵌套深度,以及日志中可记录字段;使用完的大对象不必无意义地长期挂在页面状态上。异常分支既要给出用户可理解的提示,也要避免把原始输入全文写入 HiLog。

结果、验证方式和下一步

实际执行 Node.js 22 得到:PK-41 ACCEPT、PK-42 DOWNGRADE、PK-43 HOLD_VERSION、PK-44 HOLD_REQUIRED,总计 ACCEPT=1 DOWNGRADE=1 HOLD=2。这组结果能证明当前业务脚本对这四份构造数据作出了确定判断,不能证明 HarmonyOS 精准碰一碰已经完成端到端互传、系统接收回调可靠,或某一型号设备支持所需能力。

img

如果后续扩展到 v3,我会先建立 v3 的契约说明和拒收负例,再决定是否增加新的映射函数。老版本兼容应该有明确维护窗口和退场计划,否则 if (version===...) 很快会散落到每个业务页面。先把协议层与 UI 层分离,升级时才不会牵动所有按钮、列表和详情状态。

这篇想留下的工程经验其实很朴素:设备已经把数据交给应用,不代表应用已经理解那份数据。只有确认版本、字段和语义都符合合同,才能把它当作当前清单展示。没有被理解的数据,就应该停在入口,而不是带着一个成功动画继续往下走。

官方资料与能力边界

Logo

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

更多推荐