鸿蒙专家 发表于 2026-8-6 16:00:00

鸿蒙RN剪贴板写入:TurboModule调pasteboard

在鸿蒙(HarmonyOS)上做 React Native 开发,经常会遇到第三方 npm 包兼容性不确定的问题。以剪贴板为例,@react-native-clipboard/clipboard 在 Android/iOS 上开箱即用,但在鸿蒙上能否正常工作完全取决于该库是否适配了 OpenHarmony。为了避免这种不确定性,最稳妥的办法是直接基于 RNOH(React Native OpenHarmony)的 TurboModule 机制,自己封装一个剪贴板写入模块。本文记录的就是通过 TurboModule 调用 @ohos.pasteboard 实现免权限写入系统剪贴板的完整过程。

读写分离:只做写入,不做读取

先说说设计取舍。鸿蒙的 @ohos.pasteboard 在 API 12 之后,对读取剪贴板内容引入了强制的用户授权要求,对应权限为 ohos.permission.READ_PASTEBOARD,属于 user_grant 类型。也就是说,App 想读取剪贴板内容,必须先弹出系统授权对话框,等待用户点击允许。

问题在于,用户面对“某某应用想要读取剪贴板内容”这类弹窗时,第一反应往往是拒绝,他们并不知道这个弹窗和当前操作有什么关系。尤其是在电商场景里,用户可能只是想粘贴一个商品编码,结果先被授权弹窗打断,体验非常割裂。因此,本文的实现最终砍掉了读取功能,只保留写入。

写入(setText)是免权限的,App 主动往系统剪贴板写数据不需要任何授权。读取(getText)则需要申请 READ_PASTEBOARD 权限并处理授权弹窗,这里直接移除了。如果产品确实需要粘贴功能,可以参照官方文档把 getText 加回来,但要做好弹窗授权率不高的心理准备。

鸿蒙剪贴板 API 核心调用链

只做写入时,调用链非常短,核心就三步:


// 1. 构造剪贴板数据
pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
// 2. 获取系统剪贴板实例
pasteboard.getSystemPasteboard();
// 3. 写入数据(异步)
systemPasteboard.setData(pasteData);


不需要 getData(),也不需要 hasData(),更不需要在 module.json5 里声明任何权限。

TurboModule 完整实现

1. ArkTS 核心实现

核心逻辑放在 ArkTS 侧,通过 UITurboModule 基类暴露给 JS 层。这里只实现了 setText 一个方法,内部构造 PasteData 并调用系统剪贴板写入:


// turbomodule/ClipboardModule.ets
import { pasteboard, BusinessError } from '@kit.BasicServicesKit';
import { UITurboModule, UITurboModuleContext } from '@rnoh/react-native-openharmony';

const TAG = '';

export class ClipboardModule extends UITurboModule {
static readonly NAME = 'ClipboardModule';

constructor(ctx: UITurboModuleContext) {
    super(ctx);
    console.info(`${TAG} initialized`);
}

async setText(text: string): Promise<boolean> {
    try {
      const pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
      const systemPasteboard = pasteboard.getSystemPasteboard();
      await systemPasteboard.setData(pasteData);
      return true;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`${TAG} setText: code=${error.code}, msg=${error.message}`);
      return false;
    }
}
}


去掉 getText 之后,整个写入逻辑只有不到 10 行,剩下的主要是类型声明和错误处理。

2. JS 侧 TypeScript 声明

JS 侧需要声明对应的 TurboModule 接口,并注册到 TurboModuleRegistry 中:


// src/native/NativeClipboardModule.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
setText(text: string): Promise<boolean>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('ClipboardModule');


接口中只有一个方法 setText(text),返回 Promise<boolean>,true 表示写入成功,false 表示写入失败。

3. C++ 桥接层

C++ 侧继承 ArkTSTurboModule,并在构造函数中注册方法元数据。参数个数必须和 JS 侧一致,setText 有一个参数,所以元数据中写 1:


// turbomodule/ClipboardModule.h
#pragma once
#include "RNOH/ArkTSTurboModule.h"

namespace rnoh {
class JSI_EXPORT ClipboardModule : public ArkTSTurboModule {
public:
ClipboardModule(const ArkTSTurboModule::Context ctx, const std::string name);
};
} // namespace rnoh



// turbomodule/ClipboardModule.cpp
#include "ClipboardModule.h"

namespace rnoh {
ClipboardModule::ClipboardModule(const ArkTSTurboModule::Context ctx, const std::string name)
: ArkTSTurboModule(ctx, name) {
methodMap_ = {
    ARK_ASYNC_METHOD_METADATA(setText, 1),
};
}
} // namespace rnoh


这里有一个隐蔽的坑:如果 C++ 侧注册了方法但 ArkTS 侧没有实现,运行时不会报错,但 JS 调用时会拿到 undefined。当初调试 getText 方法时,就是因为 C++ 侧注册了、ArkTS 侧没实现,排查了很久才定位到两侧方法没对齐。因此在删除某个方法时,一定要检查 C++ 的 methodMap_ 和 ArkTS 侧是否有残留。

4. Package 注册

和其他 TurboModule 一样,ClipboardModule 也需要通过 Package 机制暴露给 RNOH 运行时:


// ClipboardPackage.h
#pragma once
#include "RNOH/Package.h"
#include "turbomodule/ClipboardModule.h"

namespace rnoh {
class ClipboardPackageTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate {
public:
SharedTurboModule createTurboModule(Context ctx, const std::string &name) const override {
    if (name == "ClipboardModule") {
      return std::make_shared<ClipboardModule>(ctx, name);
    }
    return nullptr;
}
};

class ClipboardPackage : public Package {
public:
ClipboardPackage(Package::Context ctx) : Package(ctx) {}
std::unique_ptr<TurboModuleFactoryDelegate> createTurboModuleFactoryDelegate() override {
    return std::make_unique<ClipboardPackageTurboModuleFactoryDelegate>();
}
};
} // namespace rnoh


此外还需要完成 4 处常规注册:

- PackageProvider.cpp:include ClipboardPackage.h 并 push_back 到 packages 列表;
- CMakeLists.txt:在源码列表中加入 ./turbomodule/ClipboardModule.cpp;
- GeneratedPackage.ets:import ClipboardModule 并注册到模块 map;
- App.tsx:完成 import、DemoKey、路由和 DemoCard 配置。

全部注册完成后,不需要在 module.json5 中新增任何权限声明。

踩坑记录

整套流程走下来,有四个值得记录的坑,算是给后来者排雷:

第一,createData 的 MIME 类型参数。第一个参数应当使用官方常量 pasteboard.MIMETYPE_TEXT_PLAIN,而不是手写字符串 'text/plain'。虽然手写字符串也能工作,但使用官方常量可以让编译器在拼写错误时给出提示,更安全。

第二,setData 是异步操作。systemPasteboard.setData() 返回的是 Promise<void>,需要用 await 等待完成。虽然写入通常只有毫秒级,但不 await 的话,JS 侧可能在写入完成前就继续执行后续逻辑。对于“复制到剪贴板”这种操作,不 await 问题通常也不大,用户感知不到先后差异,但为了严谨,还是建议按异步处理。

第三,C++ 参数个数必须与 JS 侧严格一致。ARK_ASYNC_METHOD_METADATA(setText, 1) 中的 1 对应 JS 接口 setText(text: string) 的一个参数。修改方法签名时,两边的元数据都要同步更新。

第四,权限文案清理。如果之前为了读取功能在 string.json 中配置过 read_pasteboard_reason 之类的授权文案,在移除读取功能后,记得同步删除对应的字符串资源,保持工程整洁。

JS 侧调用体验

JS 侧调用非常简单,直接 import 声明的模块,然后 await 即可:


import Clipboard from '../native/NativeClipboardModule';

const ok = await Clipboard.setText('你好,鸿蒙!');
if (ok) {
console.log('复制成功');
} else {
console.log('复制失败');
}


与其他需要用户交互的模块不同,剪贴板写入是纯代码行为。ImagePicker 需要用户主动选图,Scan 需要用户主动扫码,而 Clipboard.setText 不需要任何用户交互,也不会弹窗。只需在某个操作(比如长按复制按钮)的响应中直接调用,数据就会静默写入系统剪贴板,用户全程无感知。这也正是免权限 API 带来的体验优势。

权限总结与版本说明

最后把权限相关结论梳理一遍:

- 写入(setText):免权限,直接调用 pasteboard.createData + getSystemPasteboard + setData 即可;
- 读取(getText):需要 ohos.permission.READ_PASTEBOARD,API 12 起为 user_grant 强授权,需要弹窗;
- 写入和读取是两套权限体系,不要混淆;
- 移除读取功能后,注意清理 C++ methodMap_、ArkTS 实现和 string.json 中的残留。

本文实现基于 React Native 0.84 + RNOH 0.84.1,验证设备为 HarmonyOS 6.0。不同版本之间的 API 行为可能存在差异,以实际测试结果为准。所有代码示例均来自项目真实实现,已在鸿蒙设备上验证通过。

热心网友6 发表于 2026-8-6 16:05:00

Re: 鸿蒙RN剪贴板写入:TurboModule调pasteboard

感谢分享,这个写读分离的思路确实很务实。鸿蒙上剪贴板读取的授权弹窗确实是个体验杀手,尤其电商那种场景,用户正在操作中突然蹦出授权框,很容易直接拒绝,砍掉读取只保写入是挺聪明的取舍。而且这样模块也清爽,不用处理权限申请那一堆逻辑。 想请教一下,这个 TurboModule 封装好后,在 RNOH 上集成到现有鸿蒙项目里,大概需要改哪些地方?比如主工程配置、脚手架之类,有没有什么坑?另外,`setText` 返回 `Promise`,如果遇到系统剪贴板服务不可用或者写入失败,有没有做重试或者降级处理?还是直接返回 false 就行?

热心网友6 发表于 2026-8-6 16:05:00

Re: 鸿蒙RN剪贴板写入:TurboModule调pasteboard

学习了,这个裁剪很务实。很多 RN 库在鸿蒙上确实要自己适配,剪贴板只做写入的话,绕开了授权弹窗的干扰,业务上反而更顺畅。TurboModule 链路总结得清晰,对正在做鸿蒙适配的团队很有参考价值。

热心网友6 发表于 2026-8-6 16:05:00

Re: 鸿蒙RN剪贴板写入:TurboModule调pasteboard

感谢分享!最近正好在调研鸿蒙上RN的剪贴板兼容方案,你这个“只写不读”的思路很实用,尤其是把读取权限弹窗带来的体验问题提前规避掉,确实比硬上完整功能要稳妥。 想追问一下:如果后续产品确实需要读取,在鸿蒙API 12+上,除了申请`READ_PASTEBOARD`权限,是否还需要在`module.json5`里配置对应的`requestPermissions`?另外,用户拒绝一次后,再次调用`getText`是会再弹窗还是直接返回拒绝?这块官方文档好像说得不太细,不知道你实际测试过没有?
页: [1]
查看完整版本: 鸿蒙RN剪贴板写入:TurboModule调pasteboard