从 0 到 1:react-native-screenshot-aware 鸿蒙适配实战(RNOH 0.84)

记录一个 React Native 三方库完整适配 HarmonyOS/OpenHarmony(RNOH 0.84)的 0-1 过程: 从能力调研、工程搭建、原生模块编写,到 autolinking 接入、构建踩坑、真机验证。


0. 为什么写这篇文章

随着 OpenHarmony / HarmonyOS 生态的发展,React Native OpenHarmony(RNOH)作为跨平台框架已经具备相当的成熟度(本文基于 RNOH 0.84 版本线,即官方 ohos_react_native 仓库的 0.84 分支)。但 RN 生态中绝大多数三方库只实现了 iOS / Android 原生代码,鸿蒙侧需要额外适配

react-native-screenshot-aware 是一个"小而美"的典型三方库:对外 API 简洁(一个 hook + 一个事件),原生逻辑聚焦单一能力(截图感知)。用它作为 0-1 适配的完整样例再合适不过——麻雀虽小,五脏俱全:TurboModule、Package 注册、autolinking、C++ 胶水层、构建链路、真机验证,一个都不少。

本文完整记录这次适配的决策、代码、踩坑与修复,希望能给准备适配 RN 三方库的读者一条可复制的路径。

1. 背景:RNOH 生态与三方库适配路径

1.1 RNOH 是什么

RNOH(React Native OpenHarmony)是 OpenHarmony 社区对 React Native 的官方移植,架构与 iOS / Android 对齐:

  • 版本配套:RNOH 0.84 对应 React Native 0.84;npm 包为 @react-native-oh/react-native-harmony,鸿蒙侧 ArkTS 依赖为 @rnoh/react-native-openharmony

  • 架构:仅支持 New Architecture(TurboModule / Fabric),这一点在选库时就要注意——只支持旧架构的三方库无法直接适配;

  • 工具链:DevEco Studio + hvigor 构建 + ohpm 包管理,autolinking 由 @react-native-oh/react-native-harmony-clilink-harmony 命令(或 hvigor 插件自动触发)完成。

1.2 三方库适配的本质

一个 RN 三方库在鸿蒙上"跑起来",本质要做三件事:

  1. JS 侧零改动:库的 JS 代码通过 TurboModuleRegistry.getEnforcing('XXX') 获取原生模块,只要鸿蒙侧提供同名的 TurboModule 即可;

  2. 原生侧能力对齐:把 iOS / Android 的原生实现翻译成鸿蒙 ArkTS 能力(可能由于系统 API 差异需要调整语义,这是适配中最需要设计的部分);

  3. 构建接入:让 RNOH 工程能自动发现并链接这个库(autolinking 配置 + 可构建的 HAR 模块)。

2. 适配前的准备:先摸清"库"与"平台"两张牌

2.1 摸清库的对外接口

动工前先做接口分析(用 rnoh-lib-interface-analyzer 技能对库做了一次完整的接口规格梳理),结论是:

对外能力类型说明
useScreenshotAware(callback)React Hook挂载订阅、卸载自动移除
ScreenshotAware.addListener(cb)方法返回 EmitterSubscription
ScreenshotAware.removeAllListeners()方法清空监听
ScreenshotAwareEvent常量事件名(原生侧 emitDeviceEvent 上报)

原生侧只有一个 TurboModule:ScreenshotAware(name 固定),Spec 中 addListener / removeListeners 为满足契约的空实现——事件传递由各平台事件机制完成,不走 TurboModule 方法调用。这个信息非常关键:鸿蒙侧不需要实现任何方法调用,只需要"注册系统监听 + 触发同名事件"。

2.2 摸清鸿蒙平台能力(本次适配最核心的决策)

截屏感知在鸿蒙上有两条路,调研后结论非常明确:

方案能力三方应用可用性结论
@ohos.screenshot截图系统接口❌ 需要 ohos.permission.CAPTURE_SCREEN仅系统应用可用不可用
@ohos.displaycaptureStatusChange屏幕内容被获取(截屏 / 投屏 / 录屏)的状态变化监听✅ API 12+,三方应用可用采用

这是一个典型的"系统能力不可直接迁移,用等效事件替代"的适配决策:

  • iOSUIApplicationUserDidTakeScreenshotNotification(截图专属通知);

  • Android 14+registerScreenCaptureCallback + DETECT_SCREEN_CAPTURE 权限(截图专属 API);

  • 鸿蒙display.on('captureStatusChange'),回调 true 表示屏幕内容开始被获取——截屏、投屏、录屏都会触发,无法单独区分。

所以鸿蒙版的事件语义与 iOS 接近(都是"屏幕被截取"类通知),但与 Android 14 的"仅截图"存在差异。这个差异必须在 README 中明示,让接入方按业务语义设计逻辑。适配不是逐行翻译,而是能力对齐 + 语义说明。

2.3 环境清单

组件版本
React Native / RNOH0.84.x(0.84 版本线)
DevEco Studio6.1.0 及以上(API 17 Compile SDK)
真机HarmonyOS 设备(API 26,ALN-AL00)
构建hvigor + ohpm(华为镜像源)

3. 搭建 RNOH 0.84 测试工程

适配的第一步是在本地复现"库 + 宿主工程"的运行环境。RNOH 官方仓库(ohos_react_native 0.84 分支)自带 RNOH084Demo 示例工程,直接以它为宿主工程改造,避免从零配置 RNOH 构建链。

3.1 工程结构

RNOH084Demo/
├── App.tsx                      # RN 侧入口(appKey: RNOH084Demo)
├── index.js                     # JS bundle 入口
├── node_modules/
│   └── react-native-screenshot-aware/   # 本次要适配的三方库(链接进 node_modules)
└── harmony/                     # 鸿蒙宿主工程
    ├── oh-package.json5         # 项目级依赖(file: 指向 .har)
    ├── entry/
    │   ├── oh-package.json5     # entry 级依赖
    │   └── src/main/ets/
    │       ├── PackageProvider.ets        # RN 包注册入口(由 autolinking 驱动)
    │       ├── RNOHPackagesFactory.ets    # link-harmony 自动生成
    │       ├── entryability/EntryAbility.ets
    │       └── pages/Index.ets            # RNApp 挂载页

3.2 接入库的两种形态

适配过程中,库的鸿蒙侧代码以两种形态在宿主工程中出现:

  1. 源码目录形态harmony/screenshot_aware/(ArkTS 模块源码,可直接被 hvigor 构建);

  2. 产物形态harmony/screenshot_aware.harassembleHar 打包出的 HAR,ohpm 依赖的载体)。

💡 适配开发阶段用源码目录形态迭代最快;发布阶段把 screenshot_aware.har 放进仓库,宿主工程通过 ohpm install 或 autolinking 引用。

4. 编写鸿蒙原生模块

4.1 TurboModule 实现(ScreenshotAwareTurboModule.ets)

核心逻辑:监听 display.captureStatusChange,回调为 true 时通过 RNOH 的 emitDeviceEvent 上报 ScreenshotAwareEvent,并在销毁时注销监听。

import { AnyThreadTurboModule, AnyThreadTurboModuleContext } from '@rnoh/react-native-openharmony';
import display from '@ohos.display';
​
export class ScreenshotAwareTurboModule extends AnyThreadTurboModule {
  public static readonly NAME = 'ScreenshotAware';
  public static readonly EVENT_NAME = 'ScreenshotAwareEvent';
​
  private captureStatusCallback: (captureStatus: boolean) => void;
​
  constructor(ctx: AnyThreadTurboModuleContext) {
    super(ctx);
    this.captureStatusCallback = (captureStatus: boolean): void => {
      if (captureStatus) {
        this.ctx.rnInstance.emitDeviceEvent(ScreenshotAwareTurboModule.EVENT_NAME, null);
      }
    };
    this.registerCaptureStatusChange();
  }
​
  private registerCaptureStatusChange(): void {
    try {
      display.on('captureStatusChange', this.captureStatusCallback);
    } catch (err) {
      console.error(`[ScreenshotAware] register captureStatusChange failed: ${JSON.stringify(err)}`);
    }
  }
​
  private unregisterCaptureStatusChange(): void {
    try {
      display.off('captureStatusChange', this.captureStatusCallback);
    } catch (err) {
      console.error(`[ScreenshotAware] unregister captureStatusChange failed: ${JSON.stringify(err)}`);
    }
  }
​
  // Spec 要求的契约方法,事件传递由 RNOH 事件机制完成,实现为空(与 Android 一致)
  addListener(eventName: string): void {}
​
  removeListeners(count: number): void {}
​
  __onDestroy__(): void {
    super.__onDestroy__();
    this.unregisterCaptureStatusChange();
  }
}

要点:

  • 继承 AnyThreadTurboModule:RNOH 的三方库 TurboModule 通常继承它(运行在任意线程),保持与宿主 JS 线程解耦;

  • emitDeviceEvent:RNOH 提供的跨端事件上报 API,事件名必须与 JS 侧常量 ScreenshotAwareEvent 一致;

  • __onDestroy__ 注销监听:避免模块销毁后监听泄漏。

4.2 Package 注册(ScreenshotAwarePackage.ets)

RNOH 通过 RNOHPackage 组织 TurboModule 工厂,把模块名映射到工厂函数:

import { RNOHPackage, AnyThreadTurboModule, AnyThreadTurboModuleContext } from '@rnoh/react-native-openharmony';
import { ScreenshotAwareTurboModule } from './ScreenshotAwareTurboModule';
​
export class ScreenshotAwarePackage extends RNOHPackage {
  override getAnyThreadTurboModuleFactoryByNameMap():
    Map<string, (ctx: AnyThreadTurboModuleContext) => AnyThreadTurboModule | null> {
    return new Map<string, (ctx: AnyThreadTurboModuleContext) => AnyThreadTurboModule | null>([
      [ScreenshotAwareTurboModule.NAME, (ctx) => new ScreenshotAwareTurboModule(ctx)],
    ]);
  }
}

4.3 模块导出(Index.ets)

autolinking 生成的工厂文件使用 default import 引用 Package 类,因此 Index.ets 必须同时提供命名导出与默认导出(这是后续运行期踩坑的伏笔之一):

export { ScreenshotAwarePackage } from './src/main/ets/ScreenshotAwarePackage';
export { ScreenshotAwarePackage as default } from './src/main/ets/ScreenshotAwarePackage';

4.4 库级配置

  • oh-package.json5:声明对 @rnoh/react-native-openharmony 的依赖;

  • module.json5:鸿蒙侧不需要申请 CAPTURE_SCREEN 等权限(该权限仅系统应用可用,申请了也无法使用);

  • package.json(库根目录)新增 harmony.autolinking 配置,这是 autolinking 发现本库的钥匙:

{
  "harmony": {
    "autolinking": {
      "ohPackageName": "@rnoh/react-native-screenshot-aware",
      "etsPackageClassName": "ScreenshotAwarePackage"
    }
  }
}

5. autolinking 接入与第一次构建

5.1 autolinking 做了什么

在宿主工程执行 ohpm install(或构建时 hvigor 插件自动触发 link-harmony)后,RNOH 会扫描 node_modules 中所有配置了 harmony.autolinking 的包,自动完成:

  1. 在项目 / entry 级 oh-package.json5 中写入 file: 依赖(指向库的 HAR 或源码目录);

  2. 生成 entry/src/main/ets/RNOHPackagesFactory.ets——注册 ScreenshotAwarePackage

  3. 生成 entry/src/main/cpp/autolinking.cmakeRNOHPackagesFactory.h——C++ 侧注册;

  4. 生成 entry/src/main/cpp/RNOHPackagesFactory.h 引用的库侧 C++ Package 头文件。

5.2 第一次构建:一连串连环坑

真正的"0-1"过程几乎注定不是一次成功的。这次适配的构建期踩坑,每一个都值得单独记录。


6. 构建踩坑实录(四连坑)

🕳️ 坑 1:autolinking 扫到两个 .har,包名被加了奇怪后缀

现象:生成的 RNOHPackagesFactory.ets 中 import 的是

import ScreenshotAwarePackage from '@rnoh/react-native-screenshot-aware--screenshot_aware';

包名后面多了一个 --screenshot_aware 后缀,随后 ohpm 报警告:local dependency ... does not match the actual name

根因:autolinking 的 resolveHarPackageNames 逻辑中,当 harFilePaths.length > 1 时会给包名追加 --<har文件名> 后缀来区分同名 HAR。而我的库目录里恰好有两个 .har

node_modules/react-native-screenshot-aware/harmony/screenshot_aware.har        # 根目录产物
node_modules/react-native-screenshot-aware/harmony/screenshot_aware/build/.../screenshot_aware.har  # 模块内构建产物

修复:删除模块内 build/ 构建产物,保证 harmony/ 下只有一个 .har,后缀自然消失。

💡 教训:不要把构建产物放在 autolinking 的扫描路径内。HAR 打包后的 build 目录务必清理(已加入 .gitignore)。

🕳️ 坑 2:纯 ArkTS 库没有 cpp 目录,CMake 直接报错

现象

CMake Error at autolinking.cmake:7 (add_subdirectory):
  add_subdirectory given source
  ".../oh_modules/@rnoh/react-native-screenshot-aware/src/main/cpp"
  which is not an existing directory.

根因:autolinking 生成的 autolinking.cmake 无条件为每个 autolinking 库执行 add_subdirectory(src/main/cpp) 并链接 rnoh__react_native_screenshot_aware 这个 C++ target。我的库是纯 ArkTS 实现,没有 src/main/cpp 目录。

修复:为库补充一个空 C++ 模块,提供合法的 CMake target:

# harmony/screenshot_aware/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_VERBOSE_MAKEFILE on)
​
file(GLOB screenshot_aware_SRC CONFIGURE_DEPENDS *.cpp)
​
add_library(rnoh__react_native_screenshot_aware SHARED ${screenshot_aware_SRC})
target_include_directories(rnoh__react_native_screenshot_aware PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(rnoh__react_native_screenshot_aware PUBLIC rnoh)

再放一个 empty.cpp(或后续真实实现)充当编译单元。

🕳️ 坑 3:C++ 侧缺 Package 类,头文件找不到

现象:CMake 配置通过后,C++ 编译报错:

RNOHPackagesFactory.h:8:10: fatal error: 'ReactNativeScreenshotAwarePackage.h' file not found

根因:autolinking 生成的 RNOHPackagesFactory.h 会 include 库侧的 Package 头文件并 make_shared<rnoh::ReactNativeScreenshotAwarePackage>(ctx),要求库在 rnoh 命名空间提供同名 C++ 类。

修复:提供一个继承 rnoh::Package 的最小 C++ 类:

// ReactNativeScreenshotAwarePackage.h
#pragma once
#include "RNOH/Package.h"
​
namespace rnoh {
class ReactNativeScreenshotAwarePackage : public rnoh::Package {
  using Super = rnoh::Package;
 public:
  using Super::Super;
};
} // namespace rnoh
// ReactNativeScreenshotAwarePackage.cpp
#include "ReactNativeScreenshotAwarePackage.h"

🕳️ 坑 4(最深):运行期 "Couldn't find Turbo Module on the ArkTs side"

现象:构建全部通过、HAP 装到真机、应用启动后 JS 侧报错:

[Error: Exception in HostObject::get for prop 'ScreenshotAware':
  Couldn't find Turbo Module on the ArkTs side, name: 'ScreenshotAware'
Suggestions: Have you linked a package that provides this turbo module on the CPP side?
[TypeError: Cannot read property 'useScreenshotAware' of undefined]

而设备日志却显示 ArkTS 侧明明创建成功了:

TurboModuleFactory.cpp:54> Creating Turbo Module: ScreenshotAware
setupRNOHWorker::TurboModuleProvider  TM created: ScreenshotAware

根因(这是全文最值得记录的一坑):RNOH 的 C++ 侧 TurboModuleFactory::create 流程是:

  1. 先在 ArkTS 侧 provider 中查 hasModule(name) / getModule(name)——成功拿到了 ArkTS 实例引用(所以有 "TM created" 日志);

  2. 再遍历 C++ 侧 TurboModuleFactoryDelegate 列表,调用 delegate->createTurboModule(ctx, name) 把 ArkTS 实例包装成 C++ TurboModule

  3. 如果所有 delegate 都不认这个模块名(返回 nullptr),且 ArkTS 实例引用已存在 → 抛出 FatalRNOHError,suggestion 指向 "CPP side"

官方 codegen 生成的 BaseXxxPackage 会自动实现 createTurboModuleFactoryDelegate(),为每个 ArkTS 模块生成 ArkTSTurboModule 包装。但手动适配时,这个 delegate 不会凭空出现——我的 C++ Package 类只继承了 rnoh::Package,没有重写该方法,于是 C++ 侧没人能把 ArkTS 实例包装起来。

修复:重写 createTurboModuleFactoryDelegate,提供将 ArkTS TurboModule 包装为 ArkTSTurboModule 的 delegate:

// ReactNativeScreenshotAwarePackage.h
#pragma once
​
#include "RNOH/Package.h"
#include "RNOH/ArkTSTurboModule.h"
#include "RNOH/TurboModuleFactory.h"
​
namespace rnoh {
​
class ScreenshotAwareTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate {
 public:
  SharedTurboModule createTurboModule(Context ctx, const std::string& name) const override {
    if (name == "ScreenshotAware") {
      return std::make_shared<ArkTSTurboModule>(ctx, name);
    }
    return nullptr;
  }
};
​
class ReactNativeScreenshotAwarePackage : public rnoh::Package {
  using Super = rnoh::Package;
​
 public:
  using Super::Super;
​
  std::unique_ptr<TurboModuleFactoryDelegate> createTurboModuleFactoryDelegate()
      override {
    return std::make_unique<ScreenshotAwareTurboModuleFactoryDelegate>();
  }
};
} // namespace rnoh

💡 适配纯 ArkTS TurboModule 的黄金法则:ArkTS 侧实现业务逻辑,C++ 侧只需一个把 ArkTS 实例桥接成 ArkTSTurboModule 的 delegate。ArkTSTurboModule(ctx, name) 这个包装类会通过 ctx.arkTSTurboModuleInstanceRef 把调用转发到 ArkTS 实例——这就是"ArkTS 原生实现 + C++ 胶水"的最终形态。


7. 真机运行验证

7.1 部署与启动

# 1. 构建 HAP
hvigorw --mode module -p product=default assembleHap --no-daemon
# 2. 安装到真机
hdc install entry/build/default/outputs/default/entry-default-signed.hap
# 3. 启动
hdc shell aa start -a EntryAbility -b com.rnoh084.demo

7.2 验证要点

  1. 进程存活、无崩溃hdc shell ps -A | grep rnoh084

  2. RN 实例正常:日志出现 RNInstanceCAPI::startSurface / measureSurface

  3. TurboModule 注册成功:日志出现

Creating Turbo Module: ScreenshotAware
setupRNOHWorker::TurboModuleProvider  TM created: ScreenshotAware

不再有 Couldn't find Turbo Module / useScreenshotAware of undefined 错误。

最终在真机(ALN-AL00,HarmonyOS API 26)上运行效果如下:

image-20260906115948166

7.3 一个容易被忽略的坑:真机连不上 Metro

RNOH 的 debug 模式需要 Metro 提供 JS bundle。真机通过 USB 连接时,设备上的 localhost:8081 指向的是设备自身,必须做端口反向转发:

hdc reverse tcp:8081 tcp:8081

否则应用会卡在 "Cannot connect to Metro"(日志中 URL: localhost:8081 且连接失败),看起来像适配失败,实际只是网络问题。


8. 收尾:文档、版本配套与发布

适配不是"能跑就行",要让它可交付、可持续:

  1. 能力差异文档化:新增 README.OpenHarmony.md / README.OpenHarmony_CN.md,明示鸿蒙版的能力差异(captureStatusChange 会覆盖投屏/录屏、无法区分截图与录屏、API 12+ 要求等);

  2. 版本配套表格:React Native 0.84 ↔ RNOH 0.84 ↔ @rnoh/react-native-openharmony 0.84 ↔ Compile SDK API 17,写进 README 防止接入方配错;

  3. 发布形态harmony/screenshot_aware.har 随仓库发布,宿主工程通过 package.jsonharmony.autolinking 自动发现;

  4. 最终提交:一次 commit 包含 ArkTS 模块 + C++ 胶水 + autolinking 配置 + 文档(本仓库已提交并推送)。

9. 经验总结

阶段关键动作一句话经验
选库确认库是 New Architecture(TurboModule)旧架构库适配成本高得多
接口分析梳理对外 API / 事件名 / Spec 契约事件驱动型库的适配重点是"同名事件",不是方法实现
能力调研对比 iOS / Android / 鸿蒙系统 API系统能力不可用时找等效事件替代,并文档化语义差异
工程搭建复用 RNOH 官方 Demo 做宿主0-1 阶段别从零配构建链
原生实现AnyThreadTurboModule + emitDeviceEventArkTS 侧只需业务逻辑 + 事件上报
autolinking配置 harmony.autolinking + 单一 .har构建产物别放扫描路径内
C++ 胶水Package 类 + createTurboModuleFactoryDelegate纯 ArkTS 库也要补 C++ 桥接,这是最容易漏的
运行验证真机 + hdc reverse + 日志关键字区分"适配 bug"与"环境问题"(Metro 连接)
交付双语 README + 版本配套表 + .har让接入方"照着配就能跑"

最终形态一句话:一个纯 ArkTS 的 RNOH 三方库 = ScreenshotAwareTurboModule.ets(业务)+ ScreenshotAwarePackage.ets(注册)+ 一个 C++ Package 类与 ArkTSTurboModule 桥接 delegate(胶水)+ harmony.autolinking 配置(发现机制)。

希望这篇 0-1 记录能帮到正在把 RN 三方库搬上鸿蒙的你。下一个库,会更快。


10. 参考资料与 RNOH 相关组织

按代码托管平台排序:AtomGit 优先,随后为 GitHub 等平台。

10.1 组织与官方仓库(AtomGit)

组织 / 仓库地址说明
CPF-RN 组织CPF-RN - 开源代码托管,代码协作 - AtomGitRNOH 中文化与技能集组织
ohos_react_nativeohos_react_native:基于 React Native 生态的 OpenHarmony 适配项目 - AtomGitRNOH 0.84 版本线官方仓库(含 RNOH084Demo 示例工程)
skills 技能集skills:基于 React Native 与 HarmonyOS 的开发自动化工具集合项目 - AtomGitRNOH 适配相关技能(autolinking、接口分析、文档生成等)
oh-react-native 组织oh-react-native - 开源代码托管,代码协作 - AtomGit鸿蒙适配库组织
react-native-screenshot-aware(鸿蒙适配版)react-native-screenshot-aware:基于 React Native 的跨平台截图感知项目 - AtomGit本文适配目标库的仓库

10.2 官方组件与文档

组件 / 文档地址说明
@react-native-oh/react-native-harmony(npm)https://www.npmjs.com/package/@react-native-oh/react-native-harmonyRNOH 前端包(0.84.x)
@react-native-oh/react-native-harmony-cli(npm)https://www.npmjs.com/package/@react-native-oh/react-native-harmony-clilink-harmony / autolinking 命令行工具
@rnoh/react-native-openharmony(ohpm)https://ohpm.openharmony.cn鸿蒙侧 ArkTS 运行时依赖(0.84.x)
RNOH 官方仓库(GitHub)https://github.com/react-native-oh/react-native-harmonyRNOH 上游源码与文档
React Native OpenHarmony 官网https://react-native-oh.github.io/react-native-harmony官方文档站点

10.3 上游库

项目地址说明
react-native-screenshot-aware(上游)https://github.com/a29740/react-native-screenshot-aware本文适配前的原始库(iOS / Android)
Logo

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

更多推荐