尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

React Native 通知库迁移 OpenHarmony 的桥接实践

React Native 通知库迁移 OpenHarmony 的桥接实践 1. 为什么一个同名 iOS 库会出现在 OpenHarmony 工程里1.1 RNOH 里的三方库到底是怎么“适配”的先说一个很多从 React Native 迁移到 OpenHarmony 的开发者都会踩的误区以为npm install装完一个 RN 三方库构建出 hap 包后就能直接用。实际上RN 三方库的生态基本是「JS Android 原生 iOS 原生」三件套Android 和 iOS 端各自有原生模块实现。到了 OpenHarmony这套架构完全不同没有 Android 的 Gradle 依赖没有 iOS 的 CocoaPods也没有 Android 的.so和 iOS 的 framework。RNOHReact Native for OpenHarmony能提供给我们的是一个 JS 引擎Hermes 或 QJS 自定义 Bridge ArkTS 运行时环境。三方库要跑起来必须补上一个 OHOS 专用的原生实现层。RNOH 的适配方式大致分四类纯 JS 库。比如字符串处理、状态管理这类不涉及系统能力的安装后直接跑基本不用改。已经有人做了 OHOS 适配的库。社区里很多库被移植过包名通常带react-native-oh-tpl/前缀这类你直接换包名安装即可。有 iOS/Android 原生实现但 OHOS 侧没有适配的库。这是最需要花精力的一类也是本文要解决的核心问题。重度依赖第三方 SDK 的库。比如地图、支付这需要厂商提供 OHOS SDK基本不能指望 RN 社区替你搞定。react-native-push-notification-ios就是典型的第三类有完整的 JS 接口和 iOS 原生实现但在 OHOS 上没有现成适配包。名字虽然带ios它的 JS API 其实长期以来被很多跨平台项目当作“通用通知接口”在使用业务代码里到处是onRegister、onNotification、requestPermissions。因此在迁移到 OHOS 时最省成本的做法不是重写业务侧而是保留 JS 层接口在 ArkTS 侧实现一套等价的原生桥接。1.2 push-notification-ios 这类库的价值边界很多人在立项阶段会问既然 OHOS 有自己的通知 API为什么不直接写 ArkTS 通知代码非要绕一圈去兼容一个 iOS 库这取决于项目从哪里来。如果是从零开发一个只服务于 OHOS 的应用当然直接上手ohos.notificationManager是最干净的方案。但对于已有 RN 项目、需要多端同步迭代的团队来说业务代码里可能已经有几百处调用了createPushNotification()的实例方法把这些调用全部改造成 OHOS 原生方式是一件极其容易引发回归、且测试成本不低的事情。所以我个人的判断标准是业务层与系统能力的边界是否清晰。如果业务层只是发通知、收点击响应、拿设备标识那么通过适配层保持 API 语义不变是性价比最高的方案。如果业务层需要用到通知栏大图、自定义布局、内联回复这类高度依赖系统 UI 的能力那适配成本会飙升直接重写业务层反而更快。这篇文章接下来会先拆解这个库的内部逻辑再给出一条“最小集成路径”最后重点讲桥接层怎么用 ArkTS 实现以及我在 RK3568 / RK3588 开发板、Turborepo Monorepo、6.0 编译环境下踩过的坑。2. 先把 react-native-push-notification-ios 拆开看2.1 JS 接口层它定义了通知交互的“标准语言”react-native-push-notification-ios的源码结构其实不复杂。入口文件对外暴露了一个createPushNotification工厂函数返回一个实例。这个实例上挂着一组方法业务代码中通常会这样调用import { createPushNotification } from react-native-push-notification-ios; const pushNotification createPushNotification(); pushNotification.onRegister((token) { console.log(device token:, token); }); pushNotification.onNotification((notification) { console.log(notification payload:, notification); }); pushNotification.requestPermissions();这组 JS 方法背后实际上是在调用一个叫NativePushNotificationIOS的原生模块。原生模块由 iOS 端提供JS 层通过NativeModules.NativePushNotificationIOS拿到句柄然后逐个调用方法。理解这一点很关键因为集成到 OHOS 时我们根本不需要动 JS 层只需要保证 OHOS 侧也存在一个名为NativePushNotificationIOS的 TurboModule并且实现了 JS 层调用的那几个方法即可。这就是“兼容”的本质。2.2 iOS 原生层底层依赖的 UNUserNotificationCenteriOS 端的原生实现在历史上通常依托RCTPushNotificationManager或经过重构的自定义 Manager。它具体做了这几件事register调用UNUserNotificationCenter请求授权拿到 device token 后通过onRegister回调给 JS。presentLocalNotification通过UNNotificationRequest构建本地通知加入通知中心。getInitialNotification在 App 冷启动时读取启动参数中携带的远程通知 userInfo。点击事件响应通过 AppDelegate 的didReceiveNotificationResponse方法把用户点击的通知对象回调给 JS。cancelAllLocalNotifications、cancelLocalNotifications清理通知。iOS 的通知模型是“JSON dict 为中心”的。一个通知对象长这样{ alertTitle: 标题, alertBody: 正文内容, userInfo: { orderId: 123456, type: new_order }, fireDate: 1700000000 }iOS 端拿到这个对象后把alertTitle、alertBody显示在通知栏把userInfo原样存起来等用户点击时再带回来。这个模型简单粗暴好处是通用性极强坏处是很多东西要靠约定。2.3 通知模型差异iOS userInfo 和 OHOS NotificationRequest 的映射到了 OpenHarmony情况就不一样了。OHOS 的通知 API 走的是结构化NotificationRequest路线一个本地通知的发布大致长这样不同 API 版本字段名略有差异以 SDK 声明为准import notificationManager from ohos.notificationManager; const request: notificationManager.NotificationRequest { id: 1, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: 通知标题, text: 通知正文, additionalText: 附加信息 } }, notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION }; notificationManager.publish(request, (err) { if (err) { console.error(publish failed:, err.code, err.message); return; } console.info(publish success); });所以桥接层要做的核心工作不是简单的 API 翻译而是设计一套消息格式转换协议。我的做法是这样的映射JS 层push-notification-iosiOS 侧OHOS 桥接侧requestPermissionsUNUserNotificationCenter 授权notificationManager.requestEnableNotification()presentLocalNotification构建 UNNotificationRequest 并加入中心把 JS 字典转换为 NotificationRequest再publishgetInitialNotification读取冷启动 userInfo读取 EntryAbility 冷启动 want 中的参数通知点击回调AppDelegate 的 didReceive 方法WantAgent 触发拿到参数后通过 DeviceEvent 回传 JScancelAllLocalNotifications删除全部通知请求notificationManager.cancelAll()这个表格是桥接层的“需求说明书”。每一行背后都有不少细节尤其是getInitialNotification和点击回调它们在生命周期上的坑比想象中多。3. 动手前的准备工作版本匹配、设备确认与工程结构3.1 “npm install 了三方库”不等于“OHOS 能跑”RNOH 的典型工程结构不是把 RN 工程硬塞进 DevEco Studio 工程而是分两层外层是标准的 React Native 工程有package.json、node_modules、index.js等。内层是一个由脚手架react-native-oh/react-native-harmony生成的entry/ohos目录它本质是一个 OpenHarmony 应用壳工程。最终构建出的 hap 包是把外层生成的 JS bundle 打包进壳工程里的。也就是说凡是 OHOS 侧需要的原生依赖必须出现在entry/ohos的oh-package.json5里而不是单纯出现在外层 npm 依赖里。react-native-push-notification-ios没有 OHOS 原生实现所以安装它之后还需要自己在entry/ohos下创建一个 ArkTS 模块并在模块构建配置中注册才能让 JS 层的NativeModules.NativePushNotificationIOS找到对应的 TurboModule。具体步骤我在下一节给出。先提醒环境层面的准备。3.2 hdc 下的设备信息查询版本、UDID 与 product.nameOpenHarmony 日常调试基本离不开hdc命令。它跟 ADB 很相似但又不是完全一样。三个最常用的查询命令# 列出当前连接的设备 hdc list targets # 查看系统版本和完整版本名 hdc shell param get const.ohos.fullname hdc shell param get const.ohos.apiversion # 查看设备型号/开发板型号 hdc shell param get const.product.name # 查看设备 UDID测试推送、通知 token 时经常需要 hdc shell bm get --udid这里有一个隐藏的坑const.product.name这个参数在部分开发板的系统镜像中被定制成了产品代号比如RK3568、rk3588_s之类。不要在你的应用逻辑里依赖这个参数作为设备标识它不是稳定的业务 ID只适合做硬件型号识别。真正稳定的设备标识用hdc shell bm get --udid获取的 UDID。我曾经见过一个团队把const.product.name当成设备唯一 ID 传给后端做消息推送关联结果换了一块 RK3588 开发板后后端把所有消息都发到了旧设备上排查大半天才发现是设备标识字段用错了。3.3 RK3568 / RK3588 的差异与 Turborepo 下的注意点如果你在 RK3568 上跑 RNOH 应用对性能要有心理预期。它的内存和 GPU 能力相对有限RN 的 JS bundle 在 debug 模式下加载会有明显卡顿。我的建议是RK3568 上尽量用 release 包进行功能验证不要在 debug 模式下看着卡顿就以为代码写崩了。如果项目是 Turborepo 管理的 Monoreponode_modules往往被提升到仓库根目录。RNOH 的自动链接工具在扫描原生模块时遇到 symlink 和 workspace 场景有时候会漏扫。遇到这种情况可以先npx react-native-config生成一份配置文件看看依赖树或者手动在entry/ohos中声明依赖。另外OpenHarmony 6.0 编译环境与 RNOH 的版本匹配要特别注意。RNOH 每个版本都只支持特定范围的 RN 版本比如常见的 0.72.x、0.75.x。在开搞之前先查你用的 RN 版本对应的 RNOH release note确认它能跑在目标板子的系统版本上这个步骤能节省后面大量的排障时间。4. 最小集成路径把本地通知先跑起来4.1 安装、自动链接与手动注册先正常安装 npm 包npm install react-native-push-notification-ios然后用 RNOH 的自动链接工具试一下看它能否识别出这个库的 OHOS 原生模块npx react-native config大概率在nativeModules里看不到它因为包目录下没有harmony子目录。没关系我们直接手动建桥接模块。在entry/ohos下新建一个名为NotificationModule的 ArkTS 模块目录结构大致如下entry/ohos/ ├── entry/ │ └── src/main/ │ ├── ets/ │ │ ├── entryability/ │ │ ├── pages/ │ │ └── notification/ │ │ └── NotificationModule.ets │ └── module.json5 └── oh-package.json5然后在entry/ohos的模块配置里把这个模块注册为依赖。注意RNOH 0.72 之后的版本对 TurboModule 的注册方式非常敏感模块名必须与 JS 侧NativeModules中访问的名称完全一致大小写都不能错。NativePushNotificationIOS里那个大写的IOS是最容易敲错的地方。4.2 通知权限与渠道的初始化OHOS 上发通知首先要判断用户是否授权。RNOH 应用一般会在入口文件Index.ets中先请求通知权限import notificationManager from ohos.notificationManager; notificationManager.requestEnableNotification((err) { if (err) { console.error(requestEnableNotification failed:, JSON.stringify(err)); return; } console.info(requestEnableNotification success); });这个 API 会拉起系统通知授权弹窗和 iOS 的UNUserNotificationCenter授权弹窗是同一位置的作用。在部分系统镜像上如果应用已经授权过弹窗不会重复出现所以要注意在授权成功后做一个本地标记免得每次启动都调一次。通知渠道NotificationSlot是另一个容易漏的环节。建议在应用启动时初始化import notificationManager from ohos.notificationManager; const SLOT_ID default_slot; const slot: notificationManager.NotificationSlot { slotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, level: notificationManager.SlotLevel.DEFAULT, desc: 默认通知渠道, authorized: true, enableVibration: true, enableLight: true, lightColor: 0xFF0000 }; notificationManager.addSlot(slot, (err) { if (err) { console.error(addSlot failed:, err.code, err.message); return; } console.info(addSlot success); });如果不先创建渠道在某些 API 版本上直接publish会报“slot not exist”之类的错误。这个坑在模拟器上不一定复现但在真机开发板上非常常见。4.3 发布一条本地通知的 ArkTS 代码在桥接模块里presentLocalNotification的核心逻辑是把 JS 传入的通知字典转换成 OHOS 的NotificationRequest。我在这里给出一个简化版的实现import notificationManager from ohos.notificationManager; import { TurboModule } from rnoh/react-native-openharmony/ts; export class NotificationModule extends TurboModule { presentLocalNotification(options: Recordstring, Object): void { const title (options[alertTitle] ?? ) as string; const body (options[alertBody] ?? ) as string; const userInfo (options[userInfo] ?? {}) as Recordstring, Object; const request: notificationManager.NotificationRequest { id: Date.now() % 100000, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: title, text: body, additionalText: } }, notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, deliveryTime: Date.now(), showDeliveryTime: true }; notificationManager.publish(request, (err) { if (err) { console.error(publish failed:, JSON.stringify(err)); return; } console.info(publish success); }); } }这里用Date.now() % 100000生成通知 ID 是一种妥协做法因为 OHOS 通知 ID 不能重复如果同一秒内发多条通知会互相覆盖。实际项目中建议维护一个自增 ID 计数器。4.4 用适配层保住 JS 业务代码的兼容性上面的桥接模块注册好后JS 侧有两种接法。第一种是只管自己的方法名import { NativeModules } from react-native; NativeModules.NativePushNotificationIOS.presentLocalNotification({ alertTitle: 新订单, alertBody: 您有一笔新的订单待处理, userInfo: { orderId: 123456 } });第二种也是我更推荐的做法写一个薄适配层把react-native-push-notification-ios的工厂函数输出替换成调用 OHOS 桥接模块的包装。这样业务侧可以继续使用pushNotification.onRegister(...)这类写法后续要再切回 iOS只需要改适配层的内部实现。适配层的另一个重要职责是事件分发。OHOS 桥接模块中通知点击事件是通过 RNOH 的emitDeviceEvent发到 JS 侧的。适配层需要监听这些事件并调用业务侧注册的onNotification回调。这部分逻辑放到桥接层一起讲。5. 桥接层工程化把 iOS 的 API 翻译成 ArkTS 通知服务5.1 TurboModule 骨架与事件通路RNOH 的 TurboModule 写法核心是一个继承自TurboModule的类加上装饰器声明然后重写getMethods或者使用方法装饰器。各版本的装饰器写法有差异下面给出我在 0.72.x 系列上验证过可行的骨架import { TurboModule, TurboModuleContext } from rnoh/react-native-openharmony/ts; export class NativePushNotificationIOSModule extends TurboModule { private notificationMap: Mapnumber, Recordstring, Object new Map(); private idCounter: number 1; constructor(ctx: TurboModuleContext) { super(ctx); } requestPermissions(): void { notificationManager.requestEnableNotification((err) { if (err) { this.ctx.logger.error(requestEnableNotification failed); return; } // 授权成功后可以通过事件或回调通知 JS 侧 this.ctx.rnInstance.emitDeviceEvent(remoteNotificationsRegistered, {}); }); } presentLocalNotification(options: Recordstring, Object): void { const id this.idCounter; this.notificationMap.set(id, options); const title (options[alertTitle] ?? ) as string; const body (options[alertBody] ?? ) as string; const request: notificationManager.NotificationRequest { id: id, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: title, text: body } }, notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION }; notificationManager.publish(request); } }事件通路是所有桥接层最容易出问题的地方。JS 侧通过DeviceEventEmitter接收事件而 OHOS 侧通过this.ctx.rnInstance.emitDeviceEvent发送。两边的事件名必须完全一致。建议集中管理事件名常量比如remoteNotificationsRegistered、notificationOpened、localNotificationReceived不要把字符串散落在代码里否则调用方改了一个字符就会造成“事件发不出去但又不报错”的诡异问题。5.2 getInitialNotification 与点击事件的完整链路getInitialNotification在 iOS 上对应冷启动时读取 remote notification 的 userInfo。在 OHOS 上这个信息藏在 EntryAbility 的onCreate参数 want 里。我的实现思路是在 EntryAbility 启动时把 cold-start want 里的参数保存到一个单例对象或者全局变量中桥接模块暴露getInitialNotification()方法JS 侧在 App 初始化阶段调用这个方法读取保存的参数并返回。关键代码如下EntryAbility 侧import { common, Want } from kit.AbilityKit; import { GlobalThisHelper } from ../common/GlobalThisHelper; export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: common.LaunchParam): void { const initialNotification want?.parameters?.[notificationUserInfo]; if (initialNotification) { GlobalThisHelper.getInstance().setInitialNotification(initialNotification); } } }桥接模块侧getInitialNotification(): Object | undefined { const initial GlobalThisHelper.getInstance().getInitialNotification(); return initial ?? undefined; }这个方案有个细节要提醒冷启动参数只在进程被系统拉起时才存在。如果应用已经在后台用户从通知栏点击通知走的是onNewWant而不是onCreate。所以你的 EntryAbility 同时要处理onNewWant场景onNewWant(want: Want, launchParam: common.LaunchParam): void { const notificationUserInfo want?.parameters?.[notificationUserInfo]; if (notificationUserInfo) { this.ctx.rnInstance.emitDeviceEvent(notificationOpened, notificationUserInfo); } }判断“冷启动还是热启动”是点击通知回调的主要逻辑分歧点。我的通用做法是冷启动时保存参数等 JS 侧调getInitialNotification时返回热启动时直接 emit 事件。两边互补避免重复。5.3 线程与生命周期最容易踩坑的地方桥接层里第二个高频坑是线程。notificationManager.publish的回调、WantAgent 的回调都不保证在 JS 线程执行。如果你在回调里直接调用this.ctx.rnInstance.emitDeviceEvent在某些 CPU 架构或高并发场景下可能出现事件丢失或时序错乱。我的建议是把所有发送给 JS 的事件都封装到一个统一的emitToJs(eventName, payload)方法中在这个方法内部对调用线程做一份串行化处理private emitToJs(eventName: string, payload: Object): void { this.ctx.uiTaskQueue.executeTask(() { this.ctx.rnInstance.emitDeviceEvent(eventName, payload); }); }uiTaskQueue可以把任务调度到 UI 线程串行执行极大降低线程切换带来的事件竞争问题。这个封装成本很低但在真机上实测能减少大约一半的偶发回调丢失问题。生命周期方面onNewWant触发时RNInstance 可能还未完成 JS 侧监听器注册。我的做法是先缓存事件延迟 300ms 再发送或者等 RN 加载完成标记。总之不要假设onNewWant一定发生在 JS 监听器就绪之后。6. 构建、部署与实战排障记录6.1 hap 构建产物与缓存清理RNOH 工程的构建链路一般分两步# 第一步生成 JS bundle npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output ./entry/ohos/entry/src/main/resources/rawfile/bundle.harmony.js --assets-dest ./entry/ohos/entry/src/main/resources/rawfile这一步生成的bundle.harmony.js会被打包进 hap 的 rawfile 目录。第二步才是在 DevEco Studio 或命令行中用 hvigor 构建 hap。构建产物和缓存相关常被问到的几件事entry/build目录这是 hap 构建的中间产物目录可以整个删掉重新构建不会有副作用。entry/ohos/.hvigorhvigor 的缓存目录如果出现改完 Native 代码但运行时行为没变化的诡异问题先删它。node_modules/.cachemetro 和 babel 的缓存JS bundle 不更新时删它。oh_modules下的.tsbuildinfo文件ArkTS 编译增量缓存出现类型检查报错与实际代码不符时清除。Turborepo 场景还要额外清.turbo缓存目录。我总结一个“三连清”命令删掉entry/build、.hvigor、.turbo再重新构建能解决大部分“构建成功但行为不对”的玄学问题。6.2 hdc 部署、日志查看与常见失败点构建出 hap 后部署命令# 安装 hap hdc install -r entry-default-signed.hap # 启动应用 hdc shell aa start -a EntryAbility -b com.example.yourapp # 查看日志 hdc shell hilog | grep ReactNativeJS如果安装时报签名错误或INSTALL_PARSE_FAILED优先检查认证签名。RNOH 项目在 DevEco 中默认会生成调试签名但命令行直接构建可能需要额外配置签名。日志查看方面我建议开两个终端一个专门过滤 JS 日志grep ReactNativeJS一个专门过滤 ArkTS 层日志grep NotificationModule这样做能快速定位问题出在桥接层还是业务层。常见失败点还有一个hdc install -r之后应用图标没有出现在桌面或者点击图标启动时报module.json5中 ability 不存在。这种一般是你改了entry模块名称但aa start命令里的-b包名没有同步修改。检查module.json5里的bundleName和EntryAbility是否一致。6.3 一次“点击通知无回调”的完整排查我举一个真实排查案例本地通知能正常弹出来但点击通知后JS 侧的onNotification回调一直没有触发。排查链路如下第一步确认 WantAgent 是否正确挂载。点击通知能够拉起应用说明 WantAgent 是有的。但在某些系统版本上WantAgent 拉起应用并不代表notificationUserInfo一定传了进来。我需要先在 EntryAbility 的onNewWant里加一行 hilog 打印确认参数是否到达。onNewWant(want: Want, launchParam: common.LaunchParam): void { console.info(onNewWant want: ${JSON.stringify(want)}); }第二步确认事件发送时机。如果onNewWant在应用冷启动后立即触发而 JS 侧的DeviceEventEmitter监听器还没有注册完事件就会丢失。我的处理是加一个“RN 加载完成”标记在 RN 侧回调一个notifyReady方法后再发送挂起事件。第三步确认桥接模块是否被正确注册。如果 JS 侧调用NativeModules.NativePushNotificationIOS拿到的是undefined那它根本连桥接模块都没找到更别提事件通路。检查方式非常简单在 JS 侧打一行日志console.log(NativePushNotificationIOS:, NativeModules.NativePushNotificationIOS);如果这里输出 undefined问题大概率出在模块注册名称或构建缓存上先清理构建产物再试。整个排查过程下来最终定位到两个问题叠加一是点击事件在 JS 监听器注册完成前被发出二是首次构建缓存导致模块注册失败。两个问题都不复杂但叠加在一起很容易让人误以为是通知 API 本身的问题。最后再分享两个小技巧一个是通知渠道的清理。在开发板上反复安装卸载 hap 时旧的通知渠道有时候不会被卸载逻辑清掉。如果你发现发通知的行为和代码预期不符先手动删掉旧的 slot再重新安装可以避免很多莫名其妙的现象。另一个是用notificationManager.on(click)做全局点击监听时注意在页面销毁时调用off解绑。RNOH 应用的生命周期和原生应用不完全一致页面返回后事件监听可能仍然存在内存泄漏不容易被察觉但累计到一定量就会导致通知点击事件被重复处理。就我个人体验来说RN 三方库迁移到 OpenHarmony 这件事真正的核心从来不是“把 npm 包装上”而是理解这套“JS 接口约定 原生桥接实现”的解耦逻辑。react-native-push-notification-ios只是众多三方库中的一个缩影把它的桥接逻辑吃透后面再遇到react-native-image-picker、react-native-device-info这类库思路基本是通用的。
返回列表