ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RNOH适配实战:通知设置模块的权限、渠道与持久化全流程解析

RNOH适配实战:通知设置模块的权限、渠道与持久化全流程解析 这套组合真正做起来之后解决的是个非常实际的成本问题一套业务代码不需要重写就能触达一个全新的系统底座。最近我在做一个 Steam 资讯类 App 的 OpenHarmony 适配最让我意外的不是复杂的列表页反而是看起来不起眼的通知设置模块。这个模块要处理系统权限、通知渠道、本地持久化、后台限制几乎把跨端开发的坑都踩了一遍。这篇文章不写广告我把从工程搭建到真机调试的过程、代码和踩坑记录全部摊开适合正在做 RNOH 适配尤其是资讯工具类产品的开发者参考。1. 项目全貌与设计拆解通知设置不是三个开关那么简单1.1 为什么是 React Native OpenHarmony先说结论选 React Native for OpenHarmony下面直接叫 RNOH不是因为“OpenHarmony 没有原生方案”而是因为团队里已经积累了一套完整的 React Native 业务组件库。如果每条产品线都在 OpenHarmony 上重新用 ArkTS 写一遍维护成本会直接翻倍。RNOH 的价值在于把 JS 侧的逻辑、状态管理、UI 组件大部分搬到鸿蒙底座上原生部分只需要做系统能力桥接。这个项目的业务是 Steam 资讯聚合数据来自 Steam 公开的新闻与商店接口加上自己的运营配置。界面核心是信息流列表配合搜索、收藏、详情页。整体来说 UI 不算复杂但功能链路很长需要联网、缓存、通知、设置非常适合验证 RNOH 在真实产品里的完成度。通知设置就是其中链路最长的一块从用户手指点开开关到系统通知栏真的弹出内容中间隔了权限、渠道、持久化三层每一层都有独立的状态这也是我把它单独拿出来写的原因。1.2 通知设置这个模块要拆成几块如果只是做一个“总开关”这个模块半天就能写完。但资讯类 App 的通知如果没有细分用户很快会被垃圾推送赶走。我的方案是按消息类型拆开关每个开关对应一条链路。通知类型分了三种每日摘要每天固定时间汇总一次重要资讯属于高价值低打扰。促销活动Steam 打折、专场上线提醒价格敏感用户会喜欢。关注更新用户主动关注的游戏或应用有新动态默认关闭等用户建立了关注关系再引导打开。每个开关背后都有两个状态用户是否授权了系统通知权限以及业务开关是否打开。业务开关由 App 自己持久化系统权限由 OpenHarmony 管理。代码层面必须把两层状态分开处理否则会出现系统权限关了、但 UI 上开关还亮着的尴尬场景。1.3 完整链路长什么样从用户点击开关开始事件路径是这样的前端把新状态写入本地存储AsyncStorage。前端调用原生模块确保对应的通知渠道存在。系统层面检查通知权限是否开启若未开启则发起授权请求。后台拉取资讯后发布通知之前先读取业务开关状态只有开关打开才真正 publish。最容易出错的是最后一步。很多跨端开发者习惯把“设置项存下来了”等同于“通知已经按配置发送了”实际不是。发布通知时一定要再读一次开关因为用户可能在存储写入之后、通知发布之前改变了决定。我的做法是每次发布前都从 AsyncStorage 读一次而不是把配置缓存在内存里。2. 通知权限、渠道与持久化先定规则再写代码2.1 把权限弹窗当成一道状态机来设计OpenHarmony 的通知授权模型和 Android 13 之后的 POST_NOTIFICATIONS 很像不是安装时静态授权而是运行时动态请求。应用需要调用notificationManager.requestEnableNotification()来弹出系统授权框同理可以用notificationManager.isNotificationEnabled()查询是否已经授权。这里容易踩的第一个坑是“弹窗时机选择”。如果一进设置页就立刻弹授权框很容易被用户直接拒绝。正确做法是先展示页面等用户真正打开第一个开关时再触发请求。用户能理解“我要开开关所以系统问我要权限”这个因果关系顺序不能反。我把权限状态设计成一个简单的状态机状态判定方式页面表现未决定isNotificationEnabled()返回 false且从未请求过显示“开启后接收资讯提醒”说明按钮已拒绝请求后返回 false显示“去系统设置开启”的引导入口已授权返回 true开关可正常操作有一个很重要的细节requestEnableNotification()并不保证一定成功而且拒绝之后再次调用不会重新弹窗需要用户去系统设置里手动改。所以开发时不能只写“发起请求然后静默等待回调成功”必须在请求完成后主动查询一次状态用查询结果刷新 UI。2.2 用通知渠道承接不同通知类型OpenHarmony 的通知渠道概念系统里叫 NotificationSlot和 Android 的 NotificationChannel 基本同构。每个 slot 有独立的名称、描述、级别level用户可以针对单个渠道单独调整通知行为。用类似“微信给不同的群分组设置提醒方式”的生活化类比就很好懂。我在原生侧为三类通知建了三个渠道news_digest、sale_active、follow_update。创建 slot 时设置了不同的 level促销活动用默认级别每日摘要也保持默认后续如果想要更安静的模式可以调低。渠道创建是一个幂等操作建议每次启动时都执行一次因为用户可能在系统设置里把渠道删过。渠道创建时要注意 slot ID 的稳定性。ID 一旦定下来就不要改否则用户对某个渠道已有的偏好设置会全部丢失。开发期我改过一次 ID结果测试真机上用户之前设置过的“静音”状态全部回滚到默认这个教训很直接。2.3 开关默认值第一次安装就要想清楚默认值策略会直接影响留存。刚开始我把三个开关全部打开结果测试阶段就被用户吐槽推送太频繁。后来改成只默认开启“每日摘要”这就是一个“高价值低打扰”的底线设置。开关项默认值理由每日摘要开资讯类产品的核心体验一天一条不打扰促销活动关激活的必要动作是用户主动打开避免负反馈关注更新关依赖关注关系关系建立后再引导开启持久化我用的是 AsyncStorage因为react-native-async-storage/async-storage在 RNOH 上可以直接跑。用户每次切换开关就立即写入存储。注意写入是异步的开关连点的情况下后一次写入可能覆盖前一次我在代码里对 toggle 做了简单的防抖两次点击间隔小于 200 毫秒时忽略第二次。3. 代码实操跑通通知设置全链路3.1 工程初始化与原生模块注册RNOH 工程的初始化通常是用社区模板。初始化完成后工程里同时存在 HarmonyOS 原生部分和 React Native 的 JS 部分。原生模块是在 HarmonyOS 侧用 ArkTS 写的JS 侧通过 React Native 的标准机制调用。原生模块的注册过程在不同版本里有差异但基本套路一致写一个类继承 TurboModule然后通过包注册导出给 JS。跳过的部分是脚手架细节真正难的是模块内部怎么调用 OpenHarmony 的系统 API。下面这段示意代码表达核心意图字段名以你当前 SDK 导出为准。// NotificationModule.ets import notificationManager from ohos.notificationManager; import { TurboModule } from RNOH/ts/TurboModule; export class NotificationModule extends TurboModule { async isEnabled(): Promiseboolean { return await notificationManager.isNotificationEnabled(); } async requestEnable(): Promiseboolean { return await notificationManager.requestEnableNotification(); } async ensureChannels(): Promisevoid { await this.addSlot(news_digest, 每日摘要, 每天一次重要资讯汇总); await this.addSlot(sale_active, 促销活动, 折扣与专场上线提醒); await this.addSlot(follow_update, 关注更新, 关注的新内容动态); } private async addSlot(id: string, name: string, desc: string) { const slot new notificationManager.NotificationSlot(); (slot as any).slotId id; (slot as any).name name; (slot as any).description desc; (slot as any).level notificationManager.SlotLevel.LEVEL_DEFAULT; await notificationManager.addSlot(slot); } }这段代码的意图很明确把系统能力封装成三个 JS 可以异步调用的方法。isEnabled负责权限状态查询requestEnable负责发起授权ensureChannels负责确保三个渠道都存在。3.2 原生侧能力封装查询、申请、建渠道ArkTS 侧的代码写完之后需要注册到 RNOH 的模块体系里。注册写法不同版本略有变化重点是理解“JS 侧拿到的 NativeModules.NotificationModule 就是这边导出的对象”。有一个很容易忽略的点OpenHarmony 的addSlot是异步操作真机上偶尔会失败。失败原因可能是重复添加同一 slot ID、参数校验不通过、系统通知服务没就绪。所以ensureChannels里每个 slot 都单独 try-catch一个失败不影响另外两个。发布通知之前也会检查对应 slot 是否存在不存在时先重建再发送。顺便提一句渠道名称的问题。slot 的 name 和 description 会直接展示在系统设置里用户看得到。我见过有些应用把渠道名称写成内部代码名比如notify_type_01用户完全看不懂这是干嘛的结果只能全部关闭。名称一定要写成用户能理解的话。3.3 JS 设置页与状态持久化JS 侧设置页的核心逻辑是初始化时读本地配置与系统状态用户操作开关时更新内存、写入本地存储并实时校验系统权限。import React, { useEffect, useState } from react; import { Text, View, Switch, StyleSheet, NativeModules, } from react-native; import AsyncStorage from react-native-async-storage/async-storage; const NotificationModule NativeModules.NotificationModule; const STORAGE_KEY notify_settings; const DEFAULT_SETTINGS { digest: true, sale: false, follow: false, }; const LABELS { digest: 每日摘要, sale: 促销活动, follow: 关注更新, }; export default function SettingsScreen() { const [systemEnabled, setSystemEnabled] useState(true); const [settings, setSettings] useState(DEFAULT_SETTINGS); useEffect(() { loadInitialState(); }, []); const loadInitialState async () { const enabled await NotificationModule.isEnabled(); setSystemEnabled(enabled); const raw await AsyncStorage.getItem(STORAGE_KEY); if (raw) { setSettings({ ...DEFAULT_SETTINGS, ...JSON.parse(raw) }); } }; const toggle async (key, value) { if (!systemEnabled value) { const granted await NotificationModule.requestEnable(); if (!granted) { return; } setSystemEnabled(true); } const next { ...settings, [key]: value }; setSettings(next); await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next)); }; return ( View style{styles.container} Text style{styles.title}消息通知/Text {Object.keys(LABELS).map((key) ( View key{key} style{styles.row} Text style{styles.label}{LABELS[key]}/Text Switch value{settings[key]} onValueChange{(v) toggle(key, v)} / /View ))} Text style{styles.tip} 系统通知权限当前{systemEnabled ? 已开启 : 未开启} /Text /View ); } const styles StyleSheet.create({ container: { padding: 16 }, row: { flexDirection: row, justifyContent: space-between, alignItems: center, paddingVertical: 12, }, title: { fontSize: 18, fontWeight: 600, marginBottom: 12 }, label: { fontSize: 16 }, tip: { marginTop: 16, color: #888, fontSize: 12 }, });这段代码有两点值得展开说说。第一loadInitialState用 Promise 并行触发更好但要注意 AsyncStorage 的读取可能在原生模块返回值之后才回来两个 setState 不要在同一个时序里合并否则配置会被覆盖。第二toggle里如果系统权限是关闭的用户又打开新开关就先请求权限请求成功后才写入配置这个顺序不能反否则会出现“业务开关开了但系统不允许发通知”的残留状态。3.4 启动白屏RNOH 联调的第一道坎热词里“react native 启动白屏”出现频率很高我在这个项目里也真实遇到了。现象是 App 启动后屏幕一直空白没有 JS 报错也没有崩溃日志。排查顺序很重要。我整理的 checklist 如下现象优先检查处理方式启动一直白屏Metro bundler 是否运行启动npm start确认 bundle URL 指向正确白屏且 hilog 无 JS 日志原生模块注册失败过滤RNOH/JS关键日志查看模块加载流程只有特定页面白屏页面组件或字体下载失败移除自定义字体逐个页面二分定位Release 包白屏bundle 未内置将 bundle 打包到rawfile目录并更新路径偶发白屏RN 实例初始化竞态延迟加载页面或统一走启动路由加载完成回调排到最后发现我的问题出在自定义字体的加载顺序上。RNOH 在字体加载完成之前就渲染了页面字体资源比较大时首帧容易被卡住。解决办法是把字体加载封装成独立模块渲染主界面之前先 await 完成。这种问题在 Android 上不明显到了 OpenHarmony 才暴露也算跨端开发的一种常态。4. 真机上线前的疑难杂症4.1 权限被拒后怎么引导用户拒绝权限弹窗之后应用再次调用requestEnableNotification是不会有弹窗的。如果 UI 上还继续展示一个“开启权限”按钮用户点了没反应就会认为应用坏了。我的处理方式是在开关行下面出现一行浅色提示去系统设置 通知管理 找到本应用手动打开“允许通知”。系统设置页的跳转在 OpenHarmony 上可以通过能力拉起具体接口以 SDK 为准但设计上要记住一个原则不要反复诱导用户去设置页。第一次拒绝后给引导入口第二次拒绝后就不主动提示了除非用户自己进入设置页触发查询。另外我遇到过一个诡异现象用户已经在系统设置里手动打开了通知权限但回 App 里开关还是旧的。原因是我没有监听权限变化事件。解决办法是每次页面从后台回到前台时重新查一次isNotificationEnabled()用AppState监听做刷新。这个细节在测试阶段不容易暴露但真实用户很容易触发。4.2 后台通知被系统拦截OpenHarmony 对后台任务有严格的管控策略这是所有 Android 开发者迁过来时最先不适应的点。App 退到后台之后系统可能不会让普通的定时器持续运行如果你依赖 JS 侧的setTimeout来定时拉取资讯很快就会发现推送“睡着”了。资讯类产品如果要定时推送应该走系统的延迟任务能力在 HarmonyOS 侧申请任务调度而不是让 JS 侧自己循环。更务实的方案是第一天先做本地通知链路后台拉取交给后续版本接系统推送服务这样能保证通知设置模块的闭环逻辑先被验证。我在这个项目里对后台策略做了降级App 在前台时正常拉取并发布本地通知退到后台超过五分钟就不再尝试拉取等下次启动或进入前台时补拉。产品上看起来是“通知不吵了”技术上其实是主动避开后台管控效果反而更好。4.3 网络报错先切分再查代码开发期最常见的报错是连接不上目标服务器现象可以总结为“服务连接失败”。这类问题我踩过最多次的坑是把网络问题当成了代码问题反复改接口逻辑最后发现根本不是代码的锅。排查路径应该按三层切分第一层是网络连通性。先用终端测一下目标域名能否连通。第二层是证书问题测试环境如果用自签名证书真机默认不信任会直接握手失败。第三层是业务层错误服务器已经连上了但接口返回了错误码。有一个很隐蔽的原因真机系统时间不正确导致 TLS 证书有效期校验不通过。这个判断起来很容易看一眼系统时间是否和当前时间一致就行。做跨端适配时这个经验帮我省了很多时间。4.4 常见问题速查表到这里把整个项目过程中遇到的典型问题整理成一张表方便后面的人直接对照问题可能原因解决办法权限弹窗不出现请求时机在 UI 渲染前或在后台放到页面可交互后的用户点击回调里通知发了但没显示渠道不存在或 slot level 过低发布前调用 ensureChannels检查 level权限开了但开关仍显示关本地存储未读到或写入失败查看 AsyncStorage 键值确认 JSON 格式设置被恢复默认测试期间改了渠道 ID渠道 ID 保持稳定不要随便改通知发送重复前台拉取和快速退回前台重复触发通过时间戳去重同一消息 5 分钟内只发一次页面白屏字体加载、bundle 路径、原生模块注册按 3.4 的 checklist 逐项排除5. 复盘与建议5.1 如果重做一次改动最大的地方如果这个项目能重来我会在第一天就把“系统权限”和“业务开关”两层状态彻底分离建模而不是先写着看看。当时图省事在设置页里用一个布尔值同时代表两层状态结果权限被拒绝和开关关闭混在一起页面状态变得非常被动。后来花了一整个下午重构才理清楚。另外通知渠道的设计应该由产品经理提前参与。我当时自己拍板定了三种通知类型做完后运营说还需要“预约上架提醒”和“好友动态”核心架构没问题但渠道数量得后续补。如果一开始定义好扩展规则这步会更顺。5.2 给同样在填坑的人三条建议第一条先跑通最小闭环一个开关 一个渠道 一条本地通知。能跑通再往里面加类型否则一次引入过多变量出了问题你根本不知道是哪一层挂了。第二条善用 hilog。RNOH 联调时hilog里带RNOH、JS、NotificationManager关键字的日志会帮你快速定位问题边界。别只盯着屏幕看白屏日志才是第一现场。第三条真机调试比模拟器重要得多。权限弹窗、后台限制、通知渠道这些能力在模拟器上往往表现正常只有真机才会暴露真实策略差异。这个项目如果只在模拟器上跑大概一半的坑都不会被发现上线前一定会炸。说到底通知设置模块本身不是什么高科技但它是一面照妖镜能把你对系统能力的理解、对边界状态的把握、对异常路径的处理能力照得清清楚楚。做完了这个模块我对 RNOH 的信心反而比之前更足了。
返回列表