ARTICLE DETAIL

资讯详情

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

React Native适配OpenHarmony:商城设置模块的桥接实践与踩坑记录

React Native适配OpenHarmony:商城设置模块的桥接实践与踩坑记录 做 React Native 开发第七年了OpenHarmony 适配这事从去年开始真正提上日程。手头这个商城项目的 App 是多端复用的Android、iOS 都稳定跑了好几个版本现在要往 OpenHarmony 生态走第一件事就是把手上的 RN 代码库完整跑通。这套代码里设置模块是整个项目里最不起眼、但牵扯原生能力最多的地方——通知开关、清除缓存、拨打电话、版本检测每一样都要过桥。这篇文章就把我们在 rn_for_openharmony 商城项目里做设置实现的整体思路、代码组织方式和踩过的坑一次性梳理清楚给正在做同类迁移的团队一个参考。1. 项目背景我们为什么要在OpenHarmony上跑RN商城1.1 迁移前的真实困境先说背景。我们是一个典型的电商团队App 从 2019 年就开始用 React Native 写了Android 端和 iOS 端共用一套 JS 代码业务迭代速度很快。OpenHarmony 的装机量上来之后多端覆盖就成了刚需但我们不可能为了一个平台单独养一整个 ArkUI 开发团队更不可能把已经跑了三年的 RN 商城重写一遍这时候 React Native for OpenHarmony简称 RNOH就成了最现实的方案。RNOH 本质上就是把 React Native 的运行时、渲染器和原生桥接层适配到 OpenHarmony 系统上让开发者继续写 JS/TS通过桥接调用 OpenHarmony 的底层能力。它不是一个玩具项目社区已经做到了可以跑通复杂业务的程度。我们的目标很明确在尽可能不改业务逻辑的前提下让原来那套商城代码在 OpenHarmony 上能编译、能运行、能上线。但“能跑”和“跑得稳”之间差距很大。商城 App 的业务复杂度集中在首页、商品详情、购物车、订单流程但真正折磨人的反而是设置页这种小模块。原因很简单设置页是用户进入 App 后最早接触的页面之一也是各种系统能力存储、通知、拨号、网络状态交叉最密集的地方。把这 100 行代码调顺了整个迁移项目就成功了一大半。1.2 架构选型的心路历程为什么不是纯Flutter、纯ArkUI在拍板用 RNOH 之前我们内部做过一轮完整的技术选型对比候选方案有三个纯 ArkUI 重写、Flutter 跨端、RN for OpenHarmony。纯 ArkUI 重写体验最好但成本最高。商城这种体量的项目页面数量上百业务逻辑全在 JS 层重写一遍至少三个月而且后续 Android/iOS/OpenHarmony 三端逻辑会分叉维护成本不可控。Flutter 确实跨端能力强但那是另一套全新的 Dart 代码跟现有 RN 资产零交集等于推倒重来。最后选了 RNOH。核心逻辑是我们已有的 RN 代码库复用率接近 95%只需要处理桥接层的适配和少量平台差异代码。设置页这种页面UI 层是纯 JS跑在 RNOH 上几乎不用动只有涉及原生能力的地方需要写鸿蒙侧的桥接模块。这个投入产出比是三个方案里最高的。1.3 设置模块在整个商城项目里的定位设置模块在整个商城项目里属于“低频但高频信任”的页面。用户平时不进来但一进来就是要改账号信息、清缓存、看用户协议、联系客服。它又是一个典型的“功能集合页”入口多、状态多、交互杂。从技术角度看设置页是对 RNOH 桥接能力的一次全面体检。通知开关要读系统设置、清缓存要访问文件目录、拨打电话要调系统 capability、版本更新要读原生包信息。这些功能全部依赖“JS 调原生”的通道而 RNOH 的桥接通道恰好是这个项目里最需要验证的部分。所以把设置模块定为首批迁移页面不是为了省事而是故意挑硬骨头先啃。2. 设置页功能拆解与需求盘点2.1 商城App设置页到底应该放哪些内容很多初学者把设置页当成一个简单的列表页来做实际上这里面的功能分组和交互决策非常讲究。我们对照主流电商 App 和自身业务把设置模块分成了五个分组分组功能项交互方式依赖原生能力账号与安全头像/昵称、绑定手机号、修改密码、退出登录页面跳转 弹窗确认图片选择与裁剪通知设置促销推送、订单消息、公告提醒Switch 开关通知权限查询与申请通用清除缓存、语言切换、字体大小点击执行 弹窗文件目录大小统计关于版本号、用户协议、隐私政策、开源许可页面跳转版本号读取、WebView客服电话客服、在线客服点击拨号/跳聊天页系统拨号能力这个表格做出来之后功能边界就非常清晰了。每一个功能都可以拆成“UI 展示 业务逻辑 原生能力”三层后面写代码的时候按这个分层去组织不会乱。2.2 设置项的存储方案AsyncStorage、MMKV还是原生Preferences设置页最核心的数据存储需求是“读写用户偏好配置”比如推送开关、语言选择、缓存清理的结果。我们面临的真实选择有三个方案。第一个是 AsyncStorage。这是 RN 生态最常用的键值对存储方案优点是社区维护稳定缺点是纯异步、底层是序列化字符串频繁读写大对象时有性能问题。但设置页的数据量非常小只有十几个键值对AsyncStorage 完全够用。第二个是 MMKV。腾讯开源的 key-value 组件性能极佳支持同步读写。问题在于 RNOH 社区对 MMKV 的适配并不成熟我们没有找到稳定可用的 OpenHarmony 版本如果自己写桥接模块耗时又不可控。第三个是直接桥接鸿蒙的 Preferences 接口。OpenHarmony 自带首选项能力但需要手写 NativeModule代码量不大却要为每个设置项单独维护桥接方法。最终我们选了 AsyncStorage 作为主方案原因是它有一个几乎不用改的适配路径RNOH 社区已经提供了 AsyncStorage 的鸿蒙原生支持JS 侧 API 完全一致意味着原有代码里的AsyncStorage.getItem调用一行都不用动。这对迁移项目来说是决定性的优势。我们在封装层加了一层SettingStorage工具类来做统一的读写入口后续就算要换 MMKV 或原生 Preferences也只需要替换内部实现。2.3 设置项交互模型统一协议设计设置页最怕的事情是“每个设置项各自为政”有的用 Switch有的用 Touchable有的直接写死跳转。我们花了一天时间定义了一个统一的设置项协议后续所有设置项都按这个协议来写。// settingTypes.ts export type SettingItemType nav | switch | action | group; export interface SettingItem { key: string; title: string; type: SettingItemType; icon?: string; description?: string; value?: boolean; // switch 类型使用 hidden?: boolean; // 条件显示 onPress?: () void; onValueChange?: (val: boolean) void; } export interface SettingGroup { id: string; header?: string; items: SettingItem[]; }这个协议的好处有三个第一页面的FlatList可以直接渲染整棵设置项树新增一个设置项只需要在配置数组里加一个对象第二Switch 状态和业务状态的同步逻辑被收拢到一个onValueChange回调里不会出现“UI 状态和实际状态脱节”的问题第三每个设置项都是可测试的纯数据对象写单元测试非常方便。3. 核心实现从零搭建一个可用的设置模块3.1 目录结构与页面骨架设计设置模块的代码结构我建议按“页面 组件 服务 协议”四层去组织。我们实际项目里的目录是这样的src/ ├── pages/ │ ├── settings/ │ │ ├── SettingsPage.tsx // 设置页主页面 │ │ ├── SettingSwitch.tsx // 开关组件 │ │ ├── SettingCell.tsx // 列表项组件 │ │ ├── settingConfig.ts // 设置项配置数组 │ │ └── ProfileEditPage.tsx // 账号信息编辑页 ├── services/ │ ├── SettingStorage.ts // 设置存储封装 │ ├── CacheManager.ts // 缓存清理服务 │ └── VersionService.ts // 版本信息服务 └── native/ ├── RNOHCacheManager.ets // 原生缓存模块 ├── RNOHDeviceInfo.ets // 原生设备信息模块 └── RNOHPhoneCall.ets // 拨号模块页面骨架用SectionList来做分组列表非常合适它天然支持 header 和 footer正好对应设置页的分组展示。我建议不要用 ScrollView 包着一堆 View 硬写数据一多就会卡而且代码很丑。// SettingsPage.tsx 核心代码 const groups: SettingGroup[] [ { id: account, header: 账号与安全, items: [ { key: profile, title: 账号信息, type: nav, onPress: goProfile }, { key: bindPhone, title: 绑定手机号, type: nav, description: 已绑定 138****1234 }, { key: logout, title: 退出登录, type: action, onPress: handleLogout }, ], }, // ... 更多分组 ]; SectionList sections{groups} keyExtractor{(item) item.key} renderItem{renderSettingItem} renderSectionHeader{renderSectionHeader} stickySectionHeadersEnabled{false} /注意stickySectionHeadersEnabled一定要设为 falseOpenHarmony 上 SectionList 的 sticky header 默认实现有渲染问题实测在部分版本的 RNOH 上会出现吸顶头遮挡内容的情况。3.2 用户信息模块登录态检测与账号退出账号与安全是设置页里最复杂的部分因为它涉及登录态的全局状态管理。我们项目的登录态用的是 Redux 管理但设置页的账号信息展示并不需要直接连 Redux最好是通过一个自定义 hook 来获取用户信息和登录状态避免设置页频繁触发全局 re-render。// useUserProfile.ts import { useSelector } from react-redux; import { UserProfile } from /store/types; export function useUserProfile() { const profile useSelector((state: RootState) state.user.profile); const isLoggedIn useSelector((state: RootState) state.user.isLoggedIn); return { nickname: profile?.nickname ?? 未登录, avatar: profile?.avatar ?? , phone: profile?.phone?.replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2) ?? , isLoggedIn, }; }退出登录是一个典型的“危险操作”需要二次确认弹窗。这里有一个实操细节用户点击退出后不能直接清 Redux 里的登录态就完事必须按顺序做三件事——清除本地 Token、清理用户的购物车缓存、再跳回登录页。顺序反了会导致页面已经在执行跳转逻辑了但本地 Token 还没清干净导致切后台再进 App 时自动登录又触发了。// handleLogout function handleLogout() { Alert.alert(退出登录, 确定要退出当前账号吗, [ { text: 取消, style: cancel }, { text: 退出, style: destructive, onPress: async () { await SettingStorage.clearUserData(); // 先清本地 clearCartCache(); // 再清业务缓存 dispatch(logout()); // 最后通知全局状态 navigation.reset({ index: 0, routes: [{ name: Login }] }); }, }, ]); }3.3 通知开关与缓存清理原生能力的桥接设置页最典型的两类原生能力是“通知权限申请”和“缓存清理”。这两个在 Android 上都有现成的 RN 库但 RNOH 上不一定兼容所以我们选择自己写原生模块。先看通知开关。设置项在 UI 上是一个 Switch但这个 Switch 的初始值必须是真实的系统通知权限状态不能直接给value{true}。我们需要在页面加载时调用原生模块查询权限。// NotificationService.ts import { NativeModules } from react-native; const { RNOHNotification } NativeModules; export async function getNotificationEnabled(): Promiseboolean { try { return await RNOHNotification.isNotificationEnabled(); } catch (e) { // 模块未注册或设备不支持时默认返回 true return true; } } export async function requestNotificationPermission(): Promiseboolean { try { return await RNOHNotification.requestPermission(); } catch (e) { return false; } }这里的核心细节是当用户关掉 Switch 时我们不直接写“关闭”状态而是调用系统的权限申请接口。如果用户在系统弹窗里点了同意我们才把开关状态更新为 true。很多新手在这里写反了——直接改本地状态而不通知系统结果是 UI 显示已关但推送照常收到。缓存清理也是一个经典场景。我们需要先统计缓存目录大小再执行清理最后把最新的大小更新到 UI。RNOH 侧我写了一个CacheManager原生模块用ohos.file.fs来遍历缓存目录。// RNOHCacheManager.ets import { fileIo } from kit.CoreFileKit; export class RNOHCacheManager { getCacheSize(): Promisenumber { // 遍历缓存目录累加文件大小返回字节数 let totalSize 0; const cacheDir getContext().cacheDir; // 递归遍历目录计算大小 return Promise.resolve(totalSize); } clearCache(): Promisevoid { // 删除缓存目录下的所有文件 // 注意保留目录本身只清内容 return Promise.resolve(); } }// CacheManager.ts const GB 1024 * 1024 * 1024; const MB 1024 * 1024; export async function getCacheSizeText(): Promisestring { const { RNOHCacheManager } NativeModules; const bytes await RNOHCacheManager.getCacheSize(); if (bytes GB) return ${(bytes / GB).toFixed(2)} GB; if (bytes MB) return ${(bytes / MB).toFixed(2)} MB; return ${(bytes / 1024).toFixed(0)} KB; }清理缓存这一步有个非常容易踩的坑缓存目录里有正在被 App 使用的文件比如当前用户头像的临时文件直接删除会导致页面图片闪失。我们的做法是延迟两秒再清理并且清理后强制刷新当前页面和图片缓存保证内存里的 URL 缓存重新加载。3.4 关于页面与版本更新检测关于页面是设置页里最不起眼、但最容易出错的模块。它包含版本号展示、用户协议、隐私政策、开源许可四个入口。版本号必须从原生包信息读取不能硬编码在 JS 里否则发版之后版本号对不上。// RNOHDeviceInfo.ets export class RNOHDeviceInfo { getAppVersion(): Promisestring { let version getContext().applicationInfo.versionName; let build getContext().applicationInfo.versionCode; return Promise.resolve(${version} (${build})); } }// VersionService.ts export async function getVersionText() { const { RNOHDeviceInfo } NativeModules; return await RNOHDeviceInfo.getAppVersion(); }这里要注意一个 OpenHarmony 特有的信息应用市场渠道包和直接安装的 hap 包拿到的 versionCode 可能不一样所以显示版本号的同时最好带上 build 号方便客服定位问题。我们会在版本号下面加一行“已是最新版本”或“发现新版本 v1.2.3”这个判断逻辑需要请求后端的 App 升级接口注意做接口超时保护不能因为升级接口挂掉导致整个设置页白屏。用户协议和隐私政策页面强烈建议用WebView加载在线 HTML 而不是内置富文本。原因很简单合规文本更新频繁内置到 App 里就意味着一轮发版才能改一次在线加载则随时可以调整。RNOH 上react-native-webview有社区适配版本实测基本可用但要注意对下载事件的处理部分 H5 的下载链接在 OpenHarmony WebView 里默认不响应需要在原生侧拦截。3.5 联系客服在RN侧调用系统拨号能力商城 App 的设置页里“电话客服”是高频功能。实现方式很简单RN 自带了一个Linking模块可以打开tel:协议但 OpenHarmony 上Linking.openURL对tel:的支持因版本而异实测在部分系统镜像上会静默失败。稳妥的做法是写一个原生拨号模块。// RNOHPhoneCall.ets import { call } from kit.TelephonyKit; export class RNOHPhoneCall { dialPhone(phoneNumber: string): Promisevoid { return call.makeCall(tel:${phoneNumber}, { accountId: 0 }) .catch((err) { console.error(dail phone failed: ${err.message}); throw err; }); } }// handleCallService async function handleCallService() { try { const { RNOHPhoneCall } NativeModules; await RNOHPhoneCall.dialPhone(400-800-8888); } catch (e) { Alert.alert(提示, 当前设备不支持拨打电话请稍后重试); } }这里有两个细节。第一个是拨号按钮要前置一个确认弹窗防止用户误触直接跳转拨号盘第二个是拨号成功后要监听 App 回到前台的时机把页面状态复位否则会出现从通话界面返回后设置页卡死的情况。我们在AppState里加了一个active事件的 listener从通话返回后重置当前页面的路由栈。4. 新老架构对比与适配实战4.1 OpenHarmony上RN新架构的实际进展聊 RNOH 实现绕不开 RN 的新老架构问题。老架构Paper 架构的核心是异步 BridgeJS 和原生之间靠消息队列传输数据性能天花板低。新架构Fabric TurboModule把 Bridge 换成了 JSIJavaScript InterfaceJS 可以直接持有 C 对象的引用数据可以同步传输渲染也改成了更高效的 Fabric reconciler。在 Android 和 iOS 上新架构已经是主流但 RNOH 的适配进度相对慢一些。我们用的是react-native-oh/react-native0.72.x 分支这套体系对老架构的兼容是最稳定的。社区也在同步推进新架构但 TurboModule 在鸿蒙上的实现依赖 OpenHarmony 的 NAPI 与 ArkTS 的互通能力部分系统接口在开发者测试版和商用版上的行为不一致所以我们没有激进地切新架构。落到设置页这个具体场景老架构完全够用。设置项的交互是低频事件驱动的对同步调用性能要求不高真正要求性能的是首页的信息流渲染那是另一个模块的问题。我的建议是如果你是做商城这种复杂业务优先选老架构最稳定的版本把新架构的迁移放到项目二期评估。4.2 我踩过的坑图片裁剪库与AsyncStorage的兼容问题设置页里的头像更换功能踩了一个最典型的第三方库兼容坑。原来的 Android/iOS 端用的图片选择器是react-native-image-crop-picker在 RNOH 上这个库没有原生实现编译直接报错。社区里有一个适配版本react-native-oh-tpl/react-native-image-crop-picker但实测下来它的图片裁剪交互和 iOS 端有差异在鸿蒙上是从底部弹出一个半屏裁剪器而不是全屏裁剪页面。这个问题的解决方案是拆成两步先用我们自己的原生模块调系统PhotoViewPicker选图拿到图片路径后再用原生侧把图片按 1:1 比例压缩裁剪。这样跳过了第三方裁剪库性能还更好因为系统级的PhotoViewPicker是安全控件用户授权路径更合规。AsyncStorage 遇到的坑是初始化时机。RNOH 上的 AsyncStorage 底层是异步加载数据库的如果 App 启动后立刻调用getItem前几次可能拿到 null。我们做了重试补偿机制在读取失败时延迟 100ms 重试一次实测可以消除启动期偶发的设置项丢失问题。4.3 调试链路hdc端口转发与Metro日志OpenHarmony 的 RN 调试方式和 Android 类似但工具链要换一套。第一次跑 Metro 的时候我在 RNOH 官方文档里翻到了调试入口实际操作过程是这样的# 1. 连接设备 hdc list targets # 2. 端口转发将设备的 8081 端口映射到本机 hdc rport tcp:8081 tcp:8081 # 3. 启动 Metro npx react-native start端口转发是必须的否则设备上的 RN 应用拿不到 JS bundle。这里有个容易踩的坑hdc rport在部分机器上需要 root 权限如果提示失败先检查设备是否进入了开发者模式再检查 hdc server 是否已启动。看日志也跟 Android 不一样。Android 上adb logcat一条命令就搞定OpenHarmony 上用的是hilog我习惯一直开着这个命令来过滤 RN 侧的原生报错hdc shell hilog | grep ReactNative有一个教训是Metro 的报错信息在旧版本 RNOH 上经常不完整如果发现 JS 层报错被吞掉先去hilog里捞原生侧的堆栈往往能找到真正的原因。设置页的 Switch 组件曾经出现过点击没反应的问题折腾了半天最后在 hilog 里看到是一个 ArkTS 侧的类型转换异常JS 侧没有任何提示。5. 常见问题与排查速查5.1 高频问题排查表把我们在设置模块开发过程中遇到的高频问题整理成一张表直接对着排查。问题现象可能原因解决办法设置页打开白屏SectionList 数据未加载或 Redux 未初始化检查全局 Provider 是否包裹页面设置项配置数组是否为空Switch 点击无反应原生通知模块未注册或桥接方法名不匹配在 MainPage 的 pageMap 里确认 RNOHCacheManager、RNOHNotification 均已注册清除缓存后图片显示异常删除的文件被内存图片缓存引用清理后强制 reload 当前页面并清空图片缓存组件状态联系客服无法拨号设备没有电话能力或 tel: 协议被拦截改用原生拨号模块并判断系统是否支持通话能力退出登录后仍能自动登录本地 Token 清除顺序不对先清 Token再清购物车缓存最后改变 Redux 状态版本号显示 0.0.0RNOHDeviceInfo 未实现或注册失败检查原生模块的 enable 状态确认 ArkTS 侧已 export 方法AsyncStorage 偶发空了启动期数据库未初始化完成封装 SettingStorage 增加延迟重试逻辑WebView 打不开协议页面RNOH 的 WebView 组件依赖原生 Web 组件确认 hap 包已经包含了 WebView 原生依赖检查页面路由设置项新增后未显示FlatList/SectionList 未重新渲染确认配置数组是状态数据变更后触发 setState5.2 设置页性能与体验细节设置页虽然功能简单但性能细节依然值得打磨。我总结三个最值得做的优化点。第一尽量少用useSelector。设置页的每个设置项都订阅全局 Redux 状态的话任何全局状态变动都会引发整个页面 re-render。我们的做法是只在页面顶层用一个useUserProfilehook 获取必要信息子组件全部用React.memo包裹保证局部刷新。第二设置项的列表分隔线不要用borderBottomWidth硬画。OpenHarmony 上的渲染引擎对 1px 的逻辑像素处理有兼容问题在部分设备上会出现分隔线模糊或消失。用ItemSeparatorComponent渲染一个高度为 1 的 View颜色使用系统分割线标准色效果更稳定。第三Switch 组件在快速滑动列表时容易卡顿。原因是 Switch 的 onValueChange 回调里做了异步持久化而异步回调会排队占用 UI 线程。优化方式是把持久化操作放到InteractionManager.runAfterInteractions里执行保证滑动动画优先。5.3 上线前Checklist设置模块上线前我们团队固定做一轮检查避免低级问题带上生产环境。[ ] 通知开关的初始状态必须真实读取系统权限不能硬编码 true[ ] 清除缓存后内存和磁盘大小同步更新且页面无异常报错[ ] 退出登录二次确认弹窗文案清晰点击取消不触发任何副作用[ ] 拨号功能在模拟器和真机上都要验证模拟器无电话能力时走失败分支[ ] AsyncStorage 在冷启动偶发读取失败时页面不能白屏或闪退[ ] 用户协议和隐私政策 WebView 页面加载超时要有错误提示不能一直转圈我们的经验是设置页的每一个功能都要同时验证“正常路径”和“异常路径”。比如清除缓存正常路径是点击后弹窗异常路径是缓存目录被系统锁定、删不动。如果不做异常兜底用户点击后会感觉 App 卡死了。最后说点实在的。整套设置模块做下来最大的感受是RN 在 OpenHarmony 上已经不是一个“能不能跑”的问题而是“跑得稳不稳”的问题。设置页这种高频交互页面恰恰是原生能力与 JS 侧衔接最集中的地方把它调顺了后面的支付、地图、扫码模块都会少走很多弯路。我们团队现在归档了一份内部 wiki专门记录 NativeModule 的注册方式和各模块在鸿蒙上的参数约定建议你也在项目一开始就做这件事等到踩坑之后再补成本至少翻一倍。
返回列表