# Async Clipboard API

> navigator.clipboard 的 read()、readText()、write()、writeText()，ClipboardItem 的成员，规范定义的每个 DOMException，以及带回退分支的复制与粘贴示例。

Async Clipboard API 即 `navigator.clipboard`，用 Promise 读写系统剪贴板，取代 `document.execCommand('copy')` 与 `('paste')`。纯文本走 `writeText()` 和 `readText()`；图片、HTML 与多格式载荷封装成 `ClipboardItem`，经 `write()` 和 `read()` 传输。

Chrome 66 提供了 `writeText()` 与 `readText()`，Chrome 76 补上 `read()`、`write()` 与 `ClipboardItem`；Edge 79 与 Samsung Internet 12.0 跟随 Chromium。Firefox 63 加入 `writeText()`，125 加入 `readText()`，127 加入 `read()`、`write()` 与 `ClipboardItem`。macOS 上的 Safari 13.1 与 iOS 上的 Safari 13.4 一次性提供四个方法（BCD `api.Clipboard`）。该 API 只存在于安全上下文的 `Window` 中：`localhost` 以外的 `http://` 源上 `navigator.clipboard` 为 `undefined`，Chromium 在 worker 中的读取以 `NotAllowedError` 拒绝。

## 语法

```js
navigator.clipboard.readText()
navigator.clipboard.read()
navigator.clipboard.read(formats)
navigator.clipboard.writeText(data)
navigator.clipboard.write(data)

new ClipboardItem(items)
new ClipboardItem(items, options)
clipboardItem.getType(type)
ClipboardItem.supports(type)
```

`readText()` 返回 `Promise<DOMString>`；`read()` 返回 `Promise<sequence<ClipboardItem>>`，返回的条目只带类型名，字节在调用 `getType()` 时才读取；`writeText()` 与 `write()` 返回 `Promise<undefined>`。读取在 Firefox 125+ 与 Safari 13.1+ 需要瞬时用户激活，在 Chrome 中需要激活或已授予的 `clipboard-read` 权限。写入在 Firefox 63+ 与 Safari 13.1+ 需要瞬时激活，Chrome 自 107 起需要激活或 `clipboard-write` 权限（107 之前只看权限）。`ClipboardItem.supports()` 是静态同步方法，存在于 Chrome 121、Firefox 127 与 Safari 18.4。

## 参数

三个方法参数加上 `ClipboardItem` 构造函数的两个参数，就是规范定义的全部输入。

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `data`（`writeText`） | `DOMString` | 是 | 以 `text/plain;charset=utf-8` blob 写入。Windows 上浏览器会先把 `\n` 换成 `\r\n`。 |
| `data`（`write`） | `sequence<ClipboardItem>` | 是 | Chrome 76+ 与 Firefox 127+ 只接受恰好一个条目；两个及以上的数组以 `NotAllowedError` 拒绝（BCD `api.Clipboard.write`）。 |
| `formats`（`read`） | `ClipboardUnsanitizedFormats` | 否 | 只有一个成员 `unsanitized: sequence<DOMString>`，请求原始而非净化后的字节。Chrome 122 只对 `text/html` 生效；其他类型会拒绝。 |
| `items`（构造函数） | `record<DOMString, ClipboardItemData>` | 是 | MIME 类型到 `Promise<Blob or DOMString>` 的映射（直接传 `Blob` 或字符串会被包装）。以 `"web "` 为前缀的键是自定义格式（Chrome 104）。 |
| `options.presentationStyle` | `"unspecified"`、`"inline"` 或 `"attachment"` | 否 | 默认 `"unspecified"`，给粘贴目标的提示。Chrome 未实现；Firefox 127 与 Safari 13.1 在读取到的条目上暴露它。 |
| `type`（`getType`、`supports`） | `DOMString` | 是 | 一个 MIME 类型，可带 `"web "` 前缀。 |

`ClipboardItem` 暴露 `types`（其 MIME 类型字符串的冻结数组）与 `presentationStyle`。每个浏览器都必须接受的强制类型是 `text/plain`、`text/html` 与 `image/png`；`image/svg+xml` 属于可选类型，Chrome 124 提供。

## 异常

除 `ClipboardItem` 构造函数和 `getType()` 的同步部分外，所有方法都返回被拒绝的 Promise 而不是直接抛出。

| 异常 | 条件 |
|---|---|
| `NotAllowedError` | `read()` 或 `readText()` 的「check clipboard read permission」失败（没有瞬时激活也没有已授予的 `clipboard-read`）；`write()` 或 `writeText()` 的写入检查失败；blob 类型不在强制与可选类型之内；净化未能完成；某个表示的 Promise 被拒绝；`read(formats)` 指定的 `unsanitized` 类型不在可选未净化列表内；Chrome 与 Firefox 中传入多个 `ClipboardItem`；Chromium 中文档没有焦点；`clipboard-read` 或 `clipboard-write` 被 Permissions Policy 拒绝。 |
| `NotFoundError` | `readText()` 时剪贴板没有 `text/plain` 表示，或该表示解码失败。 |
| `TypeError` | `new ClipboardItem()` 的 `items` 为空、某个键无法解析为 MIME 类型、或同一类型出现两次；`getType()` 的 `type` 无法解析。 |
| `InvalidStateError` | 对 `read()` 得到的条目调用 `getType()` 时系统剪贴板已变化，或该条目对应的系统条目已不存在。 |
| `DataError` | 仅 Chromium：`ClipboardItemData` 的 Promise 无法读取或解码，或处理 paste 事件期间剪贴板内容发生变化。 |

Firefox 多加了一道规范未命名的浏览器层关卡：`read()` 与 `readText()` 会弹出用户必须接受的粘贴提示，只有剪贴板内容同源时才跳过（BCD `api.Clipboard.read`）。拒绝该提示会以 `NotAllowedError` 拒绝。

:::observed
在 Chrome 的 DevTools Console 中执行 `await navigator.clipboard.writeText('x')`，焦点在 Console 而非页面时，以 `NotAllowedError: Document is not focused.` 拒绝；同一调用放在页面的 `click` 处理函数里则成功。权限请求被关闭的 `readText()` 调用以 `NotAllowedError: Read permission denied.` 拒绝，传入两个条目的 `write()` 以 `NotAllowedError: Support for multiple ClipboardItems is not implemented.` 拒绝。三条字符串都在 Chromium 的 [`clipboard_promise.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/clipboard/clipboard_promise.cc)（chromium.googlesource.com）中。
:::

## 示例

每个示例都只检测它真正需要的方法，并给出方法缺失时走的分支。写入示例要在 `click` 处理函数中运行，这样瞬时激活仍然有效。

### 复制文本，缺少支持时回退到隐藏 textarea

`navigator.clipboard.writeText()` 覆盖 Chrome 66+、Firefox 63+ 与 Safari 13.1+；更老的版本以及任何 `http://` 页面需要已废弃的 `document.execCommand('copy')`，它在这些引擎中仍可用于纯文本。

```js
async function copyText(text) {
  if (navigator.clipboard?.writeText) {
    await navigator.clipboard.writeText(text);
    return 'async';
  }
  const area = document.createElement('textarea');
  area.value = text;
  area.setAttribute('readonly', '');
  area.style.position = 'fixed';
  area.style.opacity = '0';
  document.body.append(area);
  area.select();
  const ok = document.execCommand('copy');
  area.remove();
  return ok ? 'legacy' : 'failed';
}

document.querySelector('#copy').addEventListener('click', async () => {
  const result = await copyText(location.href);
  console.log(`copied via ${result}`);
});
```

函数返回实际走的路径，界面只在其中一条成功时才显示「已复制」；浏览器拒绝时 `execCommand` 返回 `false` 而不是抛出。

### 在一个条目里同时写入 PNG 与 HTML 回退

一个 `ClipboardItem` 可以携带多种表示；粘贴目标挑选它能理解的最丰富的那种。有 `ClipboardItem.supports()` 时（Chrome 121、Firefox 127、Safari 18.4）先检查，没有时按「只有强制类型」处理。

```js
async function copyChart(canvas, captionHtml) {
  if (!navigator.clipboard?.write || !('ClipboardItem' in window)) {
    return copyText(captionHtml.replace(/<[^>]+>/g, ''));
  }
  const png = new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
  const items = { 'image/png': png };
  const htmlOk = typeof ClipboardItem.supports !== 'function' || ClipboardItem.supports('text/html');
  if (htmlOk) {
    items['text/html'] = new Blob([captionHtml], { type: 'text/html' });
  }
  try {
    await navigator.clipboard.write([new ClipboardItem(items)]);
  } catch (err) {
    if (err.name !== 'NotAllowedError') throw err;
    return copyText(captionHtml.replace(/<[^>]+>/g, ''));
  }
}
```

直接传入 `toBlob` 的 Promise 而不是先 `await`，浏览器就能在同一手势内启动写入、稍后再填入字节；Safari 13.1 是依赖这一顺序的引擎（web.dev，Unblocking clipboard access）。

### 不依赖 Chromium 专有权限查询读取剪贴板图片

只有 Chromium 把 `clipboard-read` 当作 Permissions API 名称；`navigator.permissions.query({ name: 'clipboard-read' })` 在 Firefox 与 Safari 中抛出 `TypeError`。跳过查询，在手势内直接调用 `read()`，按拒绝原因分支。

```js
document.querySelector('#paste').addEventListener('click', async () => {
  if (!navigator.clipboard?.read) {
    document.querySelector('#paste-area').focus(); // 让用户按 Ctrl+V / Cmd+V
    return;
  }
  try {
    const items = await navigator.clipboard.read();
    const item = items.find((i) => i.types.includes('image/png'));
    if (!item) {
      console.log('剪贴板上没有 PNG，可用类型：', items.flatMap((i) => i.types));
      return;
    }
    const blob = await item.getType('image/png');
    document.querySelector('#preview').src = URL.createObjectURL(blob);
  } catch (err) {
    if (err.name === 'NotAllowedError') {
      document.querySelector('#paste-area').focus();
      return;
    }
    throw err;
  }
});
```

聚焦一个 contenteditable 或 textarea 并监听 `paste` 事件是可移植的回退：`paste` 事件的 `clipboardData.files` 在每个引擎里都能交出同一张 PNG，不需要任何权限。

## 另请参阅

- [Web Share API](/zh/reference/capabilities/web-share/)，剪贴板写入是它的常用回退
- [File System Access API](/zh/reference/capabilities/file-system-access/)，处理大到不适合经剪贴板中转的文件
- [Clipboard API and events: write() method](https://www.w3.org/TR/clipboard-apis/#dom-clipboard-write)（w3.org）
- [Unblocking clipboard access](https://web.dev/articles/async-clipboard)（web.dev）
- [Async Clipboard API](https://webkit.org/blog/10855/async-clipboard-api/)（webkit.org）
- [Chrome Platform Status: Asynchronous Clipboard API](https://chromestatus.com/feature/5861289330999296)（chromestatus.com）