1. 为什么要在OpenHarmony上用React Native读写NFC
先说结论:这个组合能让你用一套JS/TypeScript代码,同时覆盖Android、iOS以及OpenHarmony三条业务线,NFC标签读取这种硬件能力则通过桥接层交给原生侧处理。
我最早接触这个需求,是给一款工业巡检设备做配套App。设备跑的是OpenHarmony标准系统,但团队里大部分前端同学只会React技术栈,如果按传统方式全部用ArkTS重写业务,估算下来光UI层就要多写两个月。当时正好OpenHarmony开源社区在推进React Native的适配工作,就把RN引了进来。真正让我决定用它做NFC读取的原因有三点:第一,RN在OpenHarmony上的架构已经能把原生能力暴露成普通JS模块,NFC这种系统服务调用起来比想象中顺;第二,NFC标签解析本身是纯逻辑操作,TS写起来比ArkTS更顺手,而且生态里现成的解析库可以直接搬;第三,同一套业务代码后面还能复用到Android平板和iOS手机上,对多端交付是实打实的减负。
当然这个方案也有代价。OpenHarmony的RN适配版本目前还落后官方主版本一大截,很多新特性不能用,遇到问题往上排查时可以参考的资料也少。所以这篇文章我不会只讲“怎么做”,还会把适配过程中踩过的坑和判断依据一起写出来。无论你是做工业巡检、智慧零售、设备配网还是门禁权限管理,只要涉及在OpenHarmony设备上读取RFID/NFC标签,这套框架都能给你一个可落地的起点。
1.1 这个组合能解决什么实际问题
把RN和NFC放一起,最典型的场景是这么几类:
- 巡检/点检:设备上贴NFC标签,App靠近即可读取设备编号、上次维保时间,判断是否到期。
- 库存/资产管理:仓库货架贴标签,手机或手持终端一扫,弹出物料信息,不用人工录入。
- 配网/初始化:路由器或智能家电贴标签,App读取标签里的Wi-Fi SSID和密码字段,一键完成配置。
- 展陈/零售互动:商品标签里存URL或智能海报,扫码枪和NFC手机都能触发跳转。
这些场景里,NFC读取只是一个“入口动作”,真正的业务跑在UI、网络、数据库这些层。用RN做上层的好处是,团队里写业务页面的同学完全不用关心硬件细节,只需要调一个readTag()方法拿到对象,然后按类型渲染即可。底层是OpenHarmony还是Android,对业务代码来说几乎透明。
1.2 方案选型背后的三个关键考量
很多人第一反应会问:OpenHarmony自己明明有NFC接口,为什么非要套一层RN?我的理由有三个。
第一是团队成本。会ArkTS的开发者数量远少于会React的,招人、培养都贵,而且业务页面一旦复杂起来,ArkTS的开发效率并不比RN高。第二是跨端复用。同一个巡检App,南方工厂用OpenHarmony手持机,北方仓库可能用Android旧平板,差异只存在于启动时的平台判断,业务代码完全复用。第三是生态。RN的NFC解析库、表单组件、图表组件极其丰富,这些在OpenHarmony原生生态里还很薄弱,与其自己造轮子,不如直接站到RN生态的肩膀上。
但我也要提醒一句:如果你们的产品只跑OpenHarmony一个平台,而且团队里没有React经验,那我建议直接用ArkTS写。引入RN会带来JS引擎开销、包体积增加、版本适配滞后等问题,单一平台场景下是纯粹的负资产。选RN的前提是“多端一致”的需求真实存在,而不是为了技术炫技。
2. NFC标签技术基础:读数据之前先弄懂这五件事
NFC和RFID经常被人混着说,其实它们是包含关系。RFID是射频识别技术的统称,工作频率覆盖低频、高频、超高频;NFC是RFID在高频(13.56MHz)段的一个子集,特点是通信距离近(通常小于10cm)、支持点对点通信和卡模拟。在OpenHarmony和Android上做标签读取,我们打交道的主要是符合ISO 14443/15693标准的Type A/B/V标签,以及NFC Forum定义的Type 1到Type 5标签。
2.1 标签类型与协议家族
要把NFC数据读对,第一步是搞清楚你面前的是哪种标签。我在项目里整理过一个速查表:
| 标签类型 | 底层协议 | 典型芯片 | 常见用途 | NDEF支持 |
|---|---|---|---|---|
| Type 1 | ISO 14443A | NXP Topaz | 票务、名片 | 只读为主 |
| Type 2 | ISO 14443A | NXP NTAG213/215/216 | 智能海报、防伪 | 支持 |
| Type 3 | ISO 18092 / JIS X 6319-4 | Sony FeliCa | 交通卡、电子钱包 | 支持 |
| Type 4 | ISO 14443A/B | NXP DESFire、Mifare Plus | 门禁、支付 | 支持 |
| Type 5 | ISO 15693 | NXP ICODE SLIX | 图书管理、药品追溯 | 部分支持 |
OpenHarmony的NFC接口会通过getTagInfo()返回标签的协议类型,一般在nfcA、nfcB、nfcF、nfcV、isoDep、ndef等字段里做判断。实际业务里,我最常见的是Type 2的NTAG系列,因为便宜、稳定、写入流程简单,库存和巡检场景用得最多。如果你要做的功能是往标签里写数据,优先选Type 2或Type 4;如果只是读,Type 1也够,但兼容性要现场实测。
2.2 NDEF数据格式解析
绝大多数NFC标签存取的数据是NDEF(NFC Data Exchange Format)消息。NDEF本质上是一个TLV(Type-Length-Value)结构:一条NDEF消息由一个或多个NDEF记录组成,每个记录由记录头、类型、载荷长度、可选ID和载荷构成。
一个NDEF记录的头部第一个字节拆开看是这样的:
- Bit 7(MB):消息起始记录,1表示这是第一条记录。
- Bit 6(ME):消息结束记录,1表示这是最后一条记录。
- Bit 5(CF):链式记录标志,载荷太长被拆分时置1。
- Bit 4(SR):短记录标志,1表示载荷长度只有1字节,0表示用4字节表示长度。
- Bit 3(IL):ID长度字段是否存在。
- Bit 2-0(TNF):Type Name Format,即类型名格式。
这个字节我一开始也是死记硬背,后来写了个简单的解析函数才知道,SR那位特别容易忽略。很多标签芯片在写入时,如果载荷小于256字节,会用短记录格式,你按4字节去读就把长度算错了。
MB和ME同样重要。一个标签里可能有连续多个NDEF记录,比如智能海报就是“标题文本记录+URI记录+Action记录”拼在一起。解析时必须根据MB和ME判断边界,否则会把两条记录彻底搅在一起。
2.3 命名格式TNF与常用RTD类型
TNF字段决定了类型字段怎么解释。NFDEF记录头里TNF的取值范围和含义如下:
- 0x00:空记录,类型和载荷都无效。
- 0x01:NFC Forum well-known type,类型字段是RTD(Record Type Definition)。
- 0x02:MIME媒体类型,比如
text/plain、application/json。 - 0x03:绝对URI,类型字段就是完整URI。
- 0x04:外部类型,通常以
urn:nfc:ext:domain:type形式存在。 - 0x05:未知类型,载荷无类型定义。
- 0x06:未更改类型,用于链式记录的场景。
- 0x07:保留。
实际项目里,TNF=0x01的well-known type最常用,其中RTD字段又分三种:
T:文本记录,载荷前1字节是语言码长度和编码标志(bit 7为0表示UTF-8,1表示UTF-16),后面跟着语言码(如en、zh)和实际文本。U:URI记录,载荷前1字节是URI标识符前缀索引,0x04代表https://,0x03代表http://,后面才是真正URI。Sp:智能海报,载荷里嵌套了一个完整的NDEF消息,需要递归解析。
我在解析“U”记录时吃过一次亏。URI前缀代码表里0x00是“没有前缀”,后面直接跟完整URI;但如果标签写入工具帮你写了完整https://,而前缀索引又填了0x04,就会拼出https://https://。这种情况通常不是解析代码的问题,而是标签写入工具各自的习惯不同。稳妥的做法是解析完拼接后,用正则校验一下,把重复协议头的情况拦掉。
2.4 标签能力集差异
不是所有标签都能写,也不是所有标签都能读NDEF。比如Mifare Classic(常用于门禁)走的是私有协议,很多手机和OpenHarmony设备并不把它当作标准NDEF标签暴露出来;DESFire要选择应用ID和文件ID才能读到NDEF,而且需要先认证,涉及密钥交换的部分还是留在原生层处理比较合适。这些标签的读取权限、写入权限、密码保护策略都各不一样,做方案设计时一定要提前问清楚现场用的是什么芯片,否则代码写完了到现场发现标签类型不匹配,整个功能直接废掉。
3. 从零搭建RN+OpenHarmony开发环境
环境搭建是整套流程里最容易让人放弃的一步,因为网上资料少、版本杂。我把自己验证过的一整套流程写出来,照着做能省不少时间。先说明一下,我用的组合是:OpenHarmony 4.0 Release标准系统 + DevEco Studio 4.0 + react-native-ohos 0.72.x分支。这个组合不是最新的,但胜在稳定,社区反馈和样例代码最全。
3.1 开发板与系统版本选择
OpenHarmony不是所有设备都支持NFC,这一点特别容易被忽略。我最初用的DAYU200开发板,系统默认不带NFC驱动,翻了半天资料才发现要自己在源码里加NFC芯片驱动并重新编译系统。如果你不想折腾系统编译,直接买市面上的OpenHarmony商用设备,比如带NFC的扫码手持终端、RK3566/RK3588方案的面板,通常出厂系统已经带好NFC功能。
系统版本建议选标准系统(Standard),API Level大于等于9。OpenHarmony的NFC Tag接口在API 6就有了,但API 9之后才把权限模型稳定下来,API 8及以下很多接口行为不一致,排查起来很痛苦。客户端设备建议用API 9或10。
3.2 环境依赖安装
需要装的有这些:
- Node.js 18+,建议LTS版本,RNOH的工具链对Node版本比较敏感,太新的版本偶尔会有兼容告警。
- OpenHarmony SDK,通过DevEco Studio的SDK Manager下载,注意勾选API 9或10的SDK。
- react-native-ohos相关CLI工具:
@react-native-ohos/cli。 - 开发板连接电脑用的串口工具或HDC工具(OpenHarmony的调试工具,类似Android的adb)。
安装完成后,确认hdc list targets能看到设备,就说明连接正常。NDK方面,OpenHarmony的NDK路径通常和DevEco Studio装在一起,CMake和交叉编译工具链也需要提前备好,编译原生模块时要用。
3.3 工程初始化与目录结构
用CLI初始化工程:
npx @react-native-ohos/cli init RNNfcDemo cd RNNfcDemo初始化完成后,工程结构和标准RN项目很接近,多出的关键是harmony目录,里面放的是OpenHarmony的entry模块和原生代码。RNOH开发的原理是:把RN的C++核心、JS引擎、渲染器等编译成OpenHarmony的动态库,然后在ArkTS侧创建一个容器组件来承载RN页面。NFC这样的原生能力,通过自定义原生模块的方式暴露给JS层调用。
这个架构决定了目录里有两个需要注意的入口:
harmony/entry/src/main/ets/: 这里面是ArkTS写的入口和容器组件,RN页面会挂在某个组件上。harmony/entry/src/main/cpp/: 这里放C++层代码,NAPI(Native API)的注册和桥接工作在这里完成。
3.4 跑通Hello World的检验清单
初始化后先别急着写NFC功能,把基础工程跑通再说。我在首次运行时遇到的问题是启动白屏,这是RNOH社区里的高频问题,后面会在排查章节详细写。这里先列一个我自己用的检验清单:
- [ ]
hdc shell param get const.product.name能返回设备型号 - [ ] 工程能成功编译出
entry-default-signed.hap - [ ] 通过DevEco Studio安装HAP,点击图标能进入RN页面
- [ ] 打开DevEco的日志面板,能看到RNOH加载完成的日志,比如
RNOH: ReactApplicationContext created之类 - [ ] 在RN页面里随便写个
<Text>,带中文和Emoji,确认渲染正常
这五步都过,说明RNOH基础链路是通的,后面加原生模块只是在这个框架上做增量。
4. 原生侧NFC模块实现
NFC读取这件事必须放在原生侧做,原因很简单:RN层拿不到OpenHarmony的NFC系统服务。所以我们先写一个ArkTS模块,负责扫描标签、解析NDEF数据,再用NAPI把它暴露成JS层可调用的方法。
4.1 权限声明与Tag发现回调
在module.json5里声明NFC相关权限:
{ "module": { "requestPermissions": [ { "name": "ohos.permission.NFC_TAG_READ" } ] } }如果后面要做写入功能,还要加ohos.permission.NFC_TAG_WRITE。注意权限名字的大小写,写错了编译不报错,但运行时拿不到标签。
OpenHarmony的NFC标签发现是回调机制。在ArkTS侧,我们通过@ohos.nfc.tag模块注册监听:
import nfcTag from '@ohos.nfc.tag'; import { BusinessError } from '@ohos.base'; // 获取tagInfo需要先启动NFC扫描 function startNfcScan(): void { nfcTag.on('notify', (tagInfo: nfcTag.TagInfo) => { // 标签靠近时触发 const tagId = tagInfo.tagId; // 判断是否支持NDEF if (tagInfo.ndefSupported) { // 读取NDEF数据 } }); }这里的tagInfo对象里有一个非常重要的字段tagInfo.ndefSupported,它在底层是通过轮询标签的ATQA、SAK和协议能力综合判断的。有些标签虽然物理上支持NDEF,但状态损坏或写入了非法数据,这个字段可能为false,这时候别硬读,让用户换一张标签更合理。
4.2 NDEF消息解析的ArkTS实现
拿到NDEF标签后,最核心的工作是解析NDEF消息。这里补一段ArkTS侧的解析代码:
import nfcTag from '@ohos.nfc.tag'; import ndef from '@ohos.nfc.tag.ndef'; import { util } from '@kit.ArkTS'; class NdefRecord { tnf: number = 0; type: Uint8Array = new Uint8Array(); id: Uint8Array = new Uint8Array(); payload: Uint8Array = new Uint8Array(); } function parseNdefMessage(tag: nfcTag.TagInfo): NdefRecord[] { const ndefTag = ndef.getNdefTag(tag); if (ndefTag == null) { return []; } const msg = ndefTag.getNdefMessage(); if (msg == null) { return []; } const records: NdefRecord[] = []; msg.forEach((record) => { const r: NdefRecord = { tnf: record.tnf, type: record.type, id: record.id, payload: record.payload }; records.push(r); }); return records; }注意这里record.type和record.payload返回的是Uint8Array,不是字符串。很多刚接触的人直接对record.payload做toString(),结果得到一长串数字和逗号。正确做法是先用UTF-8解码,再按NDEF规则解析。文本类型还要处理语言码和编码标志位,URI类型则要查前缀表。
我把NDEF记录的解析收敛成一个纯函数,这样方便在ArkTS和JS层共用一套测试用例。解析文本记录时,UTD-8和UTF-16的判断要严格按payload首字节的bit 7来,很多国产标签写入器默认写UTF-8,但也有用UTF-16的,判断错了整段文本都是乱码。
4.3 NAPI桥接层映射
原生能力要暴露给RN层,需要通过NAPI注册一个模块。RNOH的桥接方式和标准RN略有不同,它基于ArkTS的NativeModule能力封装了一套turboModule协议。我们在ArkTS侧定义方法:
@NativeModule export class NfcModule { @Method readNdefTag(): Promise<string> { // 内部实现... } }然后在RN侧通过TurboModuleRegistry.get或NativeModules获取:
import { NativeModules } from 'react-native'; const { NfcModule } = NativeModules;读取后的结果我用JSON字符串传递,因为只传序列化数据最容易保证跨语言边界一致。结构体直接传对象虽然NAPI也支持,但字段多了容易踩“undefined被认为是0”的坑,不如JSON字符串干脆。
原生侧每次读取完,会返回一个对象{ tagId, records, rawMessage },其中records是一个数组,里面每个元素包含tnf、type、payloadText、payloadUri这些解析后的字段。JS层拿到后基本不需要再做二进制处理,直接渲染即可。
5. JS侧业务逻辑与界面
原生侧把数据吐出来以后,真正干活的是JS层。这也是用RN做这个项目最爽的部分——业务逻辑用TypeScript写,类型清晰,测试方便。
5.1 封装NFC读取工具类
我在JS层封装了一个NfcManager工具类,统一管理“开始扫描”“停止扫描”“读取标签”这三件事:
import { NativeModules, NativeEventEmitter } from 'react-native'; const { NfcModule } = NativeModules; const nfcEmitter = new NativeEventEmitter(NfcModule); export type NdefRecordType = { tnf: number; type: string; payloadText?: string; payloadUri?: string; }; export type NfcTagData = { tagId: string; records: NdefRecordType[]; }; class NfcManager { private scanning: boolean = false; startScan(): void { if (this.scanning) return; NfcModule.startScan(); this.scanning = true; } stopScan(): void { if (!this.scanning) return; NfcModule.stopScan(); this.scanning = false; } readTag(): Promise<NfcTagData> { return NfcModule.readNdefTag(); } onTagDetected(callback: (data: NfcTagData) => void) { return nfcEmitter.addListener('onTagDetected', callback); } } export default new NfcManager();这里有个设计细节值得说:NFC扫描是一个持续动作,不应该每读一次就开一次关一次。标签靠近后系统触发回调,数据读出来以后可以继续扫描等待下一张标签。所以startScan和stopScan是成对的生命周期管理,在页面useEffect里启动,页面卸载时停止。
NativeEventEmitter用于原生向JS侧主动推送“标签已检测到”的事件,比轮询更优雅。轮询方案在低端设备上会频繁唤醒NFC模块,耗电且容易漏事件,不推荐。
5.2 按NDEF类型动态解析文本、URI、智能海报
原生侧返回的records已经做了初步解析,但JS侧还是需要按tnf和type字段做二次分发。比如tnf=1且type=U的记录,payload是一个URI;tnf=1且type=T的记录,payload是文本;tnf=1且type=Sp是智能海报,里面嵌套的NDEF消息会以JSON字符串形式放在payload里,JS侧要再解析一次。
我把整个解析流程写得像一个管道:
function parseNdefRecords(records: NdefRecordType[]): ParsedContent[] { return records.map((record) => { if (record.tnf === 1) { const type = record.type.toLowerCase(); switch (type) { case 't': return { kind: 'text', content: record.payloadText || '' }; case 'u': return { kind: 'uri', content: record.payloadUri || '' }; case 'sp': return { kind: 'smartposter', content: JSON.parse(record.payloadText || '{}') }; default: return { kind: 'unknown', content: record.payloadText || '' }; } } if (record.tnf === 2) { // MIME类型,比如text/plain return { kind: 'mime', content: record.payloadText || '' }; } return { kind: 'unknown', content: record.payloadText || '' }; }); }这段代码在实际业务里可以根据场景扩展。比如读到一个URI,前端可以直接渲染成可点击的链接;读到一个文本,可以匹配正则判断是不是设备序列号,是的话自动跳转到对应的设备详情页。我把“解析”和“业务动作”分开,解析只负责把NDEF转成业务对象,业务动作由页面层根据对象类型自己去分发,这样更符合React的组件化思维。
5.3 防重复扫描与节流处理
NFC标签在感应区内停留时,系统可能连续上报十几次“标签已检测到”。如果每次回调都去刷新页面,列表会疯狂闪烁。我一开始没注意这个问题,现场演示时一贴上标签,页面内容跳了好几遍,非常尴尬。
解决方案是加节流。在NfcManager里维护一个lastReadAt时间戳,两次读取之间至少间隔1.5秒:
private lastReadAt: number = 0; private readonly throttleMs = 1500; onTagDetected(callback: (data: NfcTagData) => void) { return nfcEmitter.addListener('onTagDetected', (data) => { const now = Date.now(); if (now - this.lastReadAt < this.throttleMs) { return; } this.lastReadAt = now; callback(data); }); }另外还有一个“是否同一张标签”的判断。因为在巡检场景里,用户可能扫完A标签后不小心又晃到B标签,需要区分是重复上报同一张还是新标签。我简单用tagId做去重,如果同一张标签连续上报,直接忽略;如果tagId变了,立即刷新。
节流间隔不宜设太长,否则用户快速连续扫两张不同标签时会漏掉第二张。1.2到1.5秒是现场实测比较舒服的区间,既不会防抖过头,也不会重复刷新。
5.4 业务界面示例
以巡检为例,一个最简页面是这样:
import React, { useEffect, useState } from 'react'; import { View, Text, TouchableOpacity, ScrollView } from 'react-native'; import NfcManager from './NfcManager'; export function NfcReaderScreen() { const [tagInfo, setTagInfo] = useState<NfcTagData | null>(null); const [scanning, setScanning] = useState(false); useEffect(() => { const sub = NfcManager.onTagDetected((data) => { setTagInfo(data); }); return () => { sub.remove(); NfcManager.stopScan(); }; }, []); const start = () => { NfcManager.startScan(); setScanning(true); }; return ( <View style={{ flex: 1, padding: 20 }}> <TouchableOpacity onPress={start} disabled={scanning}> <Text>{scanning ? '扫描中...' : '开始扫描'}</Text> </TouchableOpacity> {tagInfo && ( <ScrollView style={{ marginTop: 20 }}> <Text>标签ID: {tagInfo.tagId}</Text> {tagInfo.records.map((rec, idx) => ( <Text key={idx}> {rec.payloadText || rec.payloadUri || '未知内容'} </Text> ))} </ScrollView> )} </View> ); }页面逻辑非常简单,因为复杂的事情都在原生解析和工具类里做掉了。实际项目中,我还会加一个“已扫描设备列表”,把每次读到的标签按时间戳存进列表,方便回看。这个列表可以直接用SectionList来做,按天分组。
6. 常见问题与排查实录
RNOH加NFC这套组合,网上可搜到的踩坑记录不多,很多问题只能现场试错。我把实际遇到的高频问题整理成了一份速查表,按排查顺序排列。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 标签贴近开发板完全无反应 | NFC服务未开启或驱动未加载 | 系统设置查看NFC开关,hdc shell hidumper -s NFC查看服务状态 |
| 能扫描到标签但拿不到NDEF数据 | 标签未格式化或为空标签 | 用专业写入工具先写入一条合法NDEF记录再测试 |
| 读出来的文本乱码 | UTF-8/UTF-16编码判断错误 | 检查payload首字节bit 7标志位 |
| 连续扫描时页面重复刷新 | 缺少节流/去重 | 在NfcManager里加时间戳和tagId去重 |
| RN页面首次打开白屏 | RNOH加载慢或日志没输出 | 排查JS bundle路径、等待时间、重启App |
| 编译报错NAPI类型不匹配 | ArkTS和C++类型映射错误 | 统一用string和JSON.stringify传参 |
| 权限请求成功但读取失败 | 动态权限二次确认未处理 | 在页面启动时主动申请并确认授权结果 |
6.1 扫描无响应的五步排查
如果标签贴近设备后,RN页面什么事都没发生,我的排查顺序是:
第一步,确认真机NFC开关是开的。OpenHarmony的部分开发板默认关闭NFC,而且不提示用户,这个坑最浅但最容易忽略。第二步,确认权限有加且已动态授权。OpenHarmony的权限模型要求运行时弹窗确认,如果用户点了拒绝,后面所有调用都是静默失败。第三步,用系统自带工具验证标签。找一台Android手机装个NFC Tools,把同一张标签贴上,如果Android也读不到,说明标签坏了或者格式不对。第四步,看HDC日志里有没有NFC服务报错。hdc shell hilog | grep NFC是最直接的排查手段,错误信息里通常会写明协议不匹配或超时。第五步,检查标签类型。如果标签是Felica(Type 3)而设备NFC天线只适配了Type A/B,读不到是正常的。
这个排查顺序能覆盖90%以上的“无反应”问题。别一上来就怀疑代码,硬件和系统层面的问题概率更大。
6.2 NDEF数据读出来是乱码
乱码问题的根源几乎都在编码判断上。NDEF文本记录的第一字节,bit 7为0表示UTF-8,为1表示UTF-16。很多标签写入器在写中文时默认用UTF-8,但部分进口工具默认用UTF-16,而文本长度记录的是字节数不是字符数,解析错编码会直接导致乱码或截断。
我在原生解析代码里,加了一个回退机制:如果按UTF-8解码后发现包含大量不可见字符(比如U+FFFD替换符),就尝试按UTF-16解码一次。这个策略在实测里能把兼容性拉高不少。但要注意,回退解码只能用于展示型场景,不能用于后续的逻辑匹配,因为不可靠。生产环境还是要靠写入端约定统一的编码标准。
6.3 TagLost与超时错误
TagLost是NFC开发中特别常见的异常,意思是“标签在通信过程中离开了感应区”或“设备在等待标签响应时标签已移走”。这个问题在手持设备上尤其明显,因为人手会难免抖动。遇到TagLost,正确做法不是报错,而是提示用户重新贴近。
我在原生层捕获TagLostException后,会向JS层抛一个友好错误码TAG_LOST,JS侧拿到后弹一个Toast“请重新贴近标签”,同时保持扫描状态不变。这样用户体验会自然很多,而不是直接退出页面。
6.4 真机调试与日志查看技巧
RN页面和OpenHarmony原生日志是两套体系,排查时要两边一起看。
RN侧的console.log可以通过DevEco Studio的Log面板看到,因为RNOH会把JS日志转发到hilog。但注意,console.info和console.warn的输出级别不同,DevEco默认过滤了verbose级别的日志,有时候看不到是级别问题,不是没打印。
原生侧日志统一用hilog查看:
hdc shell hilog -r hdc shell hilog | grep -i nfc hdc shell hilog | grep -i ndef如果怀疑RNOH本身有问题,可以开RNOH的调试模式,看RNOH_LOG级别的输出。常见白屏问题里,最多的是JS bundle路径配置错误,或者双端(OpenHarmony和RN)的版本不匹配,日志里通常会有明确的Unable to load script或Dependency mismatch字样。
我还建议在原生解析代码里加上分阶段日志:收到onNotify打一条、检查到NDEF能力打一条、解析完成打一条。这样一旦出现问题,通过日志能快速定位是“没检测到”“不支持NDEF”还是“解析异常”。这种埋点习惯在嵌入式平台上特别管用,因为现场排查往往没有断点调试条件。
写在最后的一点个人体会
NFC读取这种功能,说难不难,无非是“拿到tag、解析NDEF、渲染数据”三步。但真要落到OpenHarmony加RN这套组合上,细节却比想象中多得多。我个人最大的体会是,跨语言边界传数据时一定要收敛数据结构,能传字符串绝不传对象,能JSON序列化绝不裸传。这个原则帮我躲过了很多NAPI类型映射的暗坑。
另外一点,如果你打算在项目里批量写入标签(比如给一批库存商品快速写入初始信息),我强烈建议把写入能力也做成原生模块,和读取放到同一个模块里统一管理。写入逻辑比读取复杂,需要校验标签状态、按区块写入、验证写入结果,用RN写纯逻辑还可以,但涉及底层扇区操作最好还是留在ArkTS侧。一个库里把读写都封好,后续维护起来会轻松很多。
最后再分享一个小技巧:开发期间准备三张不同厂商的标签放在桌上,一张Type 2、一张Type 4、一张只支持RFID不支持NDEF的。每次改完代码,三张全扫一遍,能让你在30秒内发现兼容性回归。这个习惯我在做NFC项目时一直保留,建议你也试试。