React Native for OpenHarmony组件开发:从基础组件到通信实战
1. 项目系列回顾与组件学习路线1.1 上一篇跑通了什么这一篇要解决什么RN for OpenHarmony下文简称 RNOH学到今天最重要的一个概念就是组件。上一篇我们把环境搭好项目能跑起来这一篇就专门来啃组件这块硬骨头。很多朋友环境搭好之后容易卡住Hello World 能亮但真到自己写页面完全不知道从哪下手。我第一遍学的时候也是这样后来总结的解决办法就一条——先别急着看架构、看原生化实现先把组件玩明白剩下的路就顺了。如果你跟上篇节奏走完现在手头应该有一个能启动的 RNOH 工程不管是真机还是模拟器都行。这篇文章里的示例代码理论上不挑设备只要工程能跑起来都能复现。内容上我会把常用基础组件的用法过一遍再花一整节讲组件之间怎么传数据父传子、子传父最后分享几个我在 OpenHarmony 上踩得最多的组件坑。1.2 推荐的基础环境与版本在正式敲代码之前先说下我手头的环境方便你对号入座OpenHarmony 4.0 Release 及以上版本真机或模拟器都可以DevEco Studio 4.0 及以上版本主要用于编译和安装 HAPRN 0.72.x 分支RNOH 社区目前主力适配的版本工程里同时维护 react-native 和 react-native-harmony 两个关键依赖RNOH 的组件写法和 React Native 官方一致你写的 JSX 最终会通过 RNOH 的组件映射层渲染到 OpenHarmony 原生组件上。也就是说大部分时候你在页面上看到的 View、Text、Image底层对应的是 OpenHarmony 的 ArkUI 组件能力。理解这一点很重要RNOH 不是重新发明了一套组件而是把 React Native 的组件模型搬到 OpenHarmony 上。所以网上大量 React Native 的组件教程、踩坑贴只要 API 版本对得上都能参考只是最终渲染效果和个别能力会有差异。2. 拆解组件模型组件树、Props 与 State2.1 先理解组件树页面就是一棵组件树我刚开始学 RN 的时候总觉得组件是玄学后来用了一个特别土但有效的类比组件就像俄罗斯套娃。整个 App 是最外面的大娃娃页面是小一点的娃娃容器组件是再小一点的娃娃最里面的文本、按钮、图片就是不能继续拆开的最小娃娃。在 RNOH 里你写一个页面本质就是在描述一棵组件树。根节点是 App 组件往下挂各种页面容器页面容器里再挂内容组件。组件可以嵌套可以复用也能抽成独立文件。比如一个常见的商品卡片就是一个 View 容器里套了一个 Image 和一个文本区域文本区域里又挂了几个不同样式的 Text。整个页面由很多这样的卡片拼起来。看一段最简单的组件树代码import React from react; import { View, Text } from react-native; const App () { return ( View style{{ flex: 1, backgroundColor: #F5F5F5 }} Text style{{ fontSize: 20, color: #333 }}Hello OpenHarmony/Text /View ); }; export default App;这段代码里 App 返回的是一棵两层结构View 是父节点Text 是子节点。RootView 拿到这棵树之后会做 diff、计算布局然后把最终结果交给原生层渲染。你现在可以不用管底层细节只需要养成一个习惯闭上眼睛想每个页面的时候先在脑子里把它拆成一棵树再动手写代码。2.2 Props组件对外暴露的只读输入我们写组件不只是为了把界面画出来更重要的是让组件能接收外部数据这样同一个组件在不同页面能展示不同内容。React 的组件接收外界传参的方式就是 Props。Props 可以类比成函数参数外部传什么组件内部就拿到什么。它有几个特点要注意第一Props 是只读的子组件不能去修改自己收到的 Props只能读取展示第二Props 类型理论上可以由外部决定但在 TypeScript 项目里我们通常会给每个组件定义一个明确的接口interface第三父组件重新渲染时如果传给子组件的 Props 发生变化子组件也会跟着重新渲染。简单示例import React from react; import { View, Text } from react-native; interface UserCardProps { name: string; age: number; } const UserCard ({ name, age }: UserCardProps) { return ( View style{{ padding: 12, backgroundColor: #fff, borderRadius: 8, marginBottom: 8, }} Text style{{ fontSize: 18, fontWeight: 600 }}{name}/Text Text style{{ fontSize: 14, color: #666 }}{age} 岁/Text /View ); }; export default UserCard;之后在父组件里直接UserCard name张三 age{28} /就能复用了。这个组件值不值钱就看它能不能被不同页面以不同参数反复使用。2.3 State组件内部的临时状态和 Props 配套出现的概念是 State也就是组件自己维护的临时状态。什么场景需要 State凡是页面上的内容会随着用户操作变化的地方基本都需要。比如轮播图当前切到第几张、输入框里现在有没有文字、开关按钮是开还是关这些都是状态。State 和 Props 最大的区别在于State 由组件自己维护和更新Props 由外部传入且不可修改。但 State 更新后组件会自动重新渲染这是它最核心的作用。我之前见过不少人纠结“为什么要用 useState直接定义一个变量不行吗”原因很简单普通变量变了页面不会刷新只有通过 setState 触发的状态变更React 才知道要重新渲染组件树中受影响的部分。import React, { useState } from react; import { View, Text, Button } from react-native; const Counter () { const [count, setCount] useState(0); return ( View style{{ padding: 20 }} Text style{{ fontSize: 32, textAlign: center }}{count}/Text Button title加一 onPress{() setCount(count 1)} / /View ); };点击按钮后count 加一Text 里的数字随之更新。整个页面的数据流是单向的组件持有 State把 State 通过 Props 传给子组件子组件触发回调父组件再更新 State。这个模型贯穿所有 React 系框架RNOH 也不例外。把这两节吃透后面写任何复杂页面都不会慌。3. 常用基础组件实操从 View 到 FlatList3.1 View 与 Text布局和文本的基石在 RNOH 里View 的地位类似于网页里的 div是几乎所有布局的根基。它可以设置宽高、背景色、圆角、边框、阴影还能通过 flexbox 控制子组件排列。Text 则负责展示文本支持 fontSize、fontWeight、color、lineHeight 等常规样式。有个细节我一开始就踩过RN 和 Web 不一样Text 默认不会继承外层 View 的文字样式你必须在 Text 上单独设置字体、字号、颜色。同时Text 组件里不能随便塞一个 ViewText 的嵌套只能是文本节点或 Text 组件否则会出现渲染异常或告警。import React from react; import { View, Text } from react-native; const Card () { return ( View style{{ backgroundColor: #fff, borderRadius: 10, padding: 16, margin: 12 }} Text style{{ fontSize: 20, fontWeight: bold, color: #222 }}组件卡片标题/Text Text style{{ fontSize: 14, color: #888, marginTop: 6 }} 这里是组件的描述文字用来展示基础文本布局效果。 /Text /View ); };View 的样式尽量别全部写在 JSX 里多了以后很难维护。建议复杂页面用 StyleSheet.create 统一管理这样还能获得样式表的复用和性能优化。3.2 Image本地图片、网络图片与尺寸控制图片组件在移动端用得非常多。RNOH 的 Image 用法和 RN 官方一致主要分三种来源网络图片、本地 require 图片、base64 图片。网络图片是最常见的写法是Image style{{ width: 200, height: 150, borderRadius: 8 }} source{{ uri: https://example.com/demo.png }} resizeModecover /这里有个非常容易踩的坑在 OpenHarmony 工程里用网络图片一定要确认 module.json5 里配了网络权限。如果没有 INTERNET 权限图片会一直加载不出来控制台报的错还比较隐晦不是一看就懂的那种。我在自己项目里排查了很久才发现是权限问题。本地图片方面RNOH 对 require 的图片资源处理方式和 RN 官方不太一样如果发现require(./assets/logo.png)在 OpenHarmony 上不显示先检查工程里资源打包配置是否包含这张图。如果只是写 demo 验证逻辑最省事的方式是用 base64 字符串作为 uri或者直接先用网络图把功能跑通再回去补本地资源配置。resizeMode 的值有 cover、contain、stretch 等用法和网页里的 background-size 类似。cover 会裁剪并充满容器contain 会保持完整比例缩放stretch 会拉伸填满。实际项目里用得最多的是 cover尤其是头像、轮播图、封面图。3.3 TextInput 与 Button让页面可交互文本输入框和按钮是绝大多数交互页面的基础。TextInput 是一个受控组件这意味着它的输入值通常由 State 控制import React, { useState } from react; import { View, TextInput, Text, StyleSheet } from react-native; const LoginInput () { const [username, setUsername] useState(); return ( View style{styles.container} TextInput style{styles.input} placeholder请输入用户名 placeholderTextColor#aaa value{username} onChangeText{setUsername} / Text当前输入{username}/Text /View ); }; const styles StyleSheet.create({ container: { padding: 20 }, input: { height: 44, borderWidth: 1, borderColor: #ddd, borderRadius: 8, paddingHorizontal: 12, fontSize: 16, }, });handleChangeText 会收到用户输入的最新文本你直接 setState 就行。有个细节如果界面上有输入框同时有按钮建议关注一下键盘弹起后是否会遮挡输入框。RN 官方提供 KeyboardAvoidingView 组件在 OpenHarmony 上也是可用的遇到遮挡问题可以给它包一层。Button 组件是最基础的按钮只有一个 title 和 onPress 两个主要参数。它的样式在不同平台上差异比较大想自定义背景色、圆角、文字的按钮通常需要自己用 TouchableOpacity、Pressable 或 View 包一层实现。对于入门阶段先记住 Button 是最快的选择需要样式再自己封组件。3.4 ScrollView 与 FlatList滚动容器与长列表页面内容超出屏幕时需要滚动。RNOH 里有两个常用的滚动容器ScrollView 和 FlatList。ScrollView 适合内容数量可控、一次性全部渲染的场景比如一个详情页、一个表单。它的用法很简单把要滚动的内容放进去就行import React from react; import { ScrollView, View, Text } from react-native; const DetailPage () { return ( ScrollView style{{ flex: 1 }} contentContainerStyle{{ padding: 16 }} Text第一段内容/Text View style{{ height: 600, backgroundColor: #f0f0f0, marginVertical: 12 }} / Text第二段内容/Text /ScrollView ); };但如果你要渲染一个长列表比如几百上千条数据就千万别用 ScrollView 去 map。这个时候要用 FlatList。FlatList 在 RNOH 里走的是原生列表容器会做回收复用性能比 ScrollView 好很多。import React from react; import { FlatList, View, Text } from react-native; interface Item { id: string; title: string; } const data: Item[] [ { id: 1, title: 第一项 }, { id: 2, title: 第二项 }, { id: 3, title: 第三项 }, ]; const ListPage () { return ( FlatList data{data} keyExtractor{(item) item.id} renderItem{({ item }) ( View style{{ padding: 12, borderBottomWidth: 1, borderColor: #eee }} Text{item.title}/Text /View )} / ); };keyExtractor 很重要它让列表项具备稳定的标识。尤其是列表数据会发生增删改的时候不要依赖数组下标作 key否则会引发渲染错乱。4. 组件间通信落地父传子、子传父与一个小实战4.1 父传子通过 Props 把数据传下来组件通信最常见的方向是父组件向子组件传数据。数据的通道就是我们前面讲的 Props。父组件在子组件标签上写属性子组件通过 props 形参接收这个过程是单向的数据只能向下流。举个实际场景一个商品列表页里每个商品行的 UI 和交互都是一样的只有内容不同。这时候你抽一个 ProductItem 组件接收 title、price 等 Props父组件用 map 循环生成多个 ProductItem 就行。如果未来要改商品卡的样式只需要维护 ProductItem 一个文件不用改父组件里的每一处重复代码。我在写这种组件的时候尽量会在组件开头声明清楚 Props 类型比如interface ProductItemProps { title: string; price: number; onPress: () void; }尤其多人协作的项目里类型就是用代码写文档省去很多口头沟通成本。4.2 子传子通过回调把事件抛上去子组件往父组件传数据方向是相反的不能再靠 Props 直接传。正确的做法是父组件通过 Props 把一个回调函数传给子组件子组件在合适的时机调用这个回调把数据作为参数传出去。这就是所谓的“回调抛事件”。举一个加减数量的例子。父组件维护一个 number 类型的 State把这个值通过 Props 传给子组件显示同时传给子组件一个 onChange 回调。子组件内部点击加号或减号时调用 onChange把计算后的最新值传回给父组件父组件再 setState 更新。这种写法也叫“状态提升”即多个子组件需要共享同一份数据时把这份数据放到它们共同的父组件里然后通过 Props 下发、回调上传。4.3 实战手写一个自动轮播图组件轮播图在移动端应用里太常见了很多新手习惯直接引入第三方库。但我个人建议入门阶段至少手写一个因为它是理解 Props、State、生命周期清理和 ScrollView 滚动事件的最佳练手项目。需求很简单一组数据横向自动滚动底部带指示器用户手动滑动后继续自动轮播。这里我用 ScrollView 的 horizontal pagingEnabled 实现加一个定时器控制自动切换。import React, { useEffect, useRef, useState } from react; import { ScrollView, View, Text, Dimensions, NativeSyntheticEvent, NativeScrollEvent, } from react-native; const { width } Dimensions.get(window); interface BannerData { title: string; color: string; } interface BannerProps { data: BannerData[]; interval?: number; } const Banner ({ data, interval 3000 }: BannerProps) { const scrollRef useRefScrollView(null); const [active, setActive] useState(0); useEffect(() { const timer setInterval(() { const next (active 1) % data.length; scrollRef.current?.scrollTo({ x: next * width, animated: true }); setActive(next); }, interval); return () clearInterval(timer); }, [active, data.length, interval]); return ( View ScrollView ref{scrollRef} horizontal pagingEnabled showsHorizontalScrollIndicator{false} onMomentumScrollEnd{(e: NativeSyntheticEventNativeScrollEvent) { const index Math.round(e.nativeEvent.contentOffset.x / width); setActive(index); }} {data.map((item, index) ( View key{index} style{{ width, height: 150, backgroundColor: item.color, justifyContent: center }} Text style{{ textAlign: center, color: #fff, fontSize: 20 }} {item.title} /Text /View ))} /ScrollView View style{{ flexDirection: row, justifyContent: center, marginTop: 8 }} {data.map((_, index) ( View key{index} style{{ width: 6, height: 6, borderRadius: 3, backgroundColor: index active ? #333 : #ccc, marginHorizontal: 3, }} / ))} /View /View ); }; export default Banner;这个组件有四个关键点第一ScrollView 设了 horizontal 和 pagingEnabled它的作用是把滚动变成整页切换一个屏宽一页。第二定时器用 setInterval 驱动每隔 interval 毫秒调用 scrollTo 滚到下一张。第三用户手动滑动结束后通过 onMomentumScrollEnd 方法计算当前在第几页更新 active这样指示器会跟着变定时器的下一轮也是从当前页继续。第四useEffect 返回 clearInterval这一步绝对不能省否则组件卸载后定时器还在跑轻则内存泄漏重则报错。写完这个组件之后你对组件通信、State 更新时机、生命周期清理的理解会一下子通透很多。后续如果觉得自研轮播不够用再去引入社区成熟轮播库也会更容易看懂它的源码。5. 踩坑实录布局、图片、滚动的常见问题排查5.1 布局不生效与 flexDirection 的默认值我在 OpenHarmony 上调试的第一个页面就遇到了布局不生效的问题页面横向排了两个 View但始终是上下排列。排查了大半天最后发现是 flexDirection 的默认值在作怪。RN 里 View 的 flexDirection 默认是 column也就是主轴从上到下和 Web 端 flex 默认 row 不一样。如果不显式设置两个子 View 就会纵向排列。还有个容易混淆的点是 justifyContent 和 alignItems。这两个属性都是控制子组件排列的但方向不同justifyContent 控制主轴方向上的排列alignItems 控制交叉轴方向上的排列。由于主轴默认是垂直的所以先说 justifyContent 控制的是垂直方向很多人下意识以为它是水平居中的写出来效果不对就要懵半天。排查布局问题的时候我的习惯是先看父容器有没有确定的宽高或者 flex再看主轴方向最后看 justifyContent 和 alignItems 有没有写反。大多数布局问题跑不出这三步。5.2 尺寸适配与 1px 边框的写法RNOH 里的长度单位在逻辑上和 Android 的 dp 类似不是物理像素所以在高分辨率屏幕上直接写borderWidth: 1实际上显示出来可能是多个物理像素视觉上边框会显得过粗。想要真正的 1 物理像素边框可以这样写import { StyleSheet } from react-native; const styles StyleSheet.create({ divider: { borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: #eee, }, });StyleSheet.hairlineWidth 会根据当前设备的像素比自动计算出最细的线宽在不同设备上展示效果更专业。如果你需要更多适配逻辑比如根据屏幕宽度动态计算尺寸可以用 Dimensions.get(window) 拿屏幕宽高再按比例算。但要注意Dimensions 拿到的尺寸不代表安全区域页面底部如果有 Home Indicator内容可能被遮挡这时候需要配合 SafeAreaView 或 padding 处理。5.3 图片加载不出来的几类原因在 OpenHarmony 上用 Image 加载图片失败的情况还挺常见的我整理过一张排查表现象常见原因排查顺序网络图片一直转圈缺少 INTERNET 权限先查 module.json5 权限配置网络图片瞬间报错URL 不可达或证书问题用浏览器确认地址能直接访问本地图片不显示require 资源未打包进工程查工程资源打包配置图片显示但尺寸不对没设置 style 宽高给 Image 设置明确宽高某些格式不支持原生化解码能力限制换 png、jpg 验证网络图片的权限问题最容易阴人因为日志提示不明显而且有时候看起来像是组件用错了。后来我养成了习惯新建 RNOH 工程第一件事就是检查网络权限。5.4 滚动嵌套冲突与长列表卡顿滚动容器嵌套也是新手重灾区。最常见的场景是外层页面用 ScrollView里面某一段又套了一个横向 ScrollView这时候手势处理会打架表现为横滑总是触发外层竖向滚动或者反过来。我先说一个简单的判断标准能避免嵌套就避免。如果确实需要嵌套横向滚动和纵向滚动同时存在时要注意里层的横向 ScrollView 是否抢手势RNOH 原生层会做手势仲裁但不是所有情况都完美。测试时重点关注两个方向滑动的流畅度。长列表卡顿的排查思路更直接先看是不是用了 ScrollView 渲染大量数据。如果数据量超过几百条建议直接换 FlatList。FlatList 还支持 initialNumToRender、maxToRenderPerBatch 等配置项数据量特别大的时候可以按需调整。另外renderItem 里尽量不要写内联箭头函数每次渲染都会创建新函数对长列表的性能会有影响。把 renderItem 抽成组件或稳定的函数能减少不必要的重渲染。6. 最后说点实操心得学了这么多组件最后分享几条我个人跑项目的经验。第一RNOH 的组件体系中JS 侧 API 和 React Native 官方高度一致但 OpenHarmony 的原生渲染能力和 Android/iOS 并不是一一对应。遇到一个组件在文档里存在但表现不对先别急着怀疑自己写错试着去 RNOH 社区仓库看对应示例很多答案都在示例工程里。第二写组件时尽量保持单一职责。一个组件只做一件事做复杂了就想办法拆。我见过有人把一个页面全部逻辑塞进一个巨大的组件文件后期维护真的很难受。抽组件不是为了炫技是为了下一次改需求的时候你只需要改一个局部文件而不是在一千行代码里翻找。第三调试组件问题时打开 DevTools 看告警信息同时多看原生日志。RNOH 的桥接层会把一些原生错误打到 logcat 里这些信息往往是 JS 侧看不到的。小组件可以先在 demo 里单独验证跑通了再集成到业务页面能省不少排查时间。组件这块内容确实不难就是知识点琐碎。我建议你把这篇文章里的示例代码一个个敲一遍尤其是轮播图组件花一个小时完整写出来比看三遍文档都管用。下一篇我打算聊聊 RNOH 里样式的细节包括 flexbox 布局的常见布局模板和主题化方案。如果这个系列对你有帮助评论区告诉我你更想先看哪部分内容我来给你安排。