上个季度做线下门店的点餐系统,产品经理说:“用户进门扫码太 low 了,我要做鸿蒙的‘碰一碰’。桌子角贴个 NFC 标签,手机碰一下就自动拉起咱们的 App,并且直接进到点餐页,桌号也得自动带上。”

我一听,这需求挺酷。查了一下 HarmonyOS 的文档,用 Want 加 App Linking 就能搞定。本以为半天能完事,结果被这个“碰一碰”折磨了三天。

一开始,死活拉不起来

按照官方文档,NFC 的拉起是通过系统的隐式匹配完成的。我在 module.json5 里声明了权限:

json

"requestPermissions": [
  { "name": "ohos.permission.NFC_TAG" }
]

然后我在 skills 里草草写了个配置:

json

"skills": [
  {
    "actions": ["ohos.want.action.home"],
    "entities": ["entity.system.home"]
  },
  {
    "actions": ["ohos.nfc.tag.action.TAG_FOUND"],
    "uris": [
      {
        "scheme": "https",
        "host": "food.example.com"
      }
    ]
  }
]

我觉得万事大吉了,拿着一张写入 https://food.example.com/menu 的标签就往上贴。结果手机毫无反应,连个提示都没有。

排查过程极其痛苦。
我先用 hdc shell hilog 抓日志,发现系统确实读取到了 NFC 标签,但没有任何拉起应用的记录。这说明我的配置根本没有匹配上系统的分发规则。

去查阅了 App Linking 的白皮书才发现,鸿蒙对隐私和安全抓得极严。你的 host 域名,必须在这个应用的 AGC(AppGallery Connect)后台进行校验。也就是说,系统必须要确认你是这个域名的拥有者,才允许碰这个标签拉你的 App。

好了,去后台配置域名、下载校验文件、传到服务器根目录,一通操作搞完。再贴一次,系统终于弹出了“是否打开某某应用”的提示了。

https://placeholder-01.png/

致命点是冷启动与热启动的差异

App 能拉起来了,我以为大功告成,结果测试一跑,直接给我提了三个 Bug:

  1. App 没打开时去碰,能进点餐页,但桌号丢了。

  2. App 在后台运行时去碰,毫无反应。

  3. App 在前台运行时去碰,没跳转。

我去翻了 UIAbility 的生命周期,才发现自己之前对 Want 的理解太浅了。

如果你用 router.pushUrl 在首页的生命周期里去接参数,是完全行不通的。应用拉起时,系统会把 NFC 标签里的 URI 封装在 Want 对象里传给你。

我的第一版代码是这样的:

typescript

export default class EntryAbility extends UIAbility {
  onCreate(want, launchParam) {
    // 错误:只在 onCreate 里解析参数
    if (want.uri) {
      const tableId = extractTableId(want.uri);
      // 糟糕:这里直接跳转,页面栈都没初始化,必然失败
      router.pushUrl({ url: 'pages/OrderPage', params: { id: tableId } });
    }
  }
}

问题出在哪里?

  1. 热启动没接住:如果应用在后台,用户碰标签,系统走的是 onNewWant 生命周期,根本不会重新走 onCreate。所以应用毫无反应。

  2. 生命周期时序错误:在 onCreate 阶段,UI 根本没创建完,直接 router.pushUrl 极易导致黑屏或崩溃。

改进后的架构:
不要在任何 Ability 的生命周期里直接操作 UI,而是把数据缓存起来,让 UI 层自己去拿。

typescript

export default class EntryAbility extends UIAbility {
  // 增加一个全局处理函数
  private handleWant(want: Want) {
    if (want.uri) {
      const tableId = extractTableId(want.uri);
      // 把拿到的桌号放到全局 AppStorage 里
      AppStorage.setOrCreate('nfc_table_id', tableId);
      // 发个事件通知(如果是热启动,页面需要监听)
      this.context.eventHub.emit('NFC_ARRIVED', tableId);
    }
  }

  onCreate(want, launchParam) {
    // 冷启动走这里
    this.handleWant(want);
  }

  onNewWant(want, launchParam) {
    // 热启动走这里
    this.handleWant(want);
  }
}

然后在首页的 aboutToAppear 里处理跳转:

typescript

aboutToAppear() {
  const tableId = AppStorage.get<string>('nfc_table_id');
  if (tableId) {
    // 此时页面已就绪,可以安全跳转
    router.pushUrl({ url: 'pages/OrderPage', params: { id: tableId } });
    // 清空状态,防止下次启动重复跳
    AppStorage.setOrCreate('nfc_table_id', '');
  }
}

如果应用在前台,得监听一下 eventHub:

typescript

onPageShow() {
  this.context.eventHub.on('NFC_ARRIVED', (id: string) => {
    // 直接刷新当前页面的桌号数据
    this.loadMenu(id);
  });
}

NFC 标签写入的数据格式

另外,NFC 标签里写什么数据也是有讲究的。不要随便找个写卡软件写个纯文本进去,系统是不认的。

必须写入 NDEF URI 格式。你可以用带有 NFC 功能的安卓手机,下载一个叫 NFC Tools 的 App,选择“写入 -> 添加记录 -> URL”,然后把 https://food.example.com/menu?tableId=08 这个链接写进去。

系统读取到这个 URI 后,根据你在 module.json5 里配置的 scheme 和 host 进行匹配,匹配成功后,才会把这个 URI 封装到 Want 里传给你的应用。

所以,你的业务参数(比如桌号)一定要拼在 URL 的 Query 参数里,然后在应用里用正则去解析。

https://placeholder-02.jpg/

血泪总结

就为了这个“优雅的碰一碰”,我翻烂了文档,熬了两个大夜。

  1. onCreate 和 onNewWant 缺一不可。这是鸿蒙系统拉起应用的标准逻辑,做通知跳转、深链跳转都必须两者兼顾。

  2. AGC 后台的域名校验。如果你发现手机能识别标签,但死活不拉你的应用,99% 是因为你的域名没在后台校验或者校验失效了。

  3. UI 更新不要写在 Ability 里。这是架构设计的底线,数据流转一定要用 AppStorage 或 EventHub 做中转,交给 UI 页面自己去消费。

希望后来者看到我这篇踩坑日记,能少走点弯路,早点下班。

Logo

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

更多推荐