一次真实、完整、可复现的环境搭建记录:在一台 HarmonyOS PC(HUAWEI MateBook Pro / HAD-W32)上,从零把 React Native for OpenHarmony(RNOH)的开发环境跑通,并让一个 RN 应用真正在本机安装、启动、渲染出界面。

本文不是官方文档的复述,而是踩过的每一个坑 + 定位过程 + 最终解法。所有命令、版本号、报错信息都来自真实终端。

0. 先交代结论

如果你只想看结果:最终形态是

  • React Native 0.84.1 + RNOH 0.84.4(对应官方 0.84-stable 分支)
  • 工具链由 HNP 包 hmos-clt 提供:ohpm 6.0.1 / hvigorw 7.26.8 / hdc 3.2.0f / OpenHarmony SDK API 26
  • Node 用 harmonybrew 的 v26.12.0,hvigor 构建不需要 JDK
  • 工程结构:AwesomeProject/(RN 侧)+ AwesomeProject/harmony/(鸿蒙侧)
  • 三个必须知道的反直觉坑:CLI 在 Node 22+ 崩、本机文件系统偶发 EINTR、构建期间改源码会让 ninja 状态脱节

应用最终在这台鸿蒙 PC 上跑起来的样子:

Welcome to React Native

Version: 0.84.1

JS Engine: Hermes (250829098.0.9)

1. 为什么要在一台鸿蒙 PC 上折腾 RNOH

React Native for OpenHarmony(RNOH)是 OpenHarmony 社区里把 React Native 框架适配到鸿蒙的工程,它让你用 RN 的写法(JS/TS + React 组件)去开发鸿蒙原生应用。

官方文档 CPF-RN/ohos_react_native[1] 的 《环境搭建》[2] 章节,默认假设你在一台 Windows / macOS 上用 DevEco Studio 图形界面做开发:

  1. 装 DevEco Studio[3]
  2. 用 IDE 新建 Empty Ability 工程
  3. 在 IDE 里点 File > Project Structure > Signing Configs,登录华为账号自动签名
  4. 点 Run 运行

但这次的情况不太一样:开发机本身就是一台鸿蒙 PC。于是问题变成——在没有把 DevEco Studio 的 SDK 目录单独铺开的情况下,能不能纯命令行把这条路走通?

答案是能。而且走通之后发现,命令行链路其实比“必须开 IDE”清爽得多。

2. 环境侦察:这台机器上到底有什么

动手之前先摸清楚家底。这里有一个很容易踩的认知陷阱:官方的 CLI 工具链并不在常规的“安装目录”里,而是被打包成了一个 HNP 包。

2.1 基础系统信息

$ uname -a

HarmonyOS localhost HongMeng Kernel 1.13.0 #1 SMP aarch64 Toybox

$ hdc -v

Ver: 3.2.0f

一台 aarch64 的鸿蒙机器,hdc(鸿蒙的设备调试工具)已经有了。

2.2 关键发现:HNP 包里的完整工具链

先看有哪些工具:

$ for c in git node npm java cmake ninja python3 hdc ohpm hvigorw; do

printf “%-8s ” “$c”; command -v $c || echo “MISSING”

done

git      MISSING

node     /storage/Users/currentUser/.harmonybrew/bin/node

npm      /storage/Users/currentUser/.harmonybrew/bin/npm

java     MISSING

cmake    MISSING

ninja    MISSING

python3  MISSING

hdc      /storage/Users/currentUser/.local/bin/hdc

ohpm     MISSING

hvigorw  MISSING

git、java、python3、cmake 全没有,ohpm/hvigorw 也不在 PATH 上。乍一看条件很差。

但顺着 hdc 的软链找过去,会发现一个 HNP(HarmonyOS Native Package)包:

$ ls /data/service/hnp/

hmos-clt.org

$ ls /data/service/hnp/hmos-clt.org/hmos-clt_1.0.0/

ace-server  clangd  codelinter  hvigor  node  ohpm  sdk

hmos-clt(HarmonyOS Command Line Tools)就是一个完整的命令行工具链,里面包含:

目录 内容
ohpm/ 鸿蒙包管理器 ohpm 6.0.1
hvigor/ 鸿蒙构建系统 hvigorw 7.26.8
node/ 自带 Node v24.18.1
sdk/ 完整 OpenHarmony SDK

(ets / js / native / previewer / toolchains)

codelinter/ 代码检查

SDK 版本:

$ cat …/sdk/default/openharmony/ets/oh-uni-package.json

{

“apiVersion”: “26”,

“displayName”: “Ets”,

“platformVersion”: “26.0.0”,

“releaseType”: “Release”,

“version”: “26.0.0.105”

}

API 26,正好和本机 HarmonyOS 7.0 匹配。

还有一个意外之喜——SDK 里自带了 cmake 和 ninja:

$ …/sdk/default/openharmony/native/build-tools/cmake/bin/cmake –version

cmake version 3.28.2

$ …/sdk/default/openharmony/native/build-tools/cmake/bin/ninja –version

1.13.2

也就是说,前面看到的 cmake MISSING / ninja MISSING 根本不是问题——hvigor 会自己从 SDK 里找,不需要我们手工装。

2.3 hvigor 到底需不需要 JDK?

这是官方文档没写清楚、但会直接决定你要不要下一整个 JDK 的问题。

DevEco Studio 会捆绑一个 JBR(JetBrains Runtime),所以大家习惯性认为“鸿蒙构建要 Java”。但在纯命令行环境下,得验证。

先看代码:

$ grep -rn “JAVA_HOME\|javac\|java -jar” …/hvigor –include=*.js

(无输出)

再看实际调用的工具:

$ file …/sdk/default/openharmony/toolchains/lib/hap-sign-tool

hap-sign-tool: ELF shared object, 64-bit LSB arm64 …

$ file …/sdk/default/openharmony/toolchains/lib/ohos_packing_tool

ohos_packing_tool: ELF shared object, 64-bit LSB arm64 …

签名工具和打包工具都是原生 ELF 可执行文件,不是 .jar 包。

结论:hvigor 构建不需要 JDK。 后来整条链路(编译 → 打包 → 签名)也验证了这一点。

省掉一整个 JDK 安装,这在鸿蒙 PC 上(apt/brew 生态都不完整)意义很大。

3. 版本选型:为什么是 0.84 而不是最新的 0.86

RNOH 是多版本线并行的项目,同一个仓库里有 0.72-stable、0.77-stable、0.82-stable、0.84-stable 等多个分支。

选版本有三个约束:

  1. RN 和 RNOH 的版本必须对齐(前端包与原生包版本号对齐)
  2. OHOS SDK 要支持(本机只有 API 26)
  3. ohpm 没有 dist-tag,原生包必须按具体版本号安装

3.1 先看有哪些版本

先查 npm 上 `@react-native-oh/react-native-harmony`[4] 的 dist-tag:

$ npm view @react-native-oh/react-native-harmony dist-tags –json

{

“beta”: “0.82.7”,

“0.72-stable”: “0.72.140”,

“0.77-stable”: “0.77.71”,

“0.82-stable”: “0.82.30”,

“0.84-rc”: “0.84.2”,      <– 注意:0.84 线上还没有 stable tag

“latest”: “0.72.145”

}

有个细节值得注意:**0.84 线当时只有 0.84-rc tag,没有 0.84-stable**。但实际版本列表里已经有更新的补丁号:

$ npm view @react-native-oh/react-native-harmony versions –json | grep 0.84

“0.84.1” “0.84.2” “0.84.3” “0.84.4”

所以文档里说的”用 0.84-stable 拿最新稳定版”在当时是不成立的——这个 tag 还没发。得直接指定具体版本号。

3.2 确定配套的 RN 版本

不要猜,直接读包的 peerDependencies:

$ npm view @react-native-oh/react-native-harmony@0.84.4 peerDependencies –json

{ “react-native”: “0.84.1” }

于是配套关系确定:

包 版本
react-native 0.84.1
@react-native-oh/react-native-harmony 0.84.4
@react-native-oh/react-native-harmony-cli 0.84.4

3.3 顺手验证镜像源

后面要配华为云 npm 镜像[5]加速,先确认它不会漏包——RC 版本的包在镜像上容易滞后:

$ npm view @react-native-oh/react-native-harmony@0.84.4 version \

–registry=https://repo.huaweicloud.com/repository/npm/

0.84.4

$ npm view react-native versions –json \

–registry=https://repo.huaweicloud.com/repository/npm/ | grep 0.84

“0.84.0” “0.84.1”

镜像覆盖完整,可以放心配。

4. 补齐工具链

盘点下来只缺一样东西:git。

ohpm、hvigorw、hdc、cmake、ninja、SDK 全都在 hmos-clt 里,node 有 harmonybrew。但 git 没有——而 RN 开发(初始化工程、拉依赖、管理版本)没它很难受。

4.1 第一个坑:harmonybrew 装不上 git

鸿蒙 PC 上有 harmonybrew[6](Homebrew[7] 的鸿蒙移植版),理论上 brew install git 就行:

$ brew install git

==> Installing git dependency: bzip2 (1.0.8_2)

==> Pouring bzip2-1.0.8_2.arm64_ohos.bottle.tar.gz

Error: git: Interrupted system call @ rb_file_s_stat –

…/vendor/bundle/ruby/4.0.0/extensions/aarch64-linux-ohos/…/ruby-prof-2.0.5

重试一次,报错换了个文件,但病因一样:

Error: Interrupted system call – bs_fetch:open_current_file:open:

…/Homebrew/retryable_download.rb

注意这个报错——Interrupted system call,也就是 EINTR。它后面还会反复出现,是这台机器上的一条主线(见第 9 章)。而且关键在于:它在读 Homebrew 自己的 Ruby 源码时就失败了,说明不是 git 这个包的问题,是 Homebrew 在这台机器上的文件访问不稳定。

与其跟 Homebrew 缠斗,不如找现成的。

4.2 曲线救国:Homebrew 内置的 portable-git

Homebrew 自己需要一个 git 来更新 formula,所以它内置了一份 portable-git:

$ ls ~/.harmonybrew/Homebrew/Library/Homebrew/vendor/portable-git/current/bin/

git  git-cvsserver  git-receive-pack  git-shell  git-upload-archive  git-upload-pack  scalar

$ …/portable-git/current/bin/git –version

git version 2.55.0

版本 2.55.0,可用。但有一个必须确认的点:它有没有 https 支持。git 的远程 helper 在 libexec/git-core/ 下,早期版本的目录列表里看不到 git-remote-https,如果缺了就没法 git clone https://…:

$ ls …/libexec/git-core/git-remote-https

…/libexec/git-core/git-remote-https     # 存在,放心

链接到 PATH 里(~/.local/bin 已经在前面的 .zshrc 里):

P=~/.harmonybrew/Homebrew/Library/Homebrew/vendor/portable-git/current

ln -sf $P/bin/git    ~/.local/bin/git

ln -sf $P/bin/scalar ~/.local/bin/scalar

验证不能只看 –version,要真拉一次仓库:

$ git –version

git version 2.55.0

$ git clone –depth 1 https://atomgit.com/CPF-RN/ohos_react_native.git _test

Cloning into ‘_test’…

Updating files: 100% (4561/4561), done.     # https 克隆正常

4.3 顺便排掉一个”幽灵 node”

在调试 PATH 顺序时撞见一个诡异现象:把 ~/.local/bin 排到最前面后,node 直接报

sh: /storage/Users/currentUser/.local/bin/node: Permission denied

查下去发现 ~/.local/bin/ 里躺着三个指向另一个 Node 安装的软链:

$ ls -la ~/.local/bin/node ~/.local/bin/npm

node -> /storage/Users/currentUser/.local/opt/node/bin/node

npm  -> /storage/Users/currentUser/.local/opt/node/bin/npm

也就是这台机器上其实有三份 Node:

Node 版本 npm 状态
harmonybrew v26.12.0 11.20.0 ✅ 实际生效
~/.local/opt/node — 10.9.3 ⚠ 软链不可执行
hmos-clt/node v24.18.1 — 备用

之所以日常没出问题,是因为 .zshrc 里 brew shellenv 把 harmonybrew 的路径排在了 ~/.local/bin前面,那份坏软链被遮住了。

这提醒了一件事:验证环境时要用“真实 shell 加载顺序”去测,而不是自己拼一个 PATH 去测,否则测出来的问题可能根本不存在,或者反过来漏掉真实问题。用 zsh 复现真实环境:

$ zsh -c ‘source ~/.zshrc >/dev/null 2>&1; command -v node; node -v’

/storage/Users/currentUser/.harmonybrew/bin/node

v26.12.0

4.4 写入环境变量

现在把 RNOH 需要的环境变量固化到 ~/.zshrc:

# ===== HarmonyOS / React Native for OpenHarmony (RNOH) 开发环境 =====

# hmos-clt (HarmonyOS Command Line Tools) 提供 hdc / ohpm / hvigorw / OpenHarmony SDK

export HMOS_CLT_HOME=”/data/service/hnp/hmos-clt.org/hmos-clt_1.0.0″

export DEVECO_SDK_HOME=”$HMOS_CLT_HOME/sdk”

export OHPM_HOME=”$HMOS_CLT_HOME/ohpm”

# hdc 在 SDK 的 toolchains 下;ohpm / hvigorw 在工具链根目录

export PATH=”$HMOS_CLT_HOME/ohpm/bin:$HMOS_CLT_HOME/hvigor/bin:$HMOS_CLT_HOME/sdk/default/openharmony/toolchains:$PATH”

# RNOH 工程默认走 CAPI 架构,必须为 1

export RNOH_C_API_ARCH=1

# hdc 服务端口,任意未被占用的端口即可

export HDC_SERVER_PORT=7035

两个变量是 RNOH 的硬要求,漏了会出各种怪问题:

  • **RNOH_C_API_ARCH=1**:RNOH 的 Demo 工程默认是 CAPI 版本,文档里专门有一节讲它。少了它,C++ 侧的桥接会走另一套逻辑。
  • **HDC_SERVER_PORT=7035**:hdc 服务端口。真机调试(安装、日志、推送 bundle)都要用。

4.5 配置 npm 镜像源

~/.npmrc(保留原有的 prefix,加上华为云镜像):

prefix=/storage/Users/currentUser/npm

registry=https://repo.huaweicloud.com/repository/npm/

strict-ssl=false

改完 registry 必须清缓存,否则旧的 registry 结果还会命中:

$ npm cache clean –force

官方文档里还写了 sslVerify=false,但 npm 并没有这个配置项(那是别的工具的参数),加了只会产生 unknown config 警告,所以这里只保留有效的 strict-ssl。

4.6 验收

$ zsh -c ‘source ~/.zshrc >/dev/null 2>&1

for c in node npm ohpm hvigorw hdc git; do

printf “%-8s %s\n” “$c” “$(command -v $c)”

done’

node     /storage/Users/currentUser/.harmonybrew/bin/node     # v26.12.0

npm      /storage/Users/currentUser/.harmonybrew/bin/npm      # 11.20.0

ohpm     …/hmos-clt_1.0.0/ohpm/bin/ohpm                     # 6.0.1

hvigorw  …/hmos-clt_1.0.0/hvigor/bin/hvigorw                # 7.26.8

hdc      …/sdk/default/openharmony/toolchains/hdc           # 3.2.0f

git      /storage/Users/currentUser/.local/bin/git            # 2.55.0

顺手确认一件事:**ohpm / hvigorw 只从 PATH 里取 node**(脚本里是 NODE_CMD=node,或读 NODE_HOME),并不强制用自带的 Node。实测系统 node 26 与它们兼容:

$ ohpm -v      # 6.0.1

$ hvigorw –version   # 7.26.8

所以不需要把 hmos-clt/node 塞进 PATH 去替换系统 Node,保持一份 Node 更干净。

5. 创建 React Native 工程

5.1 初始化

文档明确说了 npx react-native init 已废弃,要用新的 `@react-native-community/cli`[8](RN 0.84.1 的 Release Notes[9]):

cd /storage/Users/currentUser/Desktop/rn

npx –yes @react-native-community/cli@latest init AwesomeProject \

–version 0.84.1 \

–pm npm

几个细节:

  • –version 0.84.1:必须显式指定,否则会拉到最新的 RN,和 RNOH 0.84.4 对不上(前面靠 peerDependencies 验过就是 0.84.1)。
  • –pm npm:这个参数建议加上。CLI 初始化时会问用哪个包管理器(yarn/npm/pnpm/bun),在非交互场景下就会卡住。显式指定后全程无提示。
  • –skip-git-init 这类参数没加:机器上已经有 git 了,让它正常初始化仓库即可。

结果:

✔ Downloading template

✔ Copying template

✔ Processing template

✔ Installing dependencies

产物是一个标准的 RN 工程(App.tsx / metro.config.js / android/ / ios/ / package.json …),依赖装完约 442 MB。

5.2 安装 RNOH 鸿蒙化依赖

cd AwesomeProject

npm install @react-native-oh/react-native-harmony@0.84.4 \

@react-native-oh/react-native-harmony-cli@0.84.4

装完 package.json 里多了两个依赖:

“dependencies”: {

“@react-native-oh/react-native-harmony”: “^0.84.4”,

“@react-native-oh/react-native-harmony-cli”: “^0.84.4”,

“react-native”: “0.84.1”,

…

}

这一步会在 node_modules/@react-native-oh/react-native-harmony/ 下带出三个 .har(鸿蒙原生包):

$ ls -la node_modules/@react-native-oh/react-native-harmony/*.har

react_native_openharmony.har           80138612   # 80 MB,debug 用

react_native_openharmony_release.har   13567179   # release:cpp 编成 .so,ts/ets 保留源码

react_native_openharmony_release2.har  13588935   # release2:ts/ets 也编成 .abc 字节码

80 MB 的 har,下载和后续 ohpm install 都会比较久,心里有数就行。

注意:ohpm 没有 dist-tag,原生包只能按具体版本号装。所以拖依赖时前端包 0.84.4 和原生包 0.84.4 要同时写死,别一边写 tag 一边写版本号。

5.3 配置 metro

RN 的 JS 打包靠 Metro[10]。要让它认识鸿蒙平台,得挂上 RNOH 的 metro 配置。原本的 metro.config.js 只有一行 mergeConfig,改成:

const {mergeConfig, getDefaultConfig} = require(‘@react-native/metro-config’);

const {createHarmonyMetroConfig} = require(‘@react-native-oh/react-native-harmony/metro.config’);

/** @type {import(‘@react-native/metro-config’).MetroConfig} */

const config = {

transformer: {

getTransformOptions: async () => ({

transform: {

experimentalImportSupport: false,

inlineRequires: true,

},

}),

},

};

module.exports = mergeConfig(

getDefaultConfig(__dirname),

createHarmonyMetroConfig({

reactNativeHarmonyPackageName: ‘@react-native-oh/react-native-harmony’,

}),

config,

);

关键就是 createHarmonyMetroConfig——它注册了 harmony 这个平台,Metro 才知道该为鸿蒙产出什么样的 bundle。

5.4 加一条 bundle 脚本

在 package.json 的 scripts 里加:

“dev”: “react-native bundle-harmony –dev”

这条命令由 react-native-harmony-cli 提供(还记得它注册了 bundle-harmony 命令吗),作用是生成鸿蒙用的 JS bundle。

5.5 一个会白屏的坑:App.tsx 的 safe-area

RN 0.84 的模板 App.tsx 长这样:

import { SafeAreaProvider, useSafeAreaInsets } from ‘react-native-safe-area-context’;

function App() {

return (

<SafeAreaProvider>

<StatusBar … />

<AppContent />

</SafeAreaProvider>

);

}

它依赖 react-native-safe-area-context。官方文档专门提示:这个依赖在鸿蒙上会导致白屏,必须移除。替换成把安全区直接写死:

import { NewAppScreen } from ‘@react-native/new-app-screen’;

import { StatusBar, StyleSheet, useColorScheme, View } from ‘react-native’;

function App() {

const isDarkMode = useColorScheme() === ‘dark’;

return (

<View style={styles.container}>

<StatusBar barStyle={isDarkMode ? ‘light-content’ : ‘dark-content’} />

<NewAppScreen

templateFileName=”App.tsx”

safeAreaInsets={{ top: 0, bottom: 0, left: 0, right: 0 }}

/>

</View>

);

}

const styles = StyleSheet.create({ container: { flex: 1 } });

export default App;

这是一个很容易被忽略但一定会踩的点:环境全配对了、HAP 也装上了,结果打开就是白屏,最后发现是模板自带的 safe-area 依赖。

6. 生成鸿蒙 bundle

$ npm run dev

Welcome to Metro v0.83.8

[INFO] No harmony-specific third-party packages have been detected

info Created harmony/entry/src/main/resources/rawfile/bundle.harmony.js

info Copied 7 assets

产物落在鸿蒙工程的 rawfile 目录:

$ ls harmony/entry/src/main/resources/rawfile/

assets  bundle.harmony.js

  • bundle.harmony.js:打包后的 JS(本项目约 5.4 MB)
  • assets/:bundle 里引用到的图片资源(这里是 RN 的 LogBox 图标和 new-app-screen 的插图)

这里有个顺序上的隐含依赖:bundle 要产出到 harmony/ 目录下,所以得先有鸿蒙工程。下一章先讲怎么把鸿蒙工程建出来,如果按本文顺序实操,把第 6、7 章调换一下即可。

7. 创建鸿蒙工程:别手搓,CLI 自带模板

官方文档在这一步是图形界面操作:打开 DevEco Studio → File > New > Create Project → 选 Empty Ability → 下一步 → 选 Compile SDK → 建一个叫 MyApplication 的工程。

问题来了:纯命令行怎么做?没有 IDE 的“新建工程”按钮。

7.1 一个意外的发现

本来打算手搓工程结构(AppScope/app.json5、module.json5、build-profile.json5、resources/… 一堆文件,写错一个就构建失败)。先去翻参考实现,结果在 react-native-harmony-cli 的源码里看到了这个:

$ find packages/react-native-harmony-cli/src -maxdepth 2 -type d

src/autolinking

src/codegen

src/commands        <– 命令实现

src/init            <– 工程模板!

src/init/templates

src/io

src/simulator

$ ls src/commands

bundle-harmony.ts   clean-harmony.ts   codegen-harmony.ts

codegen-lib-harmony.ts   init-harmony.ts   init.ts

link-harmony.ts   run-harmony.ts   verify-package-harmony.ts

CLI 自带一个 init-harmony 命令(源码:src/commands/init-harmony.ts[11]),而且 src/init/templates/harmony/[12] 下是一套完整的鸿蒙工程模板:

$ find src/init/templates/harmony -type f | head -20

harmony/AppScope/app.json5

harmony/AppScope/resources/base/element/string.json

harmony/AppScope/resources/base/media/app_icon.png

harmony/build-profile.json5

harmony/codelinter.json

harmony/entry/build-profile.json5

harmony/entry/hvigorfile.ts

harmony/entry/oh-package.json5

harmony/entry/src/main/cpp/CMakeLists.txt

harmony/entry/src/main/cpp/PackageProvider.cpp

harmony/entry/src/main/ets/entryability/EntryAbility.ets

harmony/entry/src/main/module.json5

harmony/hvigor/hvigor-config.json5

harmony/hvigorfile.ts

harmony/oh-package.json5

…

这比手搓靠谱得多——它是官方维护的、和当前 RNOH 版本配套的工程骨架。

命令的参数也很清楚:

$ npx react-native init-harmony –help

–bundle-name <string>              # 必填,三段式 bundle name,如 com.org.project

–project-root-path [path]          # 默认 .

–app-name [string]                 # 默认取 package.json 的 name

–rnoh-npm-package-name [string]

–rnoh-cli-npm-package-name [string]

bundle-name 这里值得停一下。它决定签名能不能过。本项目里之所以选 com.nutpi.myapplication,是因为机器上已经有一份 DevEco 生成过的 debug 签名材料,其 profile 绑定的正是这个 bundle name(详见第 8 章)。如果随便起个名字,签名就得重来。

7.2 第二个坑:命令直接崩了

$ npx react-native init-harmony –bundle-name com.nutpi.myapplication

error The “paths[0]” argument must be of type string. Received undefined.

TypeError [ERR_INVALID_ARG_TYPE]: The “paths[0]” argument must be of type string.

at Object.resolve (node:path:1257:7)

at new AbsolutePath (…/react-native-harmony-cli/dist/core/AbsolutePath.js:20:42)

at get path (…/react-native-harmony-cli/dist/io/RealFS.js:40:16)

at findRNOHHvigorPluginPath (…/dist/commands/init-harmony.js:203:23)

at …/dist/commands/init-harmony.js:102:42

AbsolutePath 收到了 undefined。顺着调用栈往回看,findRNOHHvigorPluginPath 在遍历目录项:

// dist/commands/init-harmony.js

function findRNOHHvigorPluginPath(fs, dirPath) {

for (let dirent of fs.readDirentsSync(dirPath)) {

if (dirent.isDirectory() || !dirent.name.startsWith(‘rnoh-hvigor-plugin’)) {

continue;

}

return dirent.path;      // <– 这里

}

return null;

}

而 dirent.path 是由 RealFS 里的 getter 拼出来的:

// dist/io/RealFS.js

get path() {

return new core_1.AbsolutePath(this.nodeFSDirent.path)

.copyWithNewSegment(this.nodeFSDirent.name);

}

它读的是 Node.js fs.Dirent 的 .path 属性。

7.3 根因:Node 24+ 移除了Dirent.path

Node 的 `fs.Dirent`[13] 原本有 path(父目录路径),但它在 v20.12 起被废弃,改名为 `parentPath`[14],并在 v24.0.0 被彻底移除(DEP0178[15] | 中文版[16])。

实测本机两个 Node:

$ node -e “const d=require(‘fs’).readdirSync(‘.’,{withFileTypes:true})[0];

console.log(‘path:’, d.path, ‘| parentPath:’, d.parentPath)”

# node v26.12.0 (harmonybrew)

path: undefined | parentPath: .

$ /data/…/hmos-clt_1.0.0/node/bin/node -e “同上”

# node v24.18.1 (hmos-clt)

path: undefined | parentPath: .

两个版本都是 undefined。

这里就出现了一个尴尬的版本死锁:

  • RN 0.84 要求 Node >= 22.11.0(package.json 的 engines)
  • 而 CLI 需要 Dirent.path,它在 22.x 之后被移除了
  • 本机只有 Node 24 和 26,没有 22

所以没有一个可用版本能同时满足两者,只能改 CLI。

7.4 三种解法与选择

方案 评价
降级到 Node 22 本机没有,且和 RN 0.84 的 engines 打架,还会牵连 harmonybrew 的全局环境
不用 CLI,手工复刻模板 要手改十几个文件 + 模板变量渲染(appName/bundleName/metro 配置都要替换),易错且后续 CLI 升级会脱节
给 CLI 打个兼容补丁 改 2 行,语义完全等价,风险最小 ✅

补丁就是把取值改成”优先 parentPath,回退 path”:

  get path() {

–     return new core_1.AbsolutePath(this.nodeFSDirent.path).copyWithNewSegment(this.nodeFSDirent.name);

+     // Node 22+ removed `dirent.path` (replaced by `parentPath`); fall back for older runtimes.

+     const parentPath = this.nodeFSDirent.parentPath ?? this.nodeFSDirent.path;

+     return new core_1.AbsolutePath(parentPath).copyWithNewSegment(this.nodeFSDirent.name);

}

改的文件是:

node_modules/@react-native-oh/react-native-harmony-cli/dist/io/RealFS.js

⚠ 这个补丁在 node_modules 里,npm install 之后会丢,必须重新打。

工程化一点的建议是用 patch-package 把它固化下来;本次为了保持环境最小改动,只做了手工补丁,并在环境说明里记了一笔。

7.5 重跑,工程生成成功

$ npx react-native init-harmony –bundle-name com.nutpi.myapplication

warn Overwritten “./metro.config.js”

success Created “./harmony” directory

info Run on a physical device:

1. Run `npm start` to start the Metro server.

2. Open `./harmony` project in DevEco Studio and wait for background jobs to finish.

3. …

注意那行 warn Overwritten “./metro.config.js”——CLI 自己会写 metro 配置。所以第 5.3 节手工写 metro.config.js 其实是可选的,init-harmony 会生成一份等价的。反过来也要注意:如果你已经手工改过 metro.config.js,跑 init-harmony 会被覆盖。

生成的工程结构:

$ find harmony -type f -not -path “*/oh_modules/*” | sort

harmony/.gitignore

harmony/AppScope/app.json5

harmony/AppScope/resources/base/element/string.json

harmony/AppScope/resources/base/media/app_icon.png

harmony/build-profile.json5

harmony/build-profile.template.json5

harmony/codelinter.json

harmony/entry/.gitignore

harmony/entry/build-profile.json5

harmony/entry/hvigorfile.ts

harmony/entry/oh-package.json5

harmony/entry/src/main/cpp/CMakeLists.txt

harmony/entry/src/main/cpp/PackageProvider.cpp

harmony/entry/src/main/ets/PackageProvider.ets

harmony/entry/src/main/ets/codegen/.gitignore

harmony/entry/src/main/ets/entryability/EntryAbility.ets

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

harmony/entry/src/main/ets/workers/RNOHWorker.ets

harmony/entry/src/main/module.json5

harmony/entry/src/main/resources/base/element/color.json

harmony/entry/src/main/resources/base/element/string.json

harmony/entry/src/main/resources/base/media/*.png

harmony/entry/src/main/resources/base/profile/main_pages.json

harmony/entry/src/main/resources/rawfile/        # bundle 落这里

harmony/hvigor/hvigor-config.json5

harmony/hvigorfile.ts

harmony/oh-package.json5

几个关键文件值得单独看一眼:

harmony/hvigor/hvigor-config.json5 —— 它把 RNOH 的 hvigor 插件作为依赖引进来:

{

“modelVersion”: “5.0.0”,

“dependencies”: {

“@rnoh/hvigor-plugin”: “../../node_modules/@react-native-oh/react-native-harmony-cli/harmony/rnoh-hvigor-plugin-0.84.4.tgz”

},

…

}

这就是前面 findRNOHHvigorPluginPath 要找的那个 rnoh-hvigor-plugin-*.tgz——它是 CLI 包自带的一个本地 hvigor 插件包,负责在构建时把 RNOH 接进来。

**harmony/hvigorfile.ts**:

import { appTasks } from ‘@ohos/hvigor-ohos-plugin’;

import { createRNOHProjectPlugin } from ‘@rnoh/hvigor-plugin’;

export default {

system: appTasks,

plugins: [createRNOHProjectPlugin()],

};

**harmony/entry/src/main/cpp/CMakeLists.txt**:C++ 侧的入口,编译出 librnoh_app.so。注意它引用了 oh_modules 里的 RNOH 源码:

set(RNOH_CPP_DIR “${CMAKE_CURRENT_SOURCE_DIR}/../../../../oh_modules/@rnoh/react-native-openharmony/src/main/cpp”)

…

add_subdirectory(“${RNOH_CPP_DIR}” ./rn)

include(“${CMAKE_CURRENT_SOURCE_DIR}/autolinking.cmake”)

add_library(rnoh_app SHARED

${GENERATED_CPP_FILES}

“./PackageProvider.cpp”

“${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp”

)

target_link_libraries(rnoh_app PUBLIC rnoh)

autolink_libraries(rnoh_app)

这就是为什么后面必须 ohpm install——C++ 编译需要 oh_modules 里的 RNOH 原生源码。

7.6 安装鸿蒙依赖

cd harmony

ohpm install –all

install completed in 68s 630ms。这一步会把 80 MB 的 react_native_openharmony.har 解开到 oh_modules(本项目最终 331 MB),C++ 编译要用。依赖来自 ohpm 官方仓库[17](本机 ~/.ohpm/.ohpmrc 里已配好)。

7.7 顺带修掉的一个配置冲突

init-harmony 生成的 build-profile.json5 里,SDK 版本是:

compatibleSdkVersion: ‘5.0.0(12)’,

targetSdkVersion: ‘6.0.0(20)’,

但本机 SDK 只有 API 26。这两个值是按老版本 SDK 写的,必须对齐成本机实际的:

compatibleSdkVersion: ‘26.0.0’,

targetSdkVersion: ‘26.0.0’,

另外当时还试过把 hvigor/hvigor-config.json5 的 modelVersion 从 5.0.0 改成 26.0.0(对齐机器上旧工程),结果直接报错:

> hvigor ERROR: 00303027 Configuration Error

Error Message: The modelVersion in hvigor-config.json5 is 26.0.0,

and the modelVersion in oh-package.json5 is 5.0.0

hvigor-config.json5 和 oh-package.json5 的 modelVersion 必须一致,单独改一个必挂。要么都改,要么都别改——这里选择**回退到模板自带的 5.0.0**,保持和官方模板一致,最少偏离。

官方说明:`hvigor-config.json5` 配置项[18] | `oh-package.json5`[19]

8. 签名:纯命令行下最大的那道坎

8.1 为什么绕不过去

鸿蒙设备不装未签名或签名不被信任的 HAP。而签名需要三样东西凑齐:

材料 作用 来源
.p12 存着私钥的密钥库 本地生成
.cer 开发证书(公钥 + 华为 CA 签发链) 华为签发
.p7b Profile

(描述“哪个应用、能装在哪些设备、debug 还是 release”)

华为签发

关键在 .p7b:它必须由华为签发,并且把目标设备的 UDID 列进去(debug 包)。这不是本地能凭空造出来的——官方流程就是“在 DevEco Studio 里点 Sign In,登录华为账号,自动生成签名”(参考使用真机进行调试[20])。顺带一提,鸿蒙的签名工具链本身是开源的:OpenHarmony/developtools_hapsigner[21]——SDK 里的 hap-sign-tool 就出自它。

纯命令行环境下没有这个按钮。所以这一步要么想办法复用已有材料,要么去走华为的账号体系。

8.2 先看看机器上有什么

翻用户目录,发现 ~/.ohos/config/ 下躺着一整套签名材料:

$ ls -la ~/.ohos/config/

default_MyApplicationI2N-ov7ZD3FZxit__mTtQuOR7WC0uSY36PWyeShdmRI.p12    1003 B

default_MyApplicationI2N-ov7ZD3FZxit__mTtQuOR7WC0uSY36PWyeShdmRI.csr

default_MyApplicationI2N-ov7ZD3FZxit__mTtQuOR7WC0uSY36PWyeShdmRI.cer    2914 B

default_MyApplicationI2N-ov7ZD3FZxit__mTtQuOR7WC0uSY36PWyeShdmRI.p7b    8907 B

文件名里的 MyApplicationI2N-… 说明这是之前某次 DevEco 自动签名为 MyApplication 工程生成的。

先验证这份 profile 能不能用在自己身上。.p7b 是个签名过的信封,拆开看内容:

$ openssl smime -verify -in default_MyApplication*.p7b -inform DER -noverify \

| node -e “let s=”;process.stdin.on(‘data’,d=>s+=d).on(‘end’,()=>{

const j=JSON.parse(s.slice(s.indexOf(‘{‘)));

console.log(‘bundle-name :’, j[‘bundle-info’]?.[‘bundle-name’]);

console.log(‘type        :’, j.type);

console.log(‘device-ids  :’, (j[‘debug-info’]?.[‘device-ids’]||[]).length, ‘个’);

console.log(‘validity    :’, new Date(j.validity[‘not-before’]*1000).toISOString().slice(0,10),

‘~’, new Date(j.validity[‘not-after’]*1000).toISOString().slice(0,10));

})”

输出:

bundle-name : com.nutpi.myapplication

type        : debug

device-ids  : 73 个

validity    : 2026-10-15 ~ 2027-10-15

profile 还有效(有效期一年)。而且它绑定的 bundle name 是 com.nutpi.myapplication —— 这就是第 7.1 节里 init-harmony 必须用这个名字的原因。

那设备在不在那 73 个里面?查本机 UDID:

$ hdc shell bm get –udid

udid of current device is :

<本机设备的 UDID,此处已脱敏>

和 profile 里 device-ids[0] 完全一致 ✅ —— 因为目标设备就是本机自己。

source 材料齐了,但还有一个致命问题。

8.3 第三道坎:p12 的密码是加密的

签名要用私钥,私钥在 .p12 里,打开 .p12 要密码。而 DevEco 生成的这个密码是随机且加密存储的,明文不在任何配置文件里。

试着撞几个常见口令:

$ for pw in “” 123456 000000 123456789 huawei password ohos; do

printf “pass=[%s] -> ” “$pw”

openssl pkcs12 -in default_MyApplication*.p12 -noout -passin “pass:$pw” 2>&1 | head -1

done

pass=[]         -> Mac verify error: invalid password?

pass=[123456]   -> Mac verify error: invalid password?

pass=[000000]   -> Mac verify error: invalid password?

pass=[123456789]-> Mac verify error: invalid password?

pass=[huawei]   -> Mac verify error: invalid password?

pass=[password] -> Mac verify error: invalid password?

pass=[ohos]     -> Mac verify error: invalid password?

全军覆没。密码被 DevEco 用机器级密钥加密后存着:

$ find ~/.ohos/config/material -type f

material/ac/a6f01a4dd39f3974d7e8a94728888c4b     16 bytes

material/ce/0ee5b1f24f314ba95f7abce8b904e8bb     48 bytes   # 像 AES-GCM 的密文

material/fd/0/d08f07bc410c579b12c59bea21038ae7   16 bytes

material/fd/1/d27248cb3a773121855ae389aee0c615   16 bytes

material/fd/2/a27683fd88fe7c8936fbfdfbfb13f429   16 bytes

这些是 DevEco 的加密凭据库,解密要 DevEco 自己的密钥。此路不通。

8.4 转机:翻出之前那套可用的签名配置

按理说该放弃了。但有一个线索:机器上有 ~/.hvigor/project_caches/ 和一个 ~/deveco 目录,说明之前有人在这台机器上跑过 DevEco。

于是全盘搜带签名的工程配置:

$ find ~ -name “build-profile.json5” -not -path “*/node_modules/*”

/storage/Users/currentUser/Documents/DevEcoStudioProjects/MyApplication/build-profile.json5

…

找到了! 那个最初生成签名材料的工程本体还在。打开它的 build-profile.json5:

{

“app”: {

“signingConfigs”: [

{

“name”: “default”,

“material”: {

“storePassword”: “<DevEco 生成的加密串,此处已脱敏>”,

“certpath”: “/storage/Users/currentUser/.ohos/config/default_MyApplication…cer”,

“keyAlias”: “debugKey”,

“keyPassword”: “<DevEco 生成的加密串,此处已脱敏>”,

“profile”:  “/storage/Users/currentUser/.ohos/config/default_MyApplication…p7b”,

“signAlg”: “SHA256withECDSA”,

“storeFile”: “/storage/Users/currentUser/.ohos/config/default_MyApplication…p12”

},

“type”: “HarmonyOS”

}

],

“products”: [

{

“name”: “default”,

“signingConfig”: “default”,

“targetSdkVersion”: “26.0.0”,

“compatibleSdkVersion”: “26.0.0”,

“runtimeOS”: “HarmonyOS”

}

]

}

}

这就是答案。 那串形如 0000xxxxxxxx… 的密文是加密后的密码——我们不需要解开它,因为机器上的 hvigor 本来就会解它(凭据库就在 ~/.ohos/config/material/,是机器级的)。把这段配置原样搬进我们自己的工程,hvigor 就能自己完成解密和签名。

这个工程还顺便给了两个宝贵信息:

  1. 它的 compatibleSdkVersion / targetSdkVersion 都是 26.0.0 —— 印证了第 7.7 节的判断,本机就该用 26。
  2. 它的 entry/build/default/ 目录存在 —— 说明它是成功构建过的,那套配置在这台机器上验证可用。

8.5 最终写入我们工程的签名配置

harmony/build-profile.json5:

{

app: {

signingConfigs: [

{

name: ‘default’,

material: {

storePassword: ‘<DevEco 生成的加密串,原样搬>’,

certpath: ‘/storage/Users/currentUser/.ohos/config/default_MyApplication….cer’,

keyAlias: ‘debugKey’,

keyPassword: ‘<同上>’,

profile: ‘/storage/Users/currentUser/.ohos/config/default_MyApplication….p7b’,

signAlg: ‘SHA256withECDSA’,

storeFile: ‘/storage/Users/currentUser/.ohos/config/default_MyApplication….p12’,

},

type: ‘HarmonyOS’,

},

],

products: [

{

name: ‘default’,

signingConfig: ‘default’,       // 引用上面的签名配置

compatibleSdkVersion: ‘26.0.0’,

targetSdkVersion: ‘26.0.0’,

runtimeOS: ‘HarmonyOS’,

buildOption: { nativeCompiler: ‘BiSheng’ },

},

],

buildModeSet: [{ name: ‘debug’ }, { name: ‘release’ }],

},

modules: [

{ name: ‘entry’, srcPath: ‘./entry’,

targets: [{ name: ‘default’, applyToProducts: [‘default’] }] },

],

}

提醒:安装同 bundle name 的 HAP 会覆盖设备上已安装的同名应用。这次就把那个旧的 DevEco MyApplication 覆盖掉了。如果不想覆盖,得换 bundle name,但同时也就得重新走华为账号签名。

9. 构建 HAP,以及这条路上最耗时的两个坑

9.1 命令

cd AwesomeProject/harmony

export DEVECO_SDK_HOME=”$HMOS_CLT_HOME/sdk”

export RNOH_C_API_ARCH=1

hvigorw assembleHap –no-daemon

–no-daemon 建议加上:hvigor 默认会起常驻 daemon,在非交互、需要反复排查的场景下,不用 daemon 反而更容易看清每次构建的完整过程。

产物:

entry/build/default/outputs/default/

├── entry-default-signed.hap      # 44 MB,签名版

├── entry-default-unsigned.hap

├── mapping

└── pack.info

9.2 第三个坑:Interrupted system call(EINTR)

第一次构建,跑到 C++ 编译阶段,扑了一屏这种错误:

In file included from …/libcxx-ohos/include/c++/v1/string:536:

/…/include/c++/v1/string:536:10: fatal error:

cannot open file ‘…/third-party/boost/libs/type_traits/include/__string/char_traits.h’:

Interrupted system call

#include <__string/char_traits.h>

FAILED: rn/third-party/rn/react/renderer/componentregistry/…/ComponentDescriptorRegistry.cpp.o

ninja: build stopped: subcommand failed.

注意两点:

  1. 报错是 Interrupted system call(EINTR),不是”文件不存在”,也不是用户代码的编译错误。
  2. 报错路径看着很怪 —— 它在 boost 的 include 目录里找一个 __string/char_traits.h。这其实是 C++ 头文件搜索的正常现象:编译器按 -I 顺序逐个试,真正要打开的是 SDK 里的 libc++ 头文件,试到中途某个候选路径时被打断了。

先确认文件本身没问题:

$ S=…/BiSheng/bin/../include/libcxx-ohos/include/c++/v1

$ for f in __string/char_traits.h __variant/monostate.h __compare/common_comparison_category.h; do

[ -r “$S/$f” ] && echo “$f 可读” || echo “$f 缺失”

done

__string/char_traits.h 可读

__variant/monostate.h 可读

__compare/common_comparison_category.h 可读

# 连续读 300 次,看是否稳定

$ fail=0; for i in $(seq 1 300); do cat “$S/__string/char_traits.h” >/dev/null 2>&1 || fail=$((fail+1)); done

$ echo “失败次数: $fail / 300”

失败次数: 0 / 300

文件完全正常,单线程读 300 次零失败。

再看资源水位:

$ free -m

total   used   free

Mem:          31914  29203   2710      # 只剩 2.7 GB

SwapTotal:   52428800                    # 但有 50 GB swap

SwapFree:    45937940

$ cat /proc/loadavg

23.183 22.933 28.318     # 20 核机器,负载 23

$ grep -c ^processor /proc/cpuinfo

20

结论:20 核并行编译、内存接近打满、负载 23。在这种高压并发下,文件系统偶发返回 EINTR —— 这和第 4.1 节 Homebrew 装 git 失败、以及后面 git checkout 报 Interrupted system call 是同一个病因。

这就解释了为什么它表现得像“随机故障”:同一份源码,重跑一次可能就过了。

9.3 应对:重试 + 增量推进

关键在于 **ninja 会保留已编译的 .o**。所以重试不是从零开始,而是接着上次的进度往下推:

for i in 1 2 3 4 5 6; do

echo “########## 尝试 $i ##########” >> build.log

hvigorw assembleHap –no-daemon >> build.log 2>&1

if ls entry/build/default/outputs/default/*.hap >/dev/null 2>&1; then

echo “BUILD_OK after attempt $i” >> build.log; break

fi

echo “attempt $i failed” >> build.log

done

实测每次重试都在推进(.o 文件数 239 → 283 → 376 → 522……),最终一次干净的重建会跑通。

完整的 C++ 全量编译(RN + RNOH + folly + hermes + boost + glog + fmt)大约需要 25~30 分钟,官方文档也专门提示了“全量编译 C++ 代码耗时较长,请耐心等待”。这一步没有任何捷径,第一次构建就是要等。

一个可选优化:如果机器本身内存充裕,可以在 hvigor/hvigor-config.json5 的 nodeOptions.maxOldSpaceSize 上调;但编译期的内存压力主要来自 clang 而不是 hvigor 的 Node 进程,调这个收益有限。

9.4 第四个坑:构建期间改源码,会让 ninja “记忆错乱”

这是最隐蔽的一个。

曾经有一次,构建跑到一半,我顺手改了 harmony/entry/src/main/ets/pages/Index.ets。结果那次构建最后失败在链接阶段,报的是一长串:

FAILED: …/libhermestooling.so

clang++: error: no such file or directory: ‘rn/RNOH/JSEngine/hermes/…/HermesInstance.cpp.o’

clang++: error: no such file or directory: ‘rn/third-party/libfolly.a’

clang++: error: no such file or directory: ‘rn/third-party/glog/libglog.a’

clang++: error: no such file or directory: ‘rn/third-party/boost/libs/context/libboost_context.a’

…

ninja: build stopped: subcommand failed.

去看构建目录,发现了问题:

$ find entry/.cxx -name “*.o” | wc -l

30                 # 之前明明有 522 个

$ find entry/.cxx -name “libfolly.a”

(空)

一大堆 .o 和静态库凭空消失了,但 ninja 的构建日志还认为它们是“最新的”,于是跳过重新编译、直接进入链接 → 链接器找不到文件。

这类症状的经典成因是:ninja 的 .ninja_log 与实际文件系统脱节。而在这里,触发它的很大可能是“构建期间源文件被修改”——hvigor 检测到输入变化后重跑了任务链,而 native 构建目录被重置,两边状态没对齐。

实践建议很朴素:构建期间不要动源码。 改完代码,重新构建。

9.5 成功:签名版 HAP 与签名校验

干净重跑后,产物出来了,而且可以独立验证签名有效性:

$ …/toolchains/lib/hap-sign-tool verify-app \

-inFile entry/build/default/outputs/default/entry-default-signed.hap \

-outCertChain /tmp/out.cer -outProfile /tmp/out.p7b

10-18 13:42:04.287 INFO  – hap verify successed!

10-18 13:42:04.287 INFO  – verify-app success

顺便确认 HAP 里到底装了什么:

$ unzip -l entry-default-signed.hap | grep -E “rawfile|\.so$”

resources/rawfile/bundle.harmony.js                                      5456027

resources/rawfile/rnoh.profdata                                          4834640

libs/arm64-v8a/libreactnative.so                                        13433656

libs/arm64-v8a/librnoh_app.so                                            3182128

libs/arm64-v8a/librnoh_core.so                                           5115120

libs/arm64-v8a/librnoh_core_package.so                                   2982520

libs/arm64-v8a/libhermesvm.so                                            3414896

libs/arm64-v8a/libjsi.so                                                  874168

…

  • librnoh_app.so 就是我们自己的 CMakeLists.txt 编出来的适配层
  • librnoh_core.so / librnoh_core_package.so 来自 RNOH 的 har
  • bundle.harmony.js 就是第 6 章生成的业务 JS

——三段拼起来,一个完整的 RN 鸿蒙应用。

10. 安装到本机并运行

10.1 安装

目标设备就是本机(HAD-W32,API 26),通过 hdc 直接装:

$ hdc install -r entry/build/default/outputs/default/entry-default-signed.hap

[Info]App install path:/storage/…/entry-default-signed.hap

msg:install bundle successfully.

AppMod finish

install bundle successfully —— 说明前面第 8 章那套“复用签名材料”的思路是对的,加密的密码不需要解开,hvigor 自己会解。

顺带确认一下同名应用的情况(避免覆盖掉不想覆盖的东西):

$ hdc shell bm dump -a | grep nutpi

com.nutpi.hongjie

com.nutpi.myapplication        # 旧的那个 DevEco 工程,会被覆盖

10.2 启动

$ hdc shell aa start -a EntryAbility -b com.nutpi.myapplication

start ability successfully.

$ hdc shell “ps -ef | grep nutpi”

20020174  33244  670  0 08:55:34 ?  00:00:01 com.nutpi.myapplication     # 进程活着

进程起来了,start ability successfully。看起来一切顺利。

10.3 第五个坑:装上了、启动了、但是白屏

进程活着,界面却不出来。这种“进程在、无界面”的情况,在 RN 里八成是 bundle 没加载成功。

回头看一件之前在打包时的观察:

$ unzip -l entry-default-signed.hap | grep -iE “rawfile/(bundle|hermes)”

resources/rawfile/bundle.harmony.js    5456027

# 没有 hermes_bundle.hbc

HAP 里装的是 **bundle.harmony.js**。

再看 init-harmony 生成的 Index.ets 是怎么加载 bundle 的:

jsBundleProvider: this.rnohCoreContext?.isDebugModeEnabled

? new AnyJSBundleProvider([

new MetroJSBundleProvider(),                       // 开发者机器上的 Metro 服务

new ResourceJSBundleProvider(…, ‘hermes_bundle.hbc’),   // 只找 .hbc

])

: new ResourceJSBundleProvider(…, ‘hermes_bundle.hbc’),     // 生产也只找 .hbc

它只找 hermes_bundle.hbc,而 HAP 里根本没有这个文件。

具体来说,debug 模式下它会:

  1. 先试 MetroJSBundleProvider → 没起 Metro 服务,失败
  2. 再试 hermes_bundle.hbc → 资源不存在,失败
  3. 两个都失败 → 白屏

而官方文档里那段 Index.ets 示例其实是带回落的,它列了四个 provider:

new AnyJSBundleProvider([

new MetroJSBundleProvider(),

new FileJSBundleProvider(‘/data/storage/el2/base/files/bundle.harmony.js’),

new ResourceJSBundleProvider(rm, ‘hermes_bundle.hbc’),

new ResourceJSBundleProvider(rm, ‘bundle.harmony.js’),   // <– 这行是关键

])

init-harmony 生成的版本少了最后那条 bundle.harmony.js 回落。

10.4 修复

把回落补上(顺便用 AnyJSBundleProvider 统一 debug / release 两条路径,逻辑更简单):

import {

AnyJSBundleProvider,

MetroJSBundleProvider,

RNApp,

RNOHErrorDialog,

ResourceJSBundleProvider,

RNOHCoreContext

} from ‘@rnoh/react-native-openharmony’;

import { getRNOHPackages } from ‘../PackageProvider’;

@Entry

@Component

struct Index {

@StorageLink(‘RNOHCoreContext’) private rnohCoreContext: RNOHCoreContext | undefined = undefined

build() {

Column() {

if (this.rnohCoreContext) {

if (this.rnohCoreContext?.isDebugModeEnabled) {

RNOHErrorDialog({ ctx: this.rnohCoreContext })

}

RNApp({

rnInstanceConfig: {

name: “AwesomeProject”,

createRNPackages: getRNOHPackages,

fontResourceByFontFamily: {},

enableDebugger: this.rnohCoreContext?.isDebugModeEnabled,

},

appKey: “AwesomeProject”,     // 必须与 AppRegistry.registerComponent 的 appName 一致

/**

* 按顺序尝试:

* 1. Metro 服务(npm start,支持热加载)

* 2. hermes_bundle.hbc(Hermes 字节码,生产推荐)

* 3. bundle.harmony.js(npm run dev 产出的本地 bundle)

*/

jsBundleProvider: new AnyJSBundleProvider([

new MetroJSBundleProvider(),

new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, ‘hermes_bundle.hbc’),

new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, ‘bundle.harmony.js’),

]),

})

}

}

.height(‘100%’)

.width(R...