最近在rk3568开发板上跑OpenHarmony,用React Native for OpenHarmony做了一套用户信息卡片的UI。从画草图到最终在真机上跑起来,整个过程踩了不少坑,也有一些值得沉淀的经验。这篇就完整记录一下这个卡片的实现过程,从设计稿怎么拆、组件怎么划分,到电话拨打怎么调、点击外部区域怎么收浮层,再到真机调试时常用的hdc命令和编译产物的清理,一次性说清楚。
如果你正准备在OpenHarmony设备上做RN开发,或者只是想把一个静态草图画成可交互的页面,这篇文章可以作为一份可直接抄作业的参考。
1. 项目背景与整体思路
1.1 为什么选RN for OpenHarmony
先交代一下选型背景。现在想在OpenHarmony上做应用,官方推荐的是ArkTS + ArkUI,这是一套声明式UI方案,语法上跟SwiftUI很像,学习成本不算高。但问题在于,如果团队里已经有不少会React的成员,或者你手上本来就有一套React Native的代码库,再去学一套新方案并且把业务逻辑全部重写一遍,投入产出比就很低。
React Native for OpenHarmony恰好能解决这个痛点。它把RN的渲染层和原生模块层移植到了OpenHarmony上,保留JS/TS + React的编写方式,底层通过Native的能力去绘制UI、调用系统接口。也就是说,前期积累的组件生态、状态管理方案、网络请求层都能复用,唯一的差异在原生调用那一层。
这次项目我选的是一个比较典型的场景:用户信息卡片。界面不算复杂,但涉及静态布局、头像加载、点击拨打电话、点击外部区域收起浮层这些交互,刚好能把RN for OpenHarmony的常用能力覆盖一遍。
1.2 卡片需求与运行环境
需求很简单:从产品那边拿到一张草图,上面是一个用户卡片,包含头像、昵称、用户ID、手机号、以及一个“拨打电话”的按钮。点击按钮会唤起系统的拨号能力,点击卡片以外的区域则收起整个浮层。看上去平平无奇,真正做起来的时候发现里面有三个容易翻车的点:
- 头像图片加载在OpenHarmony上走的是哪条链路,能不能用普通的Image组件。
- 电话拨打属于原生能力,RN层要怎么桥接过去。
- 浮层的点击外部收起,和RN的事件冒泡机制怎么配合。
运行环境方面,我手里的设备是rk3568的开发板(RK3588我也试过,后续会提到两者的差异),系统版本基于OpenHarmony 6.0编译出来的工程镜像。开发机用的Ubuntu,通过USB和hdc连接设备。整体链路是:开发机写RN代码 -> 打包成hap -> 安装到开发板 -> 真机预览。
1.3 功能边界与扩展思考
这个卡片本身只是一个开始。我在实现的时候刻意留了几个扩展点:卡片数据走的是统一的UserInfo类型,后续接后台接口可以直接替换数据源;电话拨打模块单独封装了一层,往后要做短信、邮件之类的通信能力,可以在同一层扩展;浮层状态管理用到了全局的事件通知,后续如果要做多个卡片联动,状态流转可以直接复用。
网上能搜到一些关于OpenHarmony移植RN的老帖子,大多停留在“能不能跑起来”,真正把业务卡片从草图做到可交互、可上真机的内容不多。这篇文章就围绕这个目标,把过程中的设计思路、关键代码、避坑记录一并放出来。
2. 设计稿解析与组件拆分
2.1 草图上的信息层级
产品给的草图很潦草,就是一张白纸上手画的几个框。我先把它转成了信息层级:
- 最外层是半透明遮罩,点击遮罩可以关闭整个卡片。
- 浮层底部是卡片主体,包含头像、昵称、ID、手机号。
- 手机号旁边有一个电话图标按钮,点击后弹出确认框,确认后调起拨号。
这里有一个容易被忽略的点:产品同时给出了“黑暗模式下卡片背景需要自动切换”的要求。OpenHarmony上ArkUI支持深色模式,但RN层拿到的色值是由JS代码控制的,需要自己监听系统主题变化,不能指望开箱即用。
我把草图中的视觉元素拆成了三个层级:
| 层级 | 内容 | 实现方式 |
|---|---|---|
| 遮罩层 | 半透明黑色、全屏 | Modal或绝对定位的View |
| 卡片层 | 白色/深色背景、圆角、阴影 | 内层View + style |
| 内容层 | 头像、文本、按钮 | 小组件组合 |
拆分的好处是,每一层都可以独立调样式、独立复用。比如遮罩层以后可能用在其他弹窗组件里,卡片层也可能用在列表的Item上,没必要全部写死在一起。
2.2 组件树与数据模型
根据信息层级,我规划了组件树:
UserCardModal ├── Backdrop (可点击遮罩) └── UserCard ├── UserAvatar ├── UserInfoText │ ├── UserName │ └── UserId └── ContactRow ├── PhoneNumber └── CallButton数据模型我定义了一个TypeScript接口:
export interface UserInfo { name: string; userId: string; phone: string; avatarUrl?: string; }这个接口贯穿整个项目。后端接口返回的数据、卡片展示的数据、拨号时传递的数据,都是同一个结构。后面如果需要加头像的本地缓存字段、加在线状态,直接扩展接口就行,不会牵动到组件内部逻辑。
组件拆分的价值在写样式的时候体现得很明显。RN的样式不像CSS那样有继承和级联,所有样式都得写在一个个style对象上。如果整张卡片只有一个大组件,style对象的体积会非常大,而且改一处可能要翻半天。拆成小组件后,每个组件只维护自己的样式,出了问题也能快速定位。
2.3 布局方案选型
这张卡片我用了Flexbox布局。RN for OpenHarmony对Flexbox的支持基本沿袭了React Native的标准能力,alignItems、justifyContent、flexDirection这些核心属性都没问题。
头像和文本部分我用了横向Flex布局,头像固定尺寸,文本区flex: 1自动占满剩余空间。电话按钮放在最右侧,用绝对定位或者Flex排列都可以。我选的是Flex排列,因为这样在窄屏设备上按钮能跟着文本区一起压缩,不会出现重叠。
尺寸和间距方面,考虑到OpenHarmony设备有手机也有平板,我没用固定的px像素值,而是统一从design system里取尺寸常量。实践下来,尺寸常量抽成单独文件非常有用。改一次全局生效,而且能让卡片在不同尺寸的屏幕上保持视觉一致性。
3. 工程准备与设备环境
3.1 创建RN项目与依赖
先说工程部分。OpenHarmony上的RN开发,第一步是创建RN项目。官方仓库里的脚手架已经能支持OpenHarmony平台,创建命令与标准RN项目基本一致:
npx @react-native-community/cli@latest init RnUserCard创建完成后,需要手动添加OpenHarmony对应的原生工程目录。这一步目前还是通过把脚手架里的openharmony文件夹拷贝到项目里来做的,没有完全自动化。官方文档里有现成的模板,直接复制过来就行。
然后是依赖安装:
npm install项目中需要额外安装的依赖包括:
- react-native-harmony,这是RN for OpenHarmony的核心包,负责把JS渲染成OpenHarmony的原生组件。
- @react-native-ohos/react-native-logs,用来收集RN层的日志,排查问题非常关键。
- 图标库,我用了lucide-react-native,它提供了不少开箱即用的线性图标,电话、关闭、更多这类常用图标都有。OpenHarmony官方没有对应的RN图标库,用lucide是社区里比较通用的做法。
依赖装好之后,先跑一遍默认模板,确保基础框架能编译通过再动代码。这一步很重要,很多人上来就改业务代码,结果编译失败也不知道是依赖问题还是代码问题。
3.2 连接OpenHarmony设备与hdc常用命令
开发板连上电脑后,第一件事就是确认设备能被正常识别。hdc是OpenHarmony的设备连接工具,类比Android开发里的adb,用法也几乎一样。
查看设备列表:
hdc list targets拿到设备序列号后,可以区分开发板上同时挂着的多个设备。如果你需要精确指定某个设备执行命令,可以加上序列号参数。
确认系统版本:
hdc shell param get const.product.name hdc shell param get const.ohos.version这里就涉及网上经常搜到的param get命令。OpenHarmony的系统参数存放在param服务里,const.product.name返回设备的产品名,const.ohos.version返回系统版本号。我在rk3568开发板上跑出来的是OpenHarmony 6.0的工程镜像,用这个命令确认过编译版本,避免代码里用了新API但系统不支持。
设备的唯一标识符方面,OpenHarmony用devudid来标识设备,在用hdc连接时显示的往往是serial。两者区别在于,serial是连接层面的序列号,devudid是设备出厂层面的唯一标识。如果你需要统计设备数量或者做设备绑定,应该用devudid而不是serial。获取devudid:
hdc shell bm get -udid这个命令返回的设备标识,在后续做用户行为统计或者远程推送时用得比较多。
3.3 USB连接与底层驱动说明
hdc能连上设备,底层靠的是USB的通信协议。开发板上通常会运行一个usbd进程,通过libusb向上层暴露USB访问能力。如果你在Linux开发机上遇到hdc连不上设备的问题,多半是插上USB之后系统识别不到开发板的USB设备节点。
排查思路是先用系统自带工具确认USB设备有没有枚举出来:
lsusb如果能看到类似Rockchip相关的设备项,说明USB连接正常,问题在hdc server没有正确匹配。这时候可以重启一下hdc服务:
hdc kill hdc start再做一次hdc list targets。实测下来,rk3568开发板在Linux主机上连USB时,偶尔会出现设备枚举慢的情况,多等几秒再执行命令就能看到设备。如果你在Windows上做开发,则需要额外安装驱动,让系统把开发板识别成hdc设备,否则hdc永远看不到target。
USBManager相关的接口,在RN层一般不会直接用到,它主要是OpenHarmony原生侧的能力。但如果你需要自己封装一个原生模块来操作USB外设,就要去了解usbManager怎么配合libusb使用。这次的卡片项目没有用到USB外设,不过我在调试阶段用hdc做过文件推送和截图,这些基础能力对UI调试帮助很大。
4. 核心功能实现
4.1 卡片主体与静态布局
先写最核心的卡片UI。我用了Modal组件做最外层,RN的Modal在OpenHarmony上支持得不错,透明背景、动画样式都能配置。
import { Modal, View, TouchableWithoutFeedback, StyleSheet } from 'react-native'; export function UserCardModal({ visible, user, onClose }: Props) { return ( <Modal visible={visible} transparent={true} animationType="fade" onRequestClose={onClose} > <TouchableWithoutFeedback onPress={onClose}> <View style={styles.overlay}> <UserCard user={user} onClose={onClose} /> </View> </TouchableWithoutFeedback> </Modal> ); }overlay是全屏的半透明遮罩,直接放在Modal内部。卡片放在overlay里,但由于卡片在布局上靠底部,只有遮罩区域能响应点击关闭事件。
样式的关键点在于,卡片要压住底部,遮罩背景半透明,需要同时处理backgroundColor和opacity。用rgba来写半透明颜色是最稳妥的,不建议分别用opacity和background色值,因为opacity会把子元素也变透明。
const styles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'flex-end', }, card: { backgroundColor: '#FFFFFF', borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 24, }, });圆角我只设置了顶部两个角,因为卡片是从底部弹出来的,底部圆角贴在屏幕边缘没有意义。这个细节在草图上看不出来,但做UI的人都会注意。
4.2 头像本地缓存与占位图片
头像处理是需求里最容易出问题的地方。OpenHarmony上RN的Image组件从网络加载图片时,底层走的是原生网络栈。如果你还没配置网络权限,或者开发板的网络连接有问题,头像会直接白屏。
先在manifest里确认网络权限:
<uses-permission ohos:name="ohos.permission.INTERNET" />然后在Image组件上做两层兜底。第一层是占位图,defaultSource属性可以指定一个本地图片作为加载中的占位;第二层是错误兜底,onError回调里把状态切到本地默认头像。
const [avatarSource, setAvatarSource] = useState( user.avatarUrl ? { uri: user.avatarUrl } : require('./assets/default_avatar.png') ); <Image source={avatarSource} style={styles.avatar} defaultSource={require('./assets/placeholder_avatar.png')} onError={() => setAvatarSource(require('./assets/default_avatar.png'))} />本地缓存这块,RN for OpenHarmony目前没有像iOS/Android上那样成熟的图片缓存库,所以我处理得比较克制:加载成功后的图片URL用一个Map存起来,下次相同URL直接复用上次的本地文件。如果你用的是原生ImageLoader那一套,后续可以自己封装一个简单的磁盘缓存模块,核心思路就是拿到网络图之后写文件,下次读文件。
4.3 电话拨打功能的接口调用
电话拨打是标准的原生能力调用。需求里提到“rn调用电话功能”,这在React Native for OpenHarmony里属于需要桥接的模块。OpenHarmony的系统API提供了@ohos.telephony能力,RN层不能直接调用,需要我们先在原生侧封装一个模块。
原生侧用ArkTS写一个模块,暴露一个方法给JS调用。简化版代码如下:
import { call } from '@kit.TelephonyKit'; export class CallModule { dial(phoneNumber: string): void { call.makeCall('', phoneNumber, false) .then(() => { // 调用成功 }) .catch((err) => { // 处理失败 }); } }然后在RN侧用TurboModule把CallModule暴露出来。RN for OpenHarmony对TurboModule的支持已经比较成熟,把原生模块注册好之后,JS侧通过NativeModules或者TurboModuleRegistry就能拿到实例:
import { NativeModules } from 'react-native'; const { CallModule } = NativeModules; export function dialPhone(phone: string) { CallModule.dial(phone); }实际调用时,我先弹了一个确认框,防止用户误触。确认框用RN自带的Alert即可:
const handleCall = () => { Alert.alert( '拨打确认', `确定要给 ${user.phone} 拨打电话吗?`, [ { text: '取消', style: 'cancel' }, { text: '拨打', onPress: () => dialPhone(user.phone) }, ] ); };这里有一个实操上的坑:OpenHarmony在真机上调用call.makeCall需要权限配置,在module.json5里声明ohos.permission.PLACE_CALL权限,同时需要用户手动授权。调试阶段我直接在代码里申请,后来发现频繁弹窗体验很差,就改成进入页面时统一申请一遍,再用一个状态变量记录是否已授权。如果你的应用以hap形式安装,没有声明权限会直接返回错误码,而不像Android那样只是回调失败。
4.4 点击卡片外部区域收起浮层的实现
“rn如何实现点击页面其他区域执行某个函数”是社区里一个非常高频的问题。这个项目里具体场景是:点击浮层遮罩之外的区域,收起卡片。
核心思路是:遮罩层是一个全屏的TouchableWithoutFeedback,点击时触发onClose;卡片本身在遮罩上层,点击卡片时阻止事件继续冒泡到遮罩。
RN的触摸事件有自己的冒泡机制。卡片区域我用一个单独的View包起来,在卡片内部不再使用TouchableWithoutFeedback,而是直接让卡片区域作为响应者。这样点击卡片时,事件不会传递到外层遮罩。同时,卡片上的按钮、文本等交互元素不会受到遮罩onPress的影响。
<Modal visible={visible} transparent onRequestClose={onClose}> <TouchableWithoutFeedback onPress={onClose}> <View style={styles.overlay}> <View style={styles.card}> {/* 卡片内容 */} </View> </View> </TouchableWithoutFeedback> </Modal>这样还有一个细节:卡片内部如果还有别的点击区域,比如拨打电话按钮,点击时先触发按钮自己的onPress,不会触发遮罩的onClose。这是因为子组件吞掉了触摸事件,没有继续冒泡。如果你发现点击卡片内部也触发了关闭,多半是内部组件没有正确处理触摸事件,可以考虑用TouchableWithoutFeedback包住内层并设置onPress为一个空函数来阻断事件穿透。
这里我建议把遮罩点击和卡片内部点击的逻辑分开来写,而不是在onPress里判断点击坐标。用坐标判断看似灵活,但在不同屏幕尺寸上很容易出偏差,调试成本高。
4.5 图标库选择与引入细节
按钮上的电话图标,我用的是lucide-react-native。这个图标库在RN社区里使用比较广泛,风格统一,体积也控制得不错。安装:
npm install lucide-react-native然后直接引入需要的图标:
import { Phone } from 'lucide-react-native'; <View style={styles.callButton}> <Phone size={20} color="#FFFFFF" /> </View>这里有几个使用细节值得注意。lucide-react-native在OpenHarmony上能不能正常显示,取决于字体加载。如果图标全都不显示,大概率是字体文件没有被打包进去。需要确认打包脚本里把lucide的ttf字体文件拷贝到了hap的resources目录下,否则图标会显示成方块。
字体文件位置一般在node_modules/lucide-react-native/fonts/下,把ttf考到项目的src/main/resources/base/media/目录即可。这一步我也是踩了坑才发现的,代码里一切正常,真机上一看全是空格。
如果你不想引入额外的图标库,也可以直接用系统符号或者自绘图标。不过对于用户信息卡片这种需要一定视觉品质的界面,lucide这类开源图标库的性价比很高,不需要自己画图标。
5. 真机调试与常见问题排查
5.1 真机调试与日志查看
连上OpenHarmony开发板后,调试RN代码最常用的方式还是日志。RN for OpenHarmony会把JS侧的console日志通过hdc转发到设备日志里,你用hdc抓日志就行。
hdc shell hilog -r hdc shell hilog | grep rnohhilog -r清空历史日志,然后实时监听,用rnoh关键字过滤RN相关输出。console.log、console.warn、异常堆栈都会经过这个通道打印出来。实测下来,rnoh日志比Android的logcat要简洁不少,过滤关键字后基本就是纯RN输出。
如果你需要调试JavaScript逻辑,可以在开发机上启动Metro Bundler,然后用hdc把设备上的调试端口反向代理到开发机。不过这需要开发板和开发机在同一网络环境下,并且配置较多。我的做法是先用console日志把UI流程跑通,遇到逻辑问题再看日志定位,效率也不低。
OpenHarmony上的RN还有一个特性:JS侧代码在Release包模式下是预编译的,不能用Chrome DevTools直接调试。如果你的业务逻辑比较复杂,建议在Debug包阶段就把逻辑测试充分,进入Release包后主要靠日志做问题定位。
5.2 编译产物清理与目录瘦身
编译过程中会生成大量中间产物,导致工程目录越来越大。网上经常有人问“openharmony编译出来的文件哪些可以删除”,我列一下我的处理方式:
build/目录:构建过程的临时产物,可以删除,下次编译会重新生成。oh_modules/与node_modules/:依赖目录,不要手动删除源文件,但.harmony/、.hvigor/这类hvigor缓存目录可以定期清理。entry/build/目录下的hap包:如果不需要历史产物,可以直接删掉。每次编译都会生成新的hap文件。
清理命令:
hvigorw clean如果你用DevEco Studio打开过这个工程,还会多出.idea/目录,里面是IDE的本地配置,不影响编译,可以忽略。
我遇到过一种情况:长时间不清理,开发板磁盘被日志和安装包占满了,导致新包安装失败。这时除了删掉编译产物,还要检查设备上的应用缓存。用hdc卸载老包、清除应用数据都能释放空间:
hdc uninstall com.example.usercard hdc shell bm clean -n com.example.usercard -d开发板的存储空间本来就比手机紧张,养成定期清理的习惯能省不少事。
5.3 常见报错与解决办法
整理一下这次开发中遇到的高频问题,按出现频率排序:
1. 设备连不上:hdc list targets为空。
排查顺序:先看USB线是不是数据线(有些线只能充电不能传数据),再看lsusb能否看到设备,最后重启hdc服务。rk3568和rk3588开发板在Linux主机上,如果经常连不上,可以检查udev规则,有没有给OpenHarmony设备的USB VendorID配置访问权限。
2. 页面白屏,hilog里没有JS日志。
大概率是Metro服务没有启动,或者是Bundle加载失败。确认开发机上Metro在运行,然后检查设备与开发机的网络连通性。如果两者通过USB连接,需要做好端口映射。
3. 图片加载不出来。
按这个顺序排查:网络权限有没有加、URL能不能在开发板上直接访问、图片是否是https链接、有没有配置证书信任。最后再检查Image组件的占位逻辑,排除代码写错的情况。
4. 真机上字体渲染异常或图标不显示。
优先排查字体文件是否打进包。在OpenHarmony上,字体文件需要放在resources的media目录下,否则运行时无法加载。检查打包脚本,确认ttf资源路径是正确。
5. 系统版本差异导致API不可用。
OpenHarmony 6.0和更早版本在部分API上有差异。如果你在别的设备上开发另一套系统版本,最好用param get把const.ohos.version打出来,确认版本一致。跨版本调试时,像call.makeCall这类系统接口的参数签名可能会发生变化,需要以当前设备系统版本的SDK文档为准。
5.4 编译性能优化与增量构建
OpenHarmony的编译速度虽然比纯Android慢一些,但通过配置可以显著缩短迭代周期。我用的方案是:先编译出hap包,确认基础功能没问题,后面的改动尽量走增量构建。
hvigor的增量构建默认是开启的。但如果你频繁修改了module.json5或者原生侧代码,会导致缓存失效,触发全量编译。所以尽量把native层的改动集中到一个版本,后续在JS层做迭代,编译速度会快很多。
代码层面也能够做优化。把业务逻辑拆到独立的JS模块里,减少入口文件的依赖量,能缩短Metro的打包时间。如果你只用到了RN核心组件,打包时通过babel插件把不需要的模块去掉,也能缩小bundle体积。
5.5 关于const.product.name与自定义系统镜像
最后提一个偏系统层的经验。开发过程中,我尝试过在rk3568上替换默认的系统镜像,做一些定制验证。这时候就需要修改镜像里的系统参数,其中const.product.name是比较关键的标识。
修改方式是在系统源码的产品配置里改,然后重新编译镜像。编译完成后,用hdc把新镜像刷进去:
hdc flash system system.img这个操作需要在解锁的状态下执行,具体解锁流程与OpenHarmony的签名机制相关。普通应用开发用不到这一步,但如果你的应用需要适配不同设备型号,而且代码里依赖了const.product.name来做逻辑判断,建议在模拟器或者开发板上跑一个统一值,避免不同设备上出现分支不一致的问题。
DevEco Studio的模拟器上const.product.name的值和真机不同,如果你用模拟器调试,遇到和产品名相关的逻辑问题,需要优先确认当前跑的是真机还是模拟器,避免把模拟器的问题带到真机上。
6. 踩坑总结与提效建议
这次从草图到代码的完整流程走下来,有几个体会想分享给做同类项目的朋友。
先让UI跑通,再做原生能力。我一开始是先写了电话拨打模块,结果发现卡片UI又调了几天,原生模块也跟着改了好几次。如果先把UI数据都mock掉,把视觉、布局、交互过一遍,再去接原生能力,迭代效率会高很多。
日志是调试的第一生产力。OpenHarmony的hilog虽然格式和logcat不太一样,但信息量足够。把console.log集中加上关键字前缀,比如[UserCard],再用grep过滤,定位速度能快一倍。
编译报错先看原生侧还是JS侧。RN for OpenHarmony的编译分两层:外层是hvigor的原生构建,内层是Metro的JS打包。如果报错信息里带ohos、module、hap这些词,基本是原生构建问题;如果带bundle、js、metro这些词,基本是JS层问题。分开排查,别混在一起瞎猜。
然后是关于开发板的差异。rk3568和rk3588我都跑过同一个工程,rk3588的渲染性能和动画流畅度都更好,但rk3568在编译产物上一样能跑。如果你手里的板子是rk3568,出现卡顿不要急着怀疑代码,先看看是不是板子性能瓶颈。UI层面尽量少用高开销的阴影和半透明叠加效果。
还有一个开发习惯值得一提:从设计稿到代码,中间一定要有一个“设计标注”的过渡阶段。RN for OpenHarmony虽然没有Sketch/Figma插件那么成熟的自动标注流程,但你可以用Figma的测量工具量出px,再一一映射到StyleSheet对象里。不要凭感觉猜尺寸,不然不同屏幕上一对比,间距全乱套。
最后,如果你准备把这类卡片组件沉淀到团队库里,建议把UserInfo类型、卡片样式常量、电话拨号封装拆到三个独立文件里,后续接真实接口的时候,只需要替换数据源,UI和交互层基本不用动。这样这个草图级别的用户信息卡片,才能真正复用在更多业务页面上。