对应Demo: ClipboardDemo | 难度: 初级 | 关键词: pasteboard, 剪贴板读写, MIMETYPE_TEXT_PLAIN, 异步getData, READ_PASTEBOARD权限

剪贴板看起来是最简单的API——复制、粘贴,两行代码搞定。但在HarmonyOS NEXT上我踩过三个坑:第一,getData是异步的,忘了await结果拿到的是Promise对象不是数据;第二,剪贴板可能是空的或包含非文本内容,不判空直接用就崩溃;第三,API 12开始读取剪贴板需要权限,不声明权限直接闪退。ClipboardDemo把这些坑都覆盖了,这篇系统梳理一下pasteboard API的正确使用方式。


请添加图片描述

基础用法:复制和粘贴文本

ClipboardDemo的核心逻辑在doCopy和doPaste两个方法里:

import { pasteboard } from '@kit.BasicServicesKit';

@Entry
@Component
struct ClipboardDemo {
  @State inputText: string = '';
  @State pasteResult: string = '';
  private systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard();

  private async doCopy(): Promise<void> {
    if (this.inputText.length === 0) {
      this.addLog('复制失败:输入内容为空');
      return;
    }
    try {
      const pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.inputText);
      this.systemPasteboard.setData(pasteData);
      this.addLog('复制成功:' + this.inputText);
    } catch (err) {
      this.addLog('复制失败:' + String(err));
    }
  }

  private async doPaste(): Promise<void> {
    try {
      const data = await this.systemPasteboard.getData();
      const text = data.getPrimaryText();
      if (text && text.length > 0) {
        this.pasteResult = text;
        this.addLog('粘贴成功:' + text);
      } else {
        this.pasteResult = '(剪贴板为空或非文本内容)';
        this.addLog('粘贴结果:剪贴板为空或非文本内容');
      }
    } catch (err) {
      this.addLog('粘贴失败:' + String(err));
    }
  }
}

复制流程分三步:

  1. pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.inputText) — 创建一个纯文本类型的剪贴板数据对象。MIMETYPE_TEXT_PLAIN是最常用的MIME类型,表示纯文本。

  2. this.systemPasteboard.setData(pasteData) — 把数据写入系统剪贴板。setData是同步方法(不需要await),写入成功后其他App就能读到。

  3. 输入为空时提前return,避免把空字符串写入剪贴板覆盖用户之前复制的内容。这是一个UX细节——用户可能刚复制了重要内容,你的App不应该因为空操作把剪贴板清空。

粘贴流程:

  1. await this.systemPasteboard.getData() — 异步读取剪贴板数据。必须await,因为 getData返回的是Promise。如果你不await直接用返回值,拿到的是一个Promise对象而不是PasteData。

  2. data.getPrimaryText() — 从PasteData中提取主文本内容。如果剪贴板里是图片或URI,getPrimaryText返回null或空字符串。

  3. 判空处理 — text可能为null(非文本内容)或空字符串(用户复制了空白内容),两种情况都要处理。


import的正确写法:named import不是default import

import { pasteboard } from '@kit.BasicServicesKit';

这个import写法是named import,pasteboard是@kit.BasicServicesKit模块导出的一个命名空间。不能写成:

import pasteboard from '@kit.BasicServicesKit';

这是default import语法,而@kit.BasicServicesKit没有默认导出。写错了编译时可能不报错(取决于ArkTS版本),运行时pasteboard是undefined,调方法就崩溃。

ArkTS的模块系统和标准TypeScript略有差异,@kit.*系列的模块都是命名空间导出,统一用花括号import。这个规则适用于所有@kit.*模块:@kit.ArkUI、@kit.BasicServicesKit、@kit.MediaKit等。


getData必须await:异步陷阱详解

doPaste方法声明了async,getData前加了await。这两个缺一不可。来看如果不await会怎样:

// 错误写法
private doPasteWrong(): void {
  const data = this.systemPasteboard.getData();
  const text = data.getPrimaryText(); // 编译报错:data是Promise,没有getPrimaryText方法
}

getData返回的类型是Promise<PasteData>,不是PasteData。你必须await把Promise解开,才能拿到PasteData对象,再调getPrimaryText。

但还有一种更隐蔽的错误写法:

// 也不对
private doPasteAlsoWrong(): void {
  this.systemPasteboard.getData().then((data: PasteData) => {
    const text = data.getPrimaryText();
    this.pasteResult = text; // 可能正常工作
  });
  // 这里的代码在getData完成之前就执行了
  this.addLog('粘贴完成'); // 日志时序错误
}

.then()写法语法上没问题,但容易搞乱时序——.then()回调外的代码在getData完成前就执行了,你的日志、状态更新可能和实际数据操作对不上。async/await的好处是让异步代码的执行顺序和书写顺序一致,不容易出错。

ArkTS对异步的支持和TypeScript基本一致,async函数返回Promise,await解Promise,异常用try/catch捕获。但有一个限制:build()函数不能是async的,所以你不能在build里直接await。所有异步操作必须在普通方法里执行,结果通过@State变量回传到UI。


getPrimaryText返回null的场景

getPrimaryText()不是万能的。它只能提取MIMETYPE_TEXT_PLAIN类型的文本内容。如果用户在相册里复制了一张图片,剪贴板里存的是MIMETYPE_PIXELMAP类型的数据,getPrimaryText返回null。

Demo里的判空逻辑:

const text = data.getPrimaryText();
if (text && text.length > 0) {
  this.pasteResult = text;
  this.addLog('粘贴成功:' + text);
} else {
  this.pasteResult = '(剪贴板为空或非文本内容)';
  this.addLog('粘贴结果:剪贴板为空或非文本内容');
}

两重判断:text && text.length > 0。text为null时短路不执行length;text为空字符串时length为0走else。两个都是"没有可用的文本内容",但含义不同——null是非文本类型,空字符串是复制了空白内容。如果你需要区分这两种情况,可以:

if (text === null) {
  this.addLog('剪贴板内容不是文本');
} else if (text.length === 0) {
  this.addLog('剪贴板文本为空');
} else {
  this.pasteResult = text;
}

实际业务中,一般不区分,统一提示"无可用文本内容"就行。


API 12+的权限变更:READ_PASTEBOARD

这是一个重大的breaking change。ClipboardDemo的对比指南里提到:

剪贴板访问当前无需权限,但未来版本可能要求 ohos.permission.READ_PASTEBOARD,建议提前关注

实际上API 12开始,读取系统剪贴板(getData)已经需要ohos.permission.READ_PASTEBOARD权限了。写入(setData)不需要权限——这和Android的逻辑类似,写剪贴板是低风险的(任何App都能写),读剪贴板是高风险的(可能读到敏感信息如密码、验证码)。

声明权限:

// module.json5
"requestPermissions": [
  {
    "name": "ohos.permission.READ_PASTEBOARD",
    "reason": "$string:read_pasteboard_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  }
]

不声明权限直接调getData,API 12+会抛异常(SecurityError),你的try/catch能捕获到,但粘贴功能就废了。

这个权限是用户授权型的——系统会弹窗询问用户是否允许你的App读取剪贴板。用户拒绝的话getData也会抛异常。所以你的代码必须处理好权限被拒绝的场景,不能假设getData一定成功。

写入剪贴板不需要权限,这一点很人性化。想象一下如果每次"复制"都要用户授权,用户体验会很糟糕——用户点击"复制"按钮时明确知道自己在写入剪贴板,不需要额外确认。但"粘贴"可能是App主动读取剪贴板,用户未必知情,所以需要权限保护。


数据大小限制:超出限制会抛异常

系统剪贴板有容量限制,超出限制setData会抛异常:

  • 单条记录上限约2MB
  • 总数据量上限约10MB
  • 单次最多512条记录

对于纯文本场景,2MB的上限基本不会碰到(2MB纯文本约100万字)。但如果你往剪贴板写图片或大量HTML,就可能超限。

ClipboardDemo的doCopy里try/catch就是为这个准备的——setData可能因为超限抛异常,你不catch就crash。异常信息会包含具体的限制描述,可以在catch里给用户友好提示:

try {
  const pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.inputText);
  this.systemPasteboard.setData(pasteData);
  this.addLog('复制成功:' + this.inputText);
} catch (err) {
  this.addLog('复制失败:' + String(err));
}

实际项目中,如果数据可能很大,写入前先检查大小:

private async doCopyLargeText(text: string): Promise<void> {
  const sizeBytes: number = new TextEncoder().encode(text).length;
  if (sizeBytes > 2 * 1024 * 1024) {
    this.addLog('文本过大,超过2MB限制');
    return;
  }
  try {
    const pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
    this.systemPasteboard.setData(pasteData);
  } catch (err) {
    this.addLog('复制失败:' + String(err));
  }
}

注意计算大小时要用UTF-8编码后的字节数,不是字符串长度。中文一个字符UTF-8编码占3字节,100万字的中文约3MB,已经超限了。


剪贴板内容随时可能被覆盖

剪贴板是全局共享的——用户在任何App里执行"复制",都会覆盖之前的内容。这意味着你在doPaste里读到的数据可能不是你之前doCopy写入的。

ClipboardDemo没有特别处理这个场景,但在实际项目中必须考虑。比如:

  1. 你的App复制了内容A
  2. 用户切到微信复制了一段文字B
  3. 用户切回你的App点"粘贴"
  4. 读到的是B,不是A

如果你的业务逻辑依赖"粘贴的一定是自己刚才复制的",就会出bug。解决方案通常是不依赖剪贴板做数据传递——App内部的数据传递用变量或路由参数,剪贴板只用于跨App的数据交互。

还有一种场景:用户在后台复制了敏感信息(密码、验证码),你的App在前台自动读取剪贴板——这属于隐私侵犯行为,API 12+的权限机制就是为了阻止这种做法。


支持更多MIME类型

pasteboard不只支持纯文本。常用的MIME类型:

类型 常量 用途
纯文本 MIMETYPE_TEXT_PLAIN 普通文字内容
HTML MIMETYPE_TEXT_HTML 富文本内容
URI MIMETYPE_TEXT_URI 文件路径或链接
像素图 MIMETYPE_PIXELMAP 图片数据

写入HTML内容的示例:

const htmlData: pasteboard.PasteData = pasteboard.createData(
  pasteboard.MIMETYPE_TEXT_HTML,
  '<b>加粗文本</b>'
);
this.systemPasteboard.setData(htmlData);

读取时需要根据类型选择对应的get方法:getPrimaryText()取纯文本,getPrimaryHtml()取HTML,getPrimaryUri()取URI。如果类型不匹配(比如数据是HTML你调getPrimaryUri),返回null。

实际项目中,纯文本覆盖了90%的使用场景。HTML和URI用得较少,像素图更少见(跨App传图片通常用URI而不是PixelMap)。


坑点汇总

  1. API 12+读取剪贴板需要ohos.permission.READ_PASTEBOARD权限 — 不声明权限调getData会抛SecurityError。写入不需要权限。

  2. getData是异步操作 — 必须使用await等待结果,不能同步获取。忘记await拿到的不是数据对象而是Promise。

  3. 内容可被其他应用覆盖 — 剪贴板全局共享,任何App的复制操作都会覆盖当前内容,不要假设读到的就是自己之前写入的。

  4. 大数据操作可能失败 — 单条记录2MB、总量10MB、512条记录的限制,超限setData抛异常,建议写入前检查大小。

  5. 空剪贴板处理 — getData可能返回空数据,getPrimaryText可能返回null或空字符串,务必判空后再使用。

  6. import必须是named importimport { pasteboard } from '@kit.BasicServicesKit',不是default import。

  7. 剪贴板不要用于App内部数据传递 — 用变量、路由参数或AppStorage代替,剪贴板只做跨App交互。


写在最后

剪贴板API本身不复杂,复杂的是围绕它的权限、异步、判空、大小限制这些工程问题——每一个没处理好都能让用户看到闪退。

Logo

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

更多推荐