这篇文章用一个使用 react-native CLI 创建的纯 RN 工程iOS 和 Android 都在跑里面用 expo-battery 读电池信息的 demo app 为例子给它加上了 HarmonyOS 支持业务代码一行没改同一份 App.tsx 现在跑在三个平台上。这篇文章把整个过程整理出来有同样需求的开发者可以照着走。expo-harmony 是什么AtomGit 仓库atomgit.com/baoshuo/expo-harmonyGitHub 仓库github.com/renbaoshuo/expo-harmony欢迎给上面这两个仓库点点 Star ~expo-harmony 是一个开源项目目标是让使用 Expo 模块的 React Native 应用跑在 HarmonyOS 上。它主要做三件事。一是给一批常用 Expo 模块补上鸿蒙的原生实现统一发布在 npm 的expo-harmony/scope 下目前有近百个包expo-battery、expo-location、expo-sqlite、expo-camera、expo-notifications 这些高频模块都在里面。二是提供expo-harmony命令行工具环境诊断、构建 HAP、选设备、安装、启动、管理 Metro一条命令串起来。三是支持自动链接装好包执行一次 link 命令原生模块的注册和构建配置都会生成不用逐个手写。工程形态上它支持两种。走 Expo CNG 的应用可以用 prebuild 直接生成鸿蒙原生工程。原生工程自己手工维护的应用走 bare 路线把一份现成的 harmony 工程模板拿进来改几个名字就能用。本文的应用属于后一种。它目前适配 Expo SDK 55 加 RNOH 0.84.1。RNOH 是 React Native for OpenHarmony鸿蒙侧的 RN 运行时版本号跟 React Native 对齐。仓库的 docs 目录 里有快速开始和 bare 接入两篇教程这篇文章的很多操作就是照着 bare 那篇做的。移植前的项目应用长什么样应用叫 ExpoSampleApp用react-native-community/cli创建react-native 和 react 固定在 0.84.1 和 19.2.3。npx react-native-community/cli init ExpoSampleApp\--version0.84.1\--package-name com.exposampleapp\--pmnpm选 0.84.1 不是随手挑的。RNOH 当前版本就是 0.84.1react-native 跟它对齐之后三个平台共用同一份 react 和 react-native 依赖不用在 Metro 里维护两套 RN 共存的别名方案省掉一堆配置。页面是一个电池面板。启动时用getPowerStateAsync读一次快照显示电量、电源状态和低电量模式同时挂三个监听器分别订阅电量、充电状态、低电量模式的变化。收到事件只做计数方便确认事件链路通不通。核心逻辑大概是这样。import * as Battery from expo-battery; // 读一次快照 const power await Battery.getPowerStateAsync(); // { batteryLevel: 0.45, batteryState: 2, lowPowerMode: false } // 订阅电量变化组件卸载时 subscription.remove() const subscription Battery.addBatteryLevelListener(({ batteryLevel }) { setEventCount(count count 1); });电量取值是 0 到 1 的浮点数取不到时返回 -1显示百分比之前要先处理这个未知值。还有一个写代码时的小细节SDK 55 的PowerState类型只有batteryLevel、batteryState、lowPowerMode三个字段没有batteryVoltage想多显示一行电压的话 tsc 会直接报错。这不是鸿蒙的问题各平台都一样。接入 Expo Modules先跑通 iOS 和 Android接入用的是官方install-expo-modules命令。这里有个坑命令内置一张 React Native 版本到 Expo SDK 的映射表它不认识 0.84.1直接跑会报Unable to find compatible Expo SDK version。加--sdk-version 55.0.0 --non-interactive显式指定就能过改出来的东西和官方 bare 流程一致Podfile 里多了use_expo_modules!Gradle 侧接上 expo-autolinkingAppDelegate 和 MainApplication 换成 Expo 提供的基类。npminstall--legacy-peer-deps\expo55.0.26 expo-modules-core55.0.26 expo-battery55.0.13\expo/metro-config55.0.27 expo/metro-runtime55.0.12 expo/log-box55.0.13 npx-yinstall-expo-modules --sdk-version55.0.0 --non-interactive另一个会卡住人的问题是版本差。Expo SDK 55 的原生代码是按 RN 0.83 写的配 0.84.1 编译不过。Android 侧是 RN 0.84.1 把Promise接口的code参数改成了可空expo-modules-core 的 Kotlin 实现还是旧签名。iOS 侧是RCTRootViewFactory的方法签名变了。两处改动都不大用 patch-package 打成补丁挂在 postinstall 里之后装依赖会自动应用。另外用 Xcode 26 的话AppDelegate 里的导入要写成internal import Expo不然 Swift 6.2 会报 ambiguous import。到这里 Android 和 iOS 都能跑了。Android 模拟器读出电量 100%AVD 默认就是满电电源状态未接电源点按钮重新读取也正常。iOS 模拟器上电量显示未知。这是正常表现iOS 模拟器没有电池仿真getPowerStateAsync返回 -1。模块本身工作正常调用没抛错监听和按钮都好使。所以在模拟器上验证模块接入看的是调用能不能走通、事件有没有流动别纠结具体数值。开始接入鸿蒙前置环境需要 DevEco Studio 和完整的 HarmonyOS SDKOHPM、Hvigor、hdc 这些工具要装齐Node 用 20 以上。模拟器在 DevEco Studio 的 Device Manager 里创建一个就行。环境有没有准备好后面 doctor 命令会挨个检查。安装鸿蒙侧依赖# Expo 鸿蒙适配包npminstall--legacy-peer-deps\expo-harmony/cli55.0.26-harmony.13 expo-harmony/metro-config55.0.26-harmony.4\expo-harmony/expo55.0.26-harmony.3 expo-harmony/expo-modules-core55.0.25-harmony.5\expo-harmony/expo-modules-autolinking55.0.25-harmony.5\expo-harmony/expo-battery55.0.13-harmony.5\react-native-ohos/react-native-safe-area-context5.6.4# RNOH 运行时与配套npminstall--save-exact --legacy-peer-deps\react-native-oh/react-native-harmony0.84.1 react-native-oh/react-native-harmony-cli0.84.1\react-native-worklets0.7.4 react-native-ohos/react-native-worklets1.0.0\hermes-compiler250829098.0.9几个包的分工说一下。expo-battery还是原来那个 JS 包业务代码继续从它 import。expo-harmony/expo-battery是对应的鸿蒙原生实现里面是编译好的 HAR 产物两个成对安装代码里的 import 不用动。expo-harmony/expo提供鸿蒙侧的应用宿主expo-harmony/expo-modules-core是 Expo Modules 的鸿蒙运行时。react-native-worklets要显式装 0.7.4。react-native-ohos/react-native-worklets自己依赖的版本是 0.7.1而expo-harmony/expo-modules-core的 peer 要求 0.7.4 以上所以要在根上把版本钉住。hermes-compiler用--save-exact它的版本要和 RNOH 原生包里内嵌的 Hermes 配套不能随手升。--legacy-peer-deps照旧加上。鸿蒙适配包声明了 RNOH 侧的 peer 依赖分步安装的中间态会让 npm 报冲突。如果项目里用了 react-native-safe-area-context 这类库鸿蒙侧再装对应的适配包这里是react-native-ohos/react-native-safe-area-context。官方包留着iOS 和 Android 继续用。适配包的 package.json 里有一段harmony.alias声明打包时 Metro 靠它把 import 自动重定向过去业务代码不用写任何平台分支。配置 Metro 和打包入口// metro.config.jsconst{getDefaultConfig}require(expo/metro-config);const{mergeConfig}require(react-native/metro-config);const{withHarmonyConfig}require(expo-harmony/metro-config);constconfig{};constisHarmonyprocess.env.EXPO_HARMONY1;module.exportswithHarmonyConfig(mergeConfig(getDefaultConfig(__dirname),config),{enabled:isHarmony,// expo-harmony 命令会自动设置 EXPO_HARMONY1projectRoot:__dirname,});withHarmonyConfig是 JS 侧的核心。enabled 用环境变量做开关expo-harmony命令跑起来会自动设置EXPO_HARMONY1平时 iOS 和 Android 的打包走原来的配置互不影响。鸿蒙打包生效时react-native 会被解析到 RNOH带harmony.alias声明的第三方库会被指到鸿蒙适配版Expo Modules 需要的初始化也会接进来。所以不要只给 react-native 手动配一个别名就完事那样初始化流程是缺的。再新建一个react-native.config.js一行就够。module.exportsrequire(react-native-oh/react-native-harmony-cli/react-native.config.js);Babel 不用动前面接 Expo 时已经换成 babel-preset-expo 了。package.json里补一个main字段指向 index.js再加四个脚本。{main:index.js,scripts:{start:harmony:expo-harmony start,run:harmony:expo-harmony run,build:harmony:expo-harmony build,doctor:harmony:expo-harmony doctor}}有一个名字对齐的要求容易漏。index.js 里AppRegistry.registerComponent注册的名字要和鸿蒙首页ExpoRNApp的 appKey 一致这里都是 ExpoSampleApp。对不上应用起不来报 appKey 未注册之类的错。准备 harmony 原生工程bare 路线不跑 prebuild原生工程从 apps/bare/harmony 整个复制过来。模板里要改的只有四个名字。文件改什么harmony/AppScope/app.json5bundleName 改成自己的harmony/AppScope/resources/base/element/string.json应用显示名harmony/entry/src/main/resources/base/element/string.jsonAbility 标签等harmony/entry/src/main/ets/pages/Index.etsappKey 对齐注册名bundleName 有个硬性要求至少三段。com.exposampleapp这种两段的会被 Hvigor 的校验直接拒绝后面补一段变成com.exposampleapp.app就过了。这个工程承担鸿蒙侧的全部原生工作ArkTS 的 Ability 和首页、C 侧的包注册、Hvigor 构建链都在里面。这些源文件提交进自己的仓库oh_modules 和构建产物加进 ignore。构建时自动生成的模块注册文件不用手写也不用提交。自动链接和原生依赖npx expo-harmony-autolinkinglink--project-root.--harmony-project-path ./harmonycdharmony ohpminstall--allcd..link 命令会解析当前装了哪些 Expo 模块生成 ArkTS 和 C 两侧的包注册文件并把鸿蒙原生依赖写进harmony/oh-package.json5。ohpm 的角色类似 CocoaPods装的是鸿蒙原生依赖。第一次构建前必须先跑这两步不然 Hvigor 找不到原生包。装完可以用npx expo-harmony-autolinking search看一眼识别结果这里识别出 4 个原生模块expo-harmony/expo、expo-harmony/expo-battery、expo-harmony/expo-modules-core和 worklets。safe-area-context 那种属于 JS 层的别名重定向不在原生模块清单里是正常的。环境检查与运行npmrun doctor:harmonydoctor 会过一遍环境和配置SDK 路径、hdc、ohpm、hvigorw、Metro 配置、识别到的原生模块一共 11 项。哪项红了照着提示修就行。npmrun run:harmonyrun 一条命令做完剩下所有事构建 debug HAP、选择或拉起模拟器、安装、启动应用、启动 Metro、设置端口反向映射。第一次构建要编译 C大概四五分钟之后就快了。机器上只有一个模拟器实例时会自动选它有多个就用--device指定。这里有个坑要提醒。如果之前给 iOS 或 Android 调试时起的 Metro 还挂在 8081 端口上run 会直接复用它但那个 Metro 没有鸿蒙的环境变量应用会报ERR_HARMONY_METRO_MANIFEST。切到鸿蒙之前先把旧的 Metro 停掉让 CLI 自己拉起一个带鸿蒙配置的。跑起来之后 Metro 日志里能看到 import 重定向的记录。[INFO] Redirected imports to 3 harmony-specific third-party package(s): [INFO] • expo-modules-core → expo-harmony/expo-modules-core [INFO] • react-native-safe-area-context → react-native-ohos/react-native-safe-area-context [INFO] • react-native-worklets → react-native-ohos/react-native-workletsreact-native 本身也指向了 RNOH。这些映射都来自各适配包里的harmony.alias声明由withHarmonyConfig执行没有一处需要手写。在鸿蒙模拟器上验证App 读出电量 45%和系统状态栏的电量图标一致。电源状态显示未知低电量模式关闭与模拟器的实际状态对得上。事件链路也验证了一下。DevEco 自带的 Emulator 命令行工具可以改模拟器电量执行一条-battery 80几秒后 App 的已收事件从 0 变成 1说明系统电量变化从原生监听一路送到了 JS 的addBatteryLevelListener。页面上的大数字还停在 45%这是因为 demo 的监听器只计数、不回写快照三个平台行为一致不是鸿蒙的问题。顺带把三个平台的电量读数差异放在这里。Android 的 AVD 默认满电读出来一般就是 100%。iOS 模拟器没有电池仿真读到未知。鸿蒙模拟器默认 45%还可以模拟任意电量。所以验证模块接入是否成功看调用能不能走通、事件有没有流动就够了数值本身随模拟器走。Release 构建用npm run build:harmony导出的 Hermes 字节码会内嵌进 HAP运行时不依赖 Metro。签名在harmony/build-profile.json5里配模拟器允许不签真机要装对应的证书和 profile。盘点一下改动接入鸿蒙这一步业务代码一行没改App.tsx 还是 iOS 和 Android 在用的那份。ios/ 和 android/ 两个目录没动。真正的改动是装了一组expo-harmony和 RNOH 的依赖metro.config.js 加了几行新建了一个一行的 react-native.config.jspackage.json 补了 main 和四个脚本再加一份从模板复制来的 harmony 工程模板里改的只有四个名字。对比一下不做这些要面对什么。自己写 expo-battery 的鸿蒙原生实现自己搭 Hvigor 工程自己处理每个模块的原生注册。expo-harmony 把这些活都包掉了应用多了一个平台业务代码还是原来那份。
