CPF-RN 社区地址:CPF-RN - 开源代码托管,代码协作 - AtomGit

上游三方库地址:https://github.com/react-native-elements/react-native-elements

npm 地址:https://www.npmjs.com/package/react-native-elements

适配后地址:rntpc_react-native-elements:基于 HarmonyOS NEXT 与 React Native 的 RNE 组件演示项目 - AtomGit

一、库概述

react-native-elements 是 React Native 生态里用得比较多的一个跨平台 UI 组件库,GitHub 上 Star 超过 2.5 万,上游官方版本支持 Android 和 iOS。这次适配的目标是让它能在 OpenHarmony / HarmonyOS NEXT 上跑起来,并且组件 API 保持不变,业务方迁移的时候不用改代码。

选这个库的原因也比较直接:它是纯 JavaScript/TypeScript 实现的,不包含原生模块,适配难度适中,适合作为 RNOH 适配的入门项目。整个库提供 30 多个开箱即用的组件,覆盖了移动端常见的 UI 场景:

按钮类:Button、ButtonGroup、Chip、FAB、SpeedDial,基于 TouchableOpacity 封装,支持主题定制和样式覆盖。

卡片与布局:Card、CardTitle、CardDivider、CardImage、Header、Divider、Tile、PricingCard,支持圆角、阴影、图片背景这些视觉效果。

表单与输入:Input、SearchBar、CheckBox、Switch、Slider,支持受控和非受控模式、验证状态、左侧图标。

列表与导航:ListItem 以及它的子组件(ListItemContent、ListItemTitle、ListItemSubtitle、ListItemChevron、ListItemCheckBox、ListItemInput、ListItemButtonGroup、ListItemAccordion、ListItemSwipeable),组合方式比较灵活。

反馈与弹窗:Badge、withBadge、Tooltip、Overlay、Dialog、BottomSheet、AirbnbRating。

图标支持:Icon 组件封装了 react-native-vector-icons,支持 Material、Ionicons、FontAwesome 等多种图标字体。

适配的时候需要把上游仓库 clone 到国内 AtomGit,操作效率更高,也方便后续提交 PR。

二、适配环境与真机设备

这次适配用的版本组合如下:

版本

React Native

0.82.1

RNOH(@react-native-oh/react-native-harmony)

0.82.30

React

19.1.1

DevEco Studio

6.0.0.858+

HarmonyOS SDK

6.1.1(24),runtimeOS: HarmonyOS

Node.js

20+(Metro 打包建议用 Node 24)

上游库版本

react-native-elements 3.4.3

全部适配和验证工作都是在华为 MatePad Edge 二合一真机上完成的,没有用模拟器。这个设备支持平板模式和电脑模式(PC 模式)两种使用形态,两种模式下都跑了安装、运行和组件验证。

设备的鸿蒙系统版本:

三、适配过程

3.1 环境搭建

RNOH 开发环境搭建直接参考 CPF-RN 社区组织下的环境搭建文档就行,这里不重复展开:

CPF-RN 社区地址:CPF-RN - 开源代码托管,代码协作 - AtomGit

环境搭好之后,确认上面列的版本都对上了,就可以开始拉代码适配了。

3.2 适配步骤

react-native-elements 是纯 JS 层的库,不涉及原生模块(C++/ArkTS),所以所有改动都集中在 JS 源码和 Demo 工程的配置里。先拉代码、建分支:

git clone https://github.com/react-native-elements/react-native-elements.git
cd react-native-elements
git checkout v3.4.3
git checkout -b feat/ohos_react-native-elements_3.4.3

分支命名按社区规范来:feat/ohos_库名称_版本号

整个适配过程改动的文件汇总在下面这张表里,后面再挑几个关键的点详细说:

改动文件

改动原因

改动内容

src/helpers/index.tsx

Platform.OS 在鸿蒙端是 harmony/ohos,原有的 ios/android 判断走 else 分支,行为不可控

新增 isHarmony、isAndroidLike 两个常量,组件中统一使用,鸿蒙走 Android 样式路径

src/config/withTheme.tsx

上游用 ThemeConsumer render-props 模式,React 19 下 Context Consumer 路径异常,导致组件不显示或主题不生效

完全重写为 useContext(ThemeContext) + forwardRef,一次解决全部 30+ 组件的主题获取

src/switch/Switch.tsx

上游通过 isIOS 区分 iOS 和 Android 的轨道/滑块颜色,鸿蒙需要走 Android 样式

将 Platform.OS 判断改为 isIOS 常量,鸿蒙自动走 Android 分支

src/list/ListItemChevron.tsx

列表项右箭头,iOS 用 Ionicons,Android/鸿蒙用 Material 图标

用 isIOS 判断图标 type 和 name,鸿蒙用 Material 的 keyboard-arrow-right

src/dialog/DialogTitle.tsx

对话框标题字重,iOS 为 500,Android/鸿蒙为 700

用 isIOS 判断 fontWeight,鸿蒙自动走 700

src/header/Header.tsx

上游用 react-native-safe-area-context 的 SafeAreaView,依赖原生视图 RNCSafeAreaView,RNOH 0.82 下不存在,导致崩溃

将 SafeAreaView 的导入改为从 react-native 核心导入

src/bottomSheet/BottomSheet.tsx

同上,BottomSheet 也用了 safe-area-context 的 SafeAreaView

同样改用 RN 核心 SafeAreaView

src/list/ListItem.tsx

React.Children.map 渲染子元素时未指定 key,React 19 下产生 key 警告

映射后的子元素包裹在带 key 的 React.Fragment 中

src/tooltip/Tooltip.tsx

计算弹出位置时只处理了 iOS 和 Android 的状态栏偏移 key,鸿蒙端弹出位置不对

新增 harmony 和 ohos 的状态栏偏移 key

harmony/entry/src/main/ets/pages/Index.ets

vector-icons 的 TTF 字体在鸿蒙端不会自动加载,Icon/CheckBox/Chevron 不显示;jsBundleProvider 优先 Metro 会导致首启白屏

fontResourceByFontFamily 注册实际使用的 TTF 字体;jsBundleProvider 优先 ResourceJSBundleProvider,Metro 仅作 debug 备选

下面挑几个关键的改动点展开说一下。

(1)平台判断 Helper

这是适配的第一步。上游代码里到处都是 Platform.OS === 'ios' 这样的判断,在 RNOH 环境下 Platform.OS 的值是 'harmony''ohos',既不是 ios 也不是 android,所有 else 分支的行为都不可控。所以在 helpers/index.tsx 里加了两个统一的常量,后面所有组件都用这两个常量来判断:

import { Platform } from 'react-native';

const isIOS = Platform.OS === 'ios';

const isHarmony =
  (Platform.OS as string) === 'harmony' || (Platform.OS as string) === 'ohos';

const isAndroidLike = Platform.OS === 'android' || isHarmony;

export { isIOS, isHarmony, isAndroidLike };

这里有个细节要注意:isAndroidLike 只用于样式路径(鸿蒙复用 Android 的视觉风格),不能用于 TouchableNativeFeedback / Ripple——水波纹是 Android 独有的原生触摸反馈,鸿蒙端应该回退到 TouchableOpacity。

(2)withTheme React 19 重写

这是整个适配里最核心的一个改动,也是最隐蔽的坑。上游的 withTheme 用的是 ThemeConsumer 的 render-props 模式(children-as-function),这种写法在 React 16 时代没问题,但 RNOH 0.82 配套的是 React 19.1.1,新的 Context Consumer 路径下 render-props 模式会出现渲染异常,表现就是所有组件都不显示或者主题不生效。

排查这个问题花了不少时间,一开始以为是组件本身的问题,后来才定位到是 withTheme 这一层。解决办法是直接重写为 useContext Hook + forwardRef:

import React, { useContext } from 'react';
import deepmerge from 'deepmerge';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { ThemeContext, ThemeProps } from './ThemeProvider';
import DefaultTheme, { FullTheme } from './theme';

const isClassComponent = (Component: any) =>
  Boolean(Component.prototype && Component.prototype.isReactComponent);

const noop = () => {};

function withTheme<P = {}, T = {}>(
  WrappedComponent: React.ComponentType<P & Partial<ThemeProps<T>>>,
  themeKey: string
) {
  const name = themeKey
    ? `Themed.${themeKey}`
    : `Themed.${WrappedComponent.displayName || WrappedComponent.name || 'Component'}`;

  const Component = WrappedComponent as React.ComponentType<any>;

  const Themed = React.forwardRef<any, any>((props, forwardedRef) => {
    const { children, ...rest } = props;
    const context = useContext(ThemeContext);

    const theme = context?.theme ?? DefaultTheme;
    const updateTheme = context?.updateTheme ?? noop;
    const replaceTheme = context?.replaceTheme ?? noop;

    const newProps = {
      theme,
      updateTheme,
      replaceTheme,
      ...deepmerge<FullTheme>(
        (themeKey && (theme[themeKey as keyof Partial<FullTheme>] as Partial<FullTheme>)) || {},
        rest,
        { clone: false }
      ),
      children,
    };

    if (isClassComponent(WrappedComponent)) {
      return <Component ref={forwardedRef} {...newProps} />;
    }
    return <Component {...newProps} />;
  });

  Themed.displayName = name;

  if (isClassComponent(WrappedComponent)) {
    return hoistNonReactStatics(Themed, WrappedComponent);
  }
  return Themed as any;
}

export default withTheme;

重写之后,所有通过 withTheme 包裹的组件(Button、Card、Input 等 30 多个)都能在 React 19 下正确拿到主题,同时保持了对类组件和函数组件的兼容,forwardRef 也确保了 ref 能正确传递到被包裹的组件。这一个改动解决了全部组件的主题获取问题,比逐个组件打补丁要干净得多。

(3)SafeAreaView 替换

上游的 Header 和 BottomSheet 组件用了 react-native-safe-area-context 提供的 SafeAreaView,这个库依赖原生视图 RNCSafeAreaView。在 RNOH 0.82 环境下,这个原生视图不存在,页面直接崩溃或者布局异常。

解决办法很直接:把 SafeAreaView 的导入从 react-native-safe-area-context 改成从 react-native 核心导入。RN 核心自带的 SafeAreaView 不依赖额外原生模块,在 RNOH 下可以直接用,功能上比 safe-area-context 版本少一些(不支持 edges 自定义),但满足 Header 和 BottomSheet 的基本安全区需求没问题。

// 修改前
import { SafeAreaView } from 'react-native-safe-area-context';

// 修改后
import { SafeAreaView } from 'react-native';

(4)组件样式适配

这部分改动比较琐碎,但逻辑都差不多:鸿蒙和 Android 同为移动端,视觉风格接近,大部分组件的样式直接复用 Android 分支就行。

比如 Switch 组件,上游通过 isIOS 区分 iOS 和 Android 的轨道/滑块颜色,把 Platform.OS 判断改成用 isIOS 常量,鸿蒙就自动走 Android 分支了。ListItemChevron 的右箭头也是同理,iOS 用 Ionicons,Android/鸿蒙用 Material 图标。DialogTitle 的字重 iOS 是 500,Android/鸿蒙是 700,同样用 isIOS 判断就行。

(5)字体注册配置

react-native-elements 的 Icon、CheckBox、ListItemChevron 这些组件都依赖 react-native-vector-icons 渲染图标。在鸿蒙端,TTF 字体文件不会自动加载,必须在 Demo 工程的 Index.ets 里通过 fontResourceByFontFamily 手动注册。

注意只注册实际用到的字体集,避免 HAP 包体积过大。这个 Demo 用了 MaterialIcons 和 MaterialCommunityIcons 两套字体。同时 jsBundleProvider 的顺序也要注意,优先用 ResourceJSBundleProvider 读本地离线 bundle,Metro 只作为 debug 模式下的备选,不然首启会白屏很久(这个问题在第四章详细说)。

(6)其他细节

还有几个小改动:ListItem 里 React.Children.map 渲染子元素时没加 key,React 19 下会报警告,给映射后的子元素包一层带 key 的 Fragment 就行。Tooltip 计算弹出位置时只处理了 iOS 和 Android 的状态栏偏移 key,加上 harmony 和 ohos 的 key。Button 在 Android 上用 TouchableNativeFeedback 实现水波纹,鸿蒙没有这个原生组件,通过 Platform.select 的 default 分支自动回退到 TouchableOpacity,不用额外改代码,文档里说明一下行为差异就行。

3.3 适配效果

适配完成后,建了一个独立的 RNOH Demo 工程来验证全部核心组件。Demo 工程分三个展示区域:

静态展示区(StaticShowcase):Button(主按钮/描边按钮)、Card(卡片 + Avatar + Badge 组合)、ListItem(带 Chevron 箭头)、Icon(带图标的按钮),用 React.memo 包裹,避免交互区状态变化时静态区跟着重渲染。

交互区(InteractivePanel):Input(带左侧图标)、SearchBar、CheckBox、Switch、Slider(拖动实时显示数值),所有交互状态保持在组件内部。

动画遮罩(FadeOverlay):用 React Native 核心 Modal + Animated 实现淡入缩放效果,useNativeDriver: true 开原生线程驱动,不卡 JS 线程。

Demo 工程的入口 App.tsx 用 ThemeProvider 包裹整个应用,把三个区域组合起来:

静态展示区的代码,展示了 Button、Card、Avatar、Badge、ListItem 这些组件的基本用法:

交互区的代码,Input、SearchBar、CheckBox、Switch、Slider 都在这里:

动画遮罩的代码,Modal + Animated 实现淡入缩放,点击遮罩或按钮关闭:

用 DevEco Studio 打开 Demo 工程的 harmony 目录,USB 连 MatePad Edge 真机,点 Run 编译安装。首次编译大概 5 到 8 分钟(包含原生 so 库编译),后面增量编译 30 秒左右。

全部组件在 MatePad Edge 的平板模式(触摸全屏)和 PC 模式(鼠标键盘窗口化)下都跑了验证。

平板模式验证结果:

用例

操作

预期

结果

Button 主按钮

触摸点击

透明度反馈,onPress 触发

通过

Button 描边按钮

触摸点击

边框样式正常,点击触发 Overlay

通过

Card 卡片

视觉检查

圆角、elevation 阴影、标题分割线正常

通过

Avatar 头像

视觉检查

圆形头像,文字"鸿"居中

通过

Badge 徽标

视觉检查

"OHOS"绿色徽标正常显示

通过

Input 输入框

软键盘输入文字

输入正常,左侧 person 图标显示

通过

SearchBar 搜索框

软键盘输入

搜索框样式正常,可输入文字

通过

CheckBox 复选框

触摸切换

勾选状态切换,Material 勾选图标显示

通过

Switch 开关

触摸切换

开关切换,Android 风格轨道/滑块颜色正确

通过

Slider 滑块

触摸拖动

滑块可拖动,数值实时更新,allowTouchTrack 点击轨道跳转

通过

ListItem 列表项

视觉检查 + 触摸

布局正常,右侧 Material Chevron 箭头显示

通过

Overlay 遮罩

点击"打开 Overlay"按钮

Modal 弹出,淡入缩放动画流畅,点击遮罩/按钮关闭

通过

页面滚动

上下滑动

ScrollView 滚动流畅,无卡顿

通过

MatePad Edge react-native平板展示

PC 模式验证结果:

用例

操作

预期

结果

窗口化布局

调整窗口大小

内容 maxWidth 限制生效,不过度拉伸,布局自适应

通过

鼠标点击 Button

鼠标左键点击

点击反馈正常,onPress 触发

通过

物理键盘输入

Input/SearchBar 中用物理键盘打字

输入正常,光标跟随

通过

鼠标拖动 Slider

鼠标按住滑块拖动

拖动流畅,数值实时更新

通过

滚轮滚动页面

鼠标滚轮上下滚动

页面滚动正常

通过

Modal 居中显示

打开 Overlay

Modal 在窗口内居中显示,遮罩覆盖整个窗口

通过

CheckBox/Switch 鼠标切换

鼠标点击切换

状态切换正常

通过

MatePad Edge react-native电脑展示

组件支持状态汇总:

组件

状态

备注

Button / ButtonGroup / Chip / FAB / SpeedDial

支持

鸿蒙使用 TouchableOpacity,无 Ripple 水波纹

Card / CardTitle / CardDivider / CardImage

支持

圆角和 elevation 阴影正常

Input

支持

左侧图标需配置字体

Avatar / Accessory

支持

Badge / withBadge

支持

SearchBar

支持

鸿蒙端使用 platform="default" 或 "android"

ListItem 及全部子组件

支持

Chevron 使用 Material 图标

CheckBox / CheckBoxIcon

支持

需配置 vector-icons 字体

Switch

支持

Android 风格样式

Slider

支持

allowTouchTrack 正常

Divider

支持

Header

支持

改用 RN 核心 SafeAreaView

LinearProgress

支持

useNativeDriver 动画正常

Tab / TabView

支持

建议视觉验证

SocialIcon / PricingCard / Tile / Rating

支持

图标需配置字体

Icon

有限支持

依赖 react-native-vector-icons + 字体注册,未注册的字体集不显示

Overlay / Dialog / BottomSheet / Tooltip

待充分验证

依赖 RN Modal,基本功能可用,复杂动画和边缘情况需进一步验证

ListItemSwipeable

待验证

依赖 react-native-gesture-handler,需确认该库的鸿蒙适配状态

对业务方来说,迁移成本基本为零:组件 API 和上游完全一致,只需要把依赖来源从 npm 换成 AtomGit 上的适配版本,在 Index.ets 里注册一下用到的图标字体,其他代码不用动。

四、常见问题与解决方案

问题一:真机安装后闪退,报 libRNOHApp is undefined

现象

打包安装到 MatePad Edge 真机后,应用启动瞬间闪退。通过 hdc hilog 看日志,核心报错是:

Couldn't create bindings between ETS and CPP. libRNOHApp is undefined.
load librnoh_app.so failed ... No such file or directory

原因

这个报错和业务代码无关,是 HAP 包里的 native 库 ABI 和真机 CPU 架构不匹配。

RNOH 启动时,ArkTS 侧需要通过 NAPI 加载 librnoh_app.so 来建立 ETS 和 C++ 的绑定。如果 HAP 包里没有当前设备架构对应的 so 文件,加载就会失败,libRNOHApp 变成 undefined,初始化直接 Fatal,进程退出。

具体到这个项目,一开始为了加快编译安装速度,把 harmony/entry/build-profile.json5 里的 abiFilters 只保留了 x86_64。但 MatePad Edge 真机是 arm64-v8a 架构,打出来的 HAP 里只有 libs/x86_64/librnoh_app.so,没有 libs/arm64-v8a/librnoh_app.so,真机启动时找不到对应 so,立刻闪退。

解决方法

1. 修改 harmony/entry/build-profile.json5,把 abiFilters 改成同时包含 arm64-v8a 和 x86_64:

"abiFilters": ["arm64-v8a", "x86_64"]

真机(手机/平板)需要 arm64-v8a,PC 模拟器需要 x86_64,双 ABI 一包两用。

2. 改完 abiFilters 后一定要 Clean 再全量编译。只改配置不清理的话,cmake 可能继续用之前的缓存,打出来的还是旧 ABI 的包。在 DevEco Studio 里用 Build → Clean Project,然后再 Rebuild。

3. 打包完成后,可以解压 HAP 文件(本质是 zip)确认里面是否包含两个架构的 so:

libs/arm64-v8a/librnoh_app.so
libs/x86_64/librnoh_app.so

4. 确认无误后再安装到真机。

小结:遇到 libRNOHApp is undefined,先查 ABI 配置和 HAP 里的 so 文件,不要先去翻业务组件代码。为了提速只打单 ABI 是真机闪退的常见原因。

问题二:应用启动后长时间白屏,很久才出内容

现象

应用能正常安装和启动,但首屏长时间白屏(大概 30 秒到 1 分钟),然后才突然显示出页面内容。期间没有报错,进程也没有退出,看起来像是卡住了。

原因

这个问题出在 JS Bundle 的加载策略上。RNOH 页面初始化时通过 JSBundleProvider 来拉取 JS 包。如果配置里优先走 MetroJSBundleProvider(调试用的 Metro 服务),设备会尝试连接开发机的 Metro 服务(默认 8081 端口)。

当 Metro 服务没开、或者设备和开发机之间网络不通、或者端口没做反向代理时,连接会一直超时重试。等超时结束后,才会 fallback 到本地资源包。这就导致了"先进去白屏很久,然后才有内容"的现象。

具体到这个项目,一开始 harmony/entry/src/main/ets/pages/Index.ets 里 jsBundleProvider 的顺序是 Metro 优先,真机演示时 Metro 没开,就出现了长时间白屏。

解决方法

1. 调整 Index.ets 里 jsBundleProvider 的顺序,优先用 ResourceJSBundleProvider(读本地 rawfile 里的 bundle.harmony.js),MetroJSBundleProvider 只作为 debug 模式下的备选:

jsBundleProvider: this.rnohCoreContext?.isDebugModeEnabled
  ? new AnyJSBundleProvider([
      new ResourceJSBundleProvider(
        this.rnohCoreContext.uiAbilityContext.resourceManager,
        'bundle.harmony.js'
      ),
      new MetroJSBundleProvider(),
    ])
  : new ResourceJSBundleProvider(
      this.rnohCoreContext.uiAbilityContext.resourceManager,
      'bundle.harmony.js'
    ),

这样一进 App 就直接用打进 HAP 的离线 bundle 秒开,Metro 只在需要热更新时才连接,不再卡首启。

2. 确保本地 bundle 已经打包进工程。发布或真机演示前,先执行 Metro 打包命令,把生成的 bundle.harmony.js 放到 harmony/entry/src/main/resources/rawfile/ 目录下,再编 HAP。

3. 如果需要热更新调试,再手动开 Metro:

npm start

然后用 hdc 做端口反向代理:

hdc rport tcp:8081 tcp:8081

注意 Metro 对 Node 版本有要求,这个项目里用 Node 24 可以正常跑,DevEco 自带的 Node 18 可能会有兼容性问题。

小结:启动白屏优先查 Bundle Provider 的顺序。真机演示应该默认优先本地 Resource Bundle,Metro 只能当开发热更新通道,不能作为首启的硬依赖,否则弱网或没开 Metro 时就会出现长时间白屏。

五、总结

这次 react-native-elements 的鸿蒙适配走下来,最大的感受是:纯 JS UI 库的适配门槛不高,但真机调试的坑比想象中多。

代码层面主要是加平台判断、处理 React 19 兼容性、替换 SafeAreaView、注册图标字体,没有涉及原生模块,适合作为 RNOH 适配的入门练手项目。其中 withTheme 的 React 19 重写是最核心也最隐蔽的一个改动,排查花了不少时间。

真正花时间的是真机调试——ABI 配置不对导致闪退、Bundle 加载顺序不对导致白屏,这两个问题在 PC 模拟器上都复现不出来。所以适配完一定要上真机跑一遍,不能只在模拟器上验证。

整体来看,react-native-elements 是一个性价比很高的适配项目,难度适中,覆盖面广,做完对 RNOH 的整个开发流程会有比较完整的理解。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐