
去年团队接了一个让不少人皱眉头的需求在搭载OpenHarmony的工业手持机上做NFC巡检功能要求贴近标签后能读取UID和标签数据还要能对接后端巡检系统。当时Android和iOS的App已经用React Native写完并在多个项目里跑得很稳大家第一反应是“OpenHarmony上能不能也跑RN”调研之后发现React Native的OpenHarmony适配版已经能支撑常规业务开发NFC这种系统能力只要写一层原生桥接就可以。这篇文章就把整个落地方案、核心代码以及调试中遇到的高频坑完整记录下来给同样准备在OpenHarmony上做RN NFC开发的团队一个参考。1. 项目背景与整体设计1.1 需求场景手持机上的NFC巡检这个项目最典型的场景是仓库和机房巡检。巡检员拿着OpenHarmony工业手持机走到一个巡检点把机器背面的NFC天线区域贴近墙上的标签机器屏幕立刻弹出这个巡检点的编号、历史巡检记录以及需要做的检查项。整个过程中巡检员不需要任何输入操作读卡就是入口动作。NFC标签本身并不存太多业务数据标签里一般就三样东西UID标签唯一序列号相当于身份证。NDEF消息标准化的数据块里面可以放一个文本、一个URL或者自定义的行业记录。技术类型ISO14443 Type A/B、Type FFelica等不同设备支持的卡型不同。工业场景里我们绝大多数用的是Type A的Mifare Classic或NTAG系列标签。NTAG只读UID能写NDEF成本低Mifare Classic带加密扇区适合门禁和部分高安全场景。开发时先把这两类都兼容掉后面省很多事。读卡动作本身很简单真正的复杂度在于RN应用怎么把“系统层发现标签”这个事件接回到JS层并且保证整个链路稳定、不白屏、不丢事件。1.2 为什么选React Native而不是原生ArkTS选型会上主要讨论了三个方案ArkTS原生开发、RNOpenHarmony适配版、Flutter。我们的结论很直接选RN。方案优点缺点ArkTS原生直接调用系统API性能最好需要单独的ArkTS开发团队业务代码无法跨端复用RN OpenHarmony复用现有RN组件和业务逻辑前端人员上手快系统API需要自己写桥接版本适配有坑FlutterUI一致性好渲染性能不错团队不熟DartNFC能力同样要写原生插件理由其实很朴素我们已经有20多个页面、几十个业务组件的RN代码Android和iOS都在跑这中间包括了登录、任务列表、表单提交、拍照上传。如果OpenHarmony端用ArkTS重新写一遍等于维护三套代码。而RN的OpenHarmony适配版虽然不是官方主推路线但核心运行环境已经比较成熟常见的ScrollView、FlatList、网络请求、图片加载都能正常工作。NFC的桥接虽然绕不开但桥接层只需要很小一个面注册标签监听、读UID、读NDEF、返回事件。把这个面控制好整体风险就可控。1.3 整体架构RN JS层到NFC硬件的调用链整个链路可以拆成四层JS业务层React组件负责页面渲染、用户交互、数据上传。RN RuntimeOpenHarmony的RN适配层提供JS引擎、原生组件映射、TurboModule机制。Native桥接层用ArkTS写的NFC Module负责调用系统NFC接口并把结果转成事件或Promise返回给JS。系统NFC能力OpenHarmony的NFC Kit包括标签发现、NDEF解析、卡模拟等能力。事件流动方向是这样NFC标签贴近设备天线 - 系统NFC服务发现标签 - NFC Kit回调Native Module - Native Module组装数据 - 通过RN事件机制发给JS - React组件更新UI。数据流方向则反过来JS发起读卡请求 - 调用Native Module - 系统开始扫描标签 - 拿到原始数据返回JS。设计时有一个原则很重要Native层只做数据采集和转换不做业务判断。比如某个UID对应哪个巡检点、要不要提示重复读卡这类事情放JS层做。原因是业务规则经常变JS层改完走热更新即可Native层最好保持稳定。2. 环境准备与工程创建2.1 开发环境的一次性搭建先列一下我当时准备好的环境照着装就行组件版本建议说明DevEco Studio5.0及以上带OpenHarmony SDK和模拟器OpenHarmony SDKAPI 12及以上NFC Kit在API 12里已经比较完整Node.js18或20 LTSRN CLI依赖React Native OHOS适配版react-native-oh-tpl/react-native 对应你RN版本0.72或0.73系列都有适配真机必须带NFC模块模拟器不支持NFC读卡别指望模拟器这里提醒一句OpenHarmony的API版本和RN适配版本之间是有绑定关系的不是随便取最新就能跑。建议先看react-native-oh-tpl/react-native的release note找到对应你RN主版本的适配版本再装配套的DevEco版本。我一开始就吃了这个亏装了最新RN 0.74适配包结果现有代码还能跑但有个原生依赖就开始报错后来退回0.73才稳定。2.2 创建RN工程并接入OpenHarmony平台用RN CLI初始化一个项目npx react-native-oh-tpl/cli init NfcRnApp cd NfcRnApp这个CLI创建出来的工程结构比普通RN项目复杂多了一个entry目录就是OpenHarmony的应用壳工程。里面主要有entry/src/main/ets/ArkTS原生代码比如我们的NFC桥接模块就放这里。entry/src/main/ets/pages/HarmonyOS应用入口Ability。App.tsxRN的业务入口等于是React组件树的根。工程创建完先跑一个空页面确认RN端到端跑通再开始写NFC。别一上来就叠NFC否则遇到问题没法判断是RN环境问题还是NFC问题。2.3 真机联调配置OpenHarmony手持机连USB调试先确认hdc list targets能看到设备。然后设置端口反向代理让设备访问电脑上的Metro服务hdc reverse tcp:8081 tcp:8081这个操作对应开发时的React Native经典操作忘了这一步你在真机上跑Debug版App能安装但加载不到JS bundle直接白屏。Realse包不用Metrobundle会打包进应用。打包命令一般是npm run build:release不同模板可能略有差异执行前看下package.json里的script。打包完安装到设备上测试。3. 桥接层设计与原生NFC模块3.1 桥接层对外接口设计写桥接前先把接口定清楚。我在项目里对外暴露了三个能力方法说明返回startScan()启动标签监听持续读卡无stopScan()停止标签监听无readOnce()单次读卡读到一张卡后自动停止PromiseTagData同时还会通过事件通道下发一个onTagRead事件里面携带TagData对象。TagData的定义如下interface TagData { uid: string; // 标签唯一ID十六进制字符串 technology: string[]; // 技术支持类型如 [NfcA, Ndef] isNdef: boolean; // 是否NDEF标签 ndefText?: string; // NDEF解析出的文本内容 records?: string[]; // 原始NDEF记录列表 }接口这么设计主要考虑到两种使用场景巡检页面需要持续读卡扫一个换一个而初始化标签或单标签查询时用readOnce就够了。事件和Promise混着用不矛盾事件适合持续流Promise适合一次性操作。3.2 用ArkTS实现NFC原生模块下面这段是核心中的核心。基于OpenHarmony API 12的NFC Kit接口命名以你SDK里的.d.ts为准不同小版本可能微调。先看最简版的原生模块代码// entry/src/main/ets/nfc/NfcReadModule.ets import { tag } from kit.NFCKit; import { TurboModule } from rnoh/react-native-openharmony/ts; export class NfcReadModule extends TurboModule { private onTagNotify (tagInfo: tag.TagInfo) { const uid tagInfo.getUid(); const technologies tagInfo.getTechnology(); const isNdef tagInfo.isNdef; let ndefText ; if (isNdef) { const ndefTag tag.getNdefTag(tagInfo); const ndefMsg ndefTag.getNdefMsg(); ndefText this.parseNdefMessage(ndefMsg); } const result: TagData { uid: this.toHexString(uid), technology: technologies, isNdef, ndefText, }; this.emit(onTagRead, result); }; startScan(): void { tag.on(notify, this.onTagNotify); } stopScan(): void { tag.off(notify, this.onTagNotify); } private parseNdefMessage(msg: tag.NdefMessage): string { if (!msg) return ; const records msg.getNdefRecords(); let text ; for (const record of records) { const payload record.getPayload(); // NDEF Text Record 的 payload 首字节是状态位接着是语言码长度再往后才是 UTF-8 文本 if (payload payload.length 0) { const status payload[0]; const langCodeLen status 0x3f; const content payload.slice(1 langCodeLen); text this.decodeUtf8(content); } } return text; } private decodeUtf8(data: Uint8Array): string { // 这里用 TextDecoder 或手动解码NDEF 标准规定文本编码为 UTF-8 const decoder util.TextDecoder.create(utf-8); return decoder.decodeToString(data); } private toHexString(data: Uint8Array): string { return Array.from(data) .map(b b.toString(16).padStart(2, 0)) .join(:) .toUpperCase(); } }这段代码有几个地方值得注意。tag.on(notify, callback)是注册标签发现回调。这个监听会在系统检测到NFC标签进入读写器范围时触发注意回调参数拿到的是TagInfo不是直接的NDEF数据。设备贴近标签又拿开、再贴近回调会重新触发这个行为后面在JS层处理防抖。tagInfo.isNdef用来判断标签是否支持NDEF。很多Mifare Classic卡不带NDEF格式但UID一定能读到。工业上有些老标签只有UID业务仍然可以靠UID绑定所以读取逻辑里先把UID拿稳NDEF作为附加信息。NDEF解析时最容易被坑的是Text Record的payload格式。首字节不是文本内容它包含状态位和语言码长度要跳过状态位和语言码才是真正的文本字节。刚开始我没这块经验直接用toString(utf-8)硬转解析出来的中文全是乱码。后来对照NDEF规范重新写了解码逻辑才正常。如果标签里放的是URL而不是文本解析方式类似URIRecord有前缀编码表需要按规范映射成完整URL。项目里大多数标签用文本记录就够了URL的场景我们现在没深入。3.3 模块注册与导出Native模块写完不注册RN JS层是找不到的。在OpenHarmony的RN工程里模块注册一般在工程的RNOHCorePackage或者对应的Package列表中完成// entry/src/main/ets/rnoh/NfcPackage.ets import { TurboModulePackage } from rnoh/react-native-openharmony/ts; import { NfcReadModule } from ../nfc/NfcReadModule; export class NfcPackage extends TurboModulePackage { createNativeModules() { return [new NfcReadModule(this.ctx)]; } }然后在入口Ability的RNOHCoreContext配置里把这个Package加进去this.rnohCoreContext new RNOHCoreContext(abilityContext, [ new NfcPackage() ]);具体文件位置和写法以你使用的RN OHOS模板为准不同小版本可能有差异。但整体思路不变写下桥接模块注册进RN运行时。注册好后JS侧就可以这样拿import { NativeModules } from react-native; const { NfcReadModule } NativeModules;如果打印NfcReadModule为undefined基本就是注册没生效或者Package没加到Context里。4. RN侧业务实现4.1 JS层封装可复用的NFC管理器原生模块暴露的方法比较底层直接在页面里用会很啰嗦而且NativeEventEmitter的监听需要手动管理页面卸载时忘了解绑容易内存泄漏。我在JS层封装了一个NfcManager工具类把原生调用和事件监听收拢起来// src/services/NfcManager.ts import { NativeModules, NativeEventEmitter } from react-native; const { NfcReadModule } NativeModules; export type NfcTagData { uid: string; technology: string[]; isNdef: boolean; ndefText?: string; }; class NfcManager { private emitter new NativeEventEmitter(NfcReadModule); private listener: any null; startScan(onTag: (tag: NfcTagData) void) { this.stopScan(); this.listener this.emitter.addListener(onTagRead, onTag); NfcReadModule.startScan(); } stopScan() { NfcReadModule.stopScan(); if (this.listener) { this.listener.remove(); this.listener null; } } async readOnce(): PromiseNfcTagData { return await NfcReadModule.readOnce(); } } export default new NfcManager();封装之后页面上调用就非常干净。4.2 扫描页面交互设计巡检页面最简单可靠的交互状态是初始状态显示“请贴近标签”按钮为“开始扫描”。扫描中按钮变“停止扫描”等待标签。读到标签弹出数据卡片展示UID和NDEF文本并自动停止扫描。异常提示NFC不可用或权限不足引导去系统设置。// src/screens/ScanScreen.tsx const [scanning, setScanning] useState(false); const [lastTag, setLastTag] useStateNfcTagData | null(null); const lastReadTime useRef(0); const handleTag useCallback((tag: NfcTagData) { // 同一个标签在短时间内可能被多次触发做2秒防抖 const now Date.now(); if (now - lastReadTime.current 2000 lastTagRef.current?.uid tag.uid) { return; } lastReadTime.current now; setLastTag(tag); NfcManager.stopScan(); setScanning(false); }, []);防抖逻辑一定要加。工业手持机贴近标签的瞬间系统可能连续回调两三次不做防抖巡检页面会弹好几个结果框非常烦人。读卡结果展示上UID用等宽字体显示并支持整段复制方便运维排查。NDEF文本字段可以拼接设备编号、巡检点位等业务信息。如果标签内容包含多个record可以在列表里展开显示。4.3 数据落地与上报读到的数据不能只活在页面上需要存下来并同步到后端。我在项目里用本地缓存加远端上报两段式处理const saveTagData async (tag: NfcTagData) { // 1. 本地缓存断网也能记录 const cacheKey tag_${tag.uid}_${Date.now()}; await AsyncStorage.setItem(cacheKey, JSON.stringify(tag)); // 2. 异步上报 fetch(https://api.example.com/tag/read, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ uid: tag.uid, ndefText: tag.ndefText, deviceId: DeviceInfo.deviceId, timestamp: Date.now(), }), }).catch(err { console.warn(上报失败稍后重试, err); }); };上报接口用HTTPS别走明文HTTP。有人在交流群里问“OpenHarmony上能不能搞FTP服务向服务器传数据”技术上是能但生产环境我强烈不推荐用FTP做数据上报。FTP是明文协议设备在弱网环境重建链接也很麻烦。用HTTPS POST或者WebSocket都更稳服务端接口也好写。如果你们环境特殊只让开21端口至少也要在这个服务前做内容加密否则巡检数据很容易被截取。5. 踩坑实录与问题排查5.1 React Native启动白屏的排查思路这个坑我在项目初期整整卡了两天现象就是App安装到真机上打开后屏幕一片白不报错也没有任何提示。排查步骤一步步来先看Metro终端有没有输出。如果Metro运行正常但页面白屏问题大概率在加载链路。确认端口反向代理有没有配。hdc reverse tcp:8081 tcp:8081没执行Debug包加载不到bundle白屏没商量。看原生侧日志。RN OHOS的容器会打印loadScript日志如果一直没出现说明原生容器没把bundle拉起来。Release包也白屏的话检查assets里有没有生成bundle文件。打包脚本有时候没把bundle拷进资源目录。最容易忽略的是React Native版本和RN OHOS适配版本的兼容性。有一次我把RN从0.72升级到0.74设备上启动直接白屏错误日志指向Hermes初始化失败。看了release note才发现适配版要搭配特定Hermes版本升级时不能只升RN主包react-native-harmony相关依赖得一齐升。5.2 NFC读不到标签的几种情况NFC回调完全不触发先别怀疑代码按需要排查的顺序来设备有没有NFC手持机有高低配低配版本可能砍了NFC硬件。在设置里查看是否有NFC开关。NFC开关打开没有工业设备有些定制ROM把NFC默认关闭需要在设置里打开。权限声明在module.json5里确认NFC相关权限已声明。注意不同API版本的权限名有调整直接翻SDK文档复制最新的。应用是否在前台系统标签notify回调一般只派发给前台应用RN页面切到后台就收不到这是正常行为。天线位置手持机的NFC天线区域通常在机身背面或侧面有些机器贴了标签在屏幕上方但读不到可以买个测试卡反复移动位置找天线区。定制硬件选天线时也要参考圆形天线设计工具的画法和匹配尺寸线圈中心和标签中心对齐最灵敏。还有一种情况设备系统自带了一个NFC读取应用抢占了标签派发的优先级。解决办法是关掉系统应用或者在系统设置里把本应用设为默认NFC处理应用。5.3 常见问题速查表把项目里反复出现的几个问题整理一下现象可能原因处理方法启动白屏Metro无连接端口反向代理丢失执行hdc reverse tcp:8081 tcp:8081后重开App启动白屏原生报Hermes错误RN版本和OHOS适配版本不匹配按release note对齐版本全部依赖一起升其它页面正常NFC回调不触发NFC权限未声明或应用不在前台检查module.json5把页面留在前台再读卡NDEF文本乱码没解析Text Record的头字节和语言码按NDEF规范跳过状态位和语言码长度同一标签连续多次弹出系统重复回调JS层加时间戳防抖UID偶尔变化防碰撞导致不同扇区被读到工业场景用全UID匹配不接受部分匹配读卡卡顿响应延迟高扫描回调里做了耗时操作Native层只采集不解析业务解析放JS或异步线程其中NDEF乱码这个问题最有代表性。NDEF虽然规范统一但Tag写的工具五花八门有些工具写进去的Text Record头字节不规范你的解析代码就得做兼容。我们后来加了解析失败的兜底直接把payload原始字节转十六进制字符串返回至少操作员能看到内容不至于完全抓瞎。6. 关于NFC安全与合法使用的几个提醒6.1 加密门禁卡复制和中继攻击的坑千万别踩NFC天然和门禁卡绑定在一起网上也有一堆相关关键词比如“NFC解密工具”“NFC Tool破解VIP秘钥”“复制加密门禁卡”“nfc密钥库keys”之类的帖子。这里我必须把安全红线说清楚。加密门禁卡不是不能复制吗技术上部分老旧加密卡确实存在已知密钥库市面上所谓“NFC密钥库keys”就是把密钥字典下发给工具逐个试出扇区密钥然后整体克隆。但这类行为在绝大多数场景下都违法违规。未经授权复制门禁卡轻则违反物业规定重则可能涉及非法侵入的法律问题。公司项目里更不能用这种手段一旦出事产品连带责任跑都跑不掉。中继攻击是另一个热度不低的词。原理是准备两个设备A和BA贴近真实门禁卡B贴近门禁读卡器中间通过无线或网络桥接信号就能在卡主不在门禁旁边时远程开门。这已经不是技术问题是很明确的非法行为动辄能扯到盗窃。见到任何销售中继设备的直接拉黑涉诈嫌疑很大。我们团队内部定了两条规矩App里不做密钥注入功能不做UID复制导出功能。技术方案上需要在代码层面屏蔽对MifareClassic扇区认证相关API的直接暴露除非有特别的安全评审。6.2 批量写入的合法场景和密钥管理“NFC批量写入”这个热词本身是中性需求比如给一批空白巡检标签初始化NDEF内容、给商品贴标、给展品做个签到点。批量写入的代码很简单就是循环调用NDEF写入接口const batchWriteTags async (records: NdefRecord[], tagIds: string[]) { for (const tagId of tagIds) { try { const tagInfo tag.getTagInfo(tagId); const ndefTag tag.getNdefTag(tagInfo); ndefTag.writeNdefMsg(buildNdefMessage(records)); } catch (e) { // 单张失败不影响整体记录后继续 console.error(标签 ${tagId} 写入失败, e); } } };批量写入时最容易忽略两个点一是写入前先擦除旧数据否则可能残留上一批内容二是写入后必须读回校验写入成功不代表内容正确。我见过一次写入报告全绿结果批量读回时发现全部少了个字节的情况后来查出来是循环太快NFC写操作没有完全落盘。密钥管理方面凡是涉及加密扇区写入的项目密钥绝不能硬编码在客户端。哪怕RN JS代码打包混淆过也能被逆向工具扒出来。密钥应该存在服务端设备端通过会话凭证临时获取或者直接放在安全元件SE里。产品在设计阶段就要把这条纳入安全评审后补会非常痛苦。6.3 再往前一步巡检数据自动上报与链路扩展读卡功能和巡检业务跑通以后还可以做很多扩展。高频的方向有两个一是批量巡检模式。拿着手持机在机房走一圈连续读十几个标签全部暂存在本地巡检结束后统一上报。这个做起来不难把防抖放开换成“停止扫描”手动结束然后批量保存即可。二是标签初始化工具独立化。把写标签的流程从巡检App里拆出来做成一个管理端工具。仓库管理员批量写标签、校验标签、导出标签清单和生产巡检彻底隔离权限也好控制。数据上报接口目前我们是HTTPS POST后续如果标签扫码频率很高考虑改成批量接口消息队列减少一次一条的网络开销。之前有人提过OpenHarmony上自己搭FTP推送文件我上面说了不建议有一定网络维护经验的人都知道在移动设备上做FTP客户端复用率很低会被弱网、端口限制反复折腾用HTTP接口才是稳妥路径。最后再分享一点个人体会RN和OpenHarmony这套组合做NFC读卡是可行的但桥接层一定要当成独立小项目来维护接口设计稳了后面所有业务页面都跟着省事。安全那根弦也别松NFC能力越强越要克制不该碰的功能坚决不做这是做硬件配套软件最基本的职业底线。