首页
/ Electron ClipboardItem 深入解析:W3C 风格剪贴板条目的构造、读写与跨平台实现

Electron ClipboardItem 深入解析:W3C 风格剪贴板条目的构造、读写与跨平台实现

2026-09-05 17:52:47作者:秋泉律Samson

本文围绕 Electron 主进程 API ClipboardItem 展开。ClipboardItem 是 Electron 对齐 W3C ClipboardItem 标准的剪贴板条目类:每个条目承载一组 MIME 类型化载荷(例如一次"复制"同时暴露纯文本和 HTML 两种表示),通过 clipboard.write() 写入系统剪贴板、通过 clipboard.read() 读取回来。读完本文,你将掌握 new ClipboardItem(items) 的完整参数规则(string / Blob / Promise / 书签对象四种载荷形态)、text/uri-list 文件剪贴板的跨平台映射细节、getType() 的两个重载行为,以及该 API 从 JS 门面到 C++ 原生绑定(ScopedClipboardWriter / 惰性读取)的完整调用链,能够直接在自己的 Electron 应用中实现多格式复制粘贴。

一、类概览:ClipboardItem 定位与约束

ClipboardItem仅主进程(Main process)可用的类,建模自 W3C ClipboardItem 类。它与 clipboard 模块(见 clipboard API 文档)配套使用:

  • 构造侧:为 clipboard.write() 构建一个剪贴板条目;
  • 读取侧clipboard.read() 返回的条目同样是 ClipboardItem 实例,可检查其 types 并调用 getType() 取回载荷。

需要牢记的两个约束:

  1. 不可继承:Electron 的内置类无法在用户代码中子类化(参见 FAQ:Class Inheritance 一节);
  2. 读取侧条目不可写回:从源码看,clipboard.read() 返回的条目内部持有来源剪贴板缓冲而非载荷(is_read_side_ 标志),若把它直接传给 clipboard.write() 会抛出 TypeError(见 clipboard-item.ts 的 kToNative 校验)。

构造器 new ClipboardItem(items)

  • items Record<string, string | ClipboardBookmark | Blob | Promise<Blob | string>> — 键为 MIME 类型,值为该类型的载荷。参数结构与 W3C ClipboardItem(items) 构造器的 items 参数一致。每个 MIME 类型都接受 stringBlobstring 按 W3C 规范要求被 UTF-8 编码为载荷字节,Blob 直接提供原始字节。唯一的例外是 electron application/bookmark 自定义格式——它接受 ClipboardBookmark 对象({ title, url })。除书签外,任何值还可以是 Promise,在 clipboard.write() 被调用时才 await 其结果。

一个涵盖全部载荷形态的标准示例:

// 每个 ClipboardItem 描述一个"一个概念剪贴板条目 + 多 MIME 表示"。
// 书签自定义格式接收结构化的 { title, url } 对象而非 Blob。
const { clipboard, ClipboardItem, nativeImage } = require('electron')

const png = nativeImage.createFromPath('/path/to/icon.png').toPNG()

clipboard.write([
  new ClipboardItem({
    'text/plain': 'hello',                      // string → UTF-8 字节
    'text/html': '<b>hello</b>',                // string
    'image/png': new Blob([png], { type: 'image/png' }),  // Blob → 原始字节
    'electron application/bookmark': {          // 书签对象
      title: 'Electron',
      url: 'https://electronjs.org'
    }
  })
])

安全警告(原文档重点):不要直接从不可信对象(例如经 IPC 从渲染进程收到的数据)构造 ClipboardItem。MIME 键本身就是一种能力面:text/uri-list 会在操作系统剪贴板上放置真实的文件引用(文件可被粘贴到另一个应用),而 electron application/osclipboard;format=...web 前缀(如 web application/x.my-format)格式会写入原始平台数据。在构造之前,必须校验并白名单化 MIME 类型与每个载荷的形状。

JS 门面的参数校验

从源码看,用户接触到的 ClipboardItemlib/browser/api/clipboard-item.ts 中 JS 类的一个异步门面,它包裹原生绑定 process._linkedBinding('electron_browser_clipboard_item')

  • 构造时只做"浅校验":items 必须是普通对象(非数组、非 null),且至少包含一个 MIME 键,否则同步抛出 TypeErrorL43-L50);
  • 每个载荷的逐类型校验被推迟到原生构造器执行——因为同步构造器无法知道一个 Promise 最终会解析成什么,逐载荷校验发生在 clipboard.write() 真正提交前的 kToNative() 阶段(L89-L110)。

原生侧的类型分发逻辑在 electron_api_clipboard_item.cc 的构造函数中,C++ 载荷用 std::variant 表示三种形态(electron_api_clipboard_item.h):

MIME 类别 C++ 存储形态 说明
text/plaintext/htmltext/rtfelectron application/findtext(macOS) std::u16string 文本类 MIME 存 UTF-16 文本
electron application/bookmark BookmarkInfo { title, url } 结构化对象,经 ScopedClipboardWriter::WriteURL 提交
其余全部(image/*web 前缀、osclipboard;format=...、任意自定义 MIME) std::vector<uint8_t> 原始字节

即:任何 MIME 都允许传 string(UTF-8 编码),书签格式必须是对象,其余值既非 string 又非 Blob 时抛 TypeError

二、实例属性 clipboardItem.types

  • Readonly,类型 string[] — 该条目携带的数据的 MIME 类型列表。
    • 构造出来的 ClipboardItem:就是构造器传入对象的键;
    • clipboard.read() 返回的条目:是平台剪贴板当前实际可用的一组 MIME 类型。

实现上,JS 门面的 types getter 在两条路径上分别取数(clipboard-item.ts L64-L67):读侧转发给原生 item.types,构造侧直接返回存储的 MIME 键。注意 clipboard.read() 的条目可能包含平台剪贴板上的任意类型——包括没有标准 MIME 映射、被归入 electron application/osclipboard;format="..." 自定义格式的平台原生格式(见 clipboard 文档),因此代码中应先检查 types 再调用 getType()

三、实例方法 getType(type)

3.1 clipboardItem.getType(type)

  • type string — 要取回的 MIME 类型。

返回 Promise<Blob> | Promise<ClipboardBookmark>,建模自 W3C ClipboardItem.getType:大多数 MIME 类型解析为 Blob;唯一例外是 getType('electron application/bookmark'),它解析为 ClipboardBookmark 对象({ title: string, url: string },定义见 clipboard-bookmark.md)。当 type 不在 clipboardItem.types 中时 reject

const { clipboard } = require('electron')

async function dumpClipboard () {
  const items = await clipboard.read()
  for (const item of items) {
    for (const type of item.types) {
      const payload = await item.getType(type)
      console.log(type, payload)
    }
  }
}

3.2 clipboardItem.getType(bookmark)

  • bookmark 'electron application/bookmark' — 固定为该 MIME 字符串。

返回 Promise<ClipboardBookmark>:剪贴板中可用书签时解析为 ClipboardBookmark;没有书签时 reject。

const { clipboard } = require('electron')

async function dumpClipboard () {
  const bookmarkType = 'electron application/bookmark'
  const items = await clipboard.read()
  for (const item of items) {
    if (item.types.includes(bookmarkType)) {
      const bookmark = await item.getType(bookmarkType)
      console.log('Bookmark found: ', bookmark)
    } else {
      console.log('There is no bookmark present')
    }
  }
}

读侧 getType 的底层分发表

从源码看,读侧条目并不在 clipboard.read() 时预取数据,而是把"来源 ui::ClipboardBuffer + 可用类型列表"存进 C++ 对象(CreateForRead 工厂),getType(mime) 被调用时才走 ReadMime 分发函数惰性读取平台剪贴板。这个按 MIME 分发的处理值得逐条理解:

MIME 底层读取路径 JS 可见结果
web 前缀自定义格式 ExtractCustomPlatformNames 查映射后按平台格式 ReadData Buffer 字节原样往返
electron application/osclipboard;format="<name>" 解析出 <name> 后按平台格式原始读取 Buffer
text/plain ReadText(Windows 上文本为空时回退 ReadAsciiText Buffer(UTF-8)
text/html ReadHTML,只取 fragment_start/end 片段(即 W3C 约定:不含 Chromium 包裹头) Buffer
text/rtf ReadRTF Buffer
image/*image/png / image/jpeg ReadPng(在 app ready 后调度 PNG 解码;image/jpeg 会把解码出的位图重新编码为 JPEG 质量 100) Buffer
electron application/bookmark ReadURLClipboardUrlInfo { title, url } 对象
electron application/findtext(仅 macOS) 同步读 NSFindPasteboard Buffer
text/uri-list ReadFilenames 得到绝对路径,再 FileInfosToURIList 序列化为 RFC 2483 Bufferfile:// URI 列表)
其他任意 MIME 回退到原始平台格式读取,保证任意自定义格式字节级往返 Buffer

两个细节对写业务代码很重要:其一,image/* 的读取依赖线程池解码 PNG,因此必须在 app ready 之后,否则 Promise 以 "clipboard.read of image/* is available only after app ready" 被 reject(源码);其二,text/html 读回的是 Chromium 剪贴板约定中剥离元数据头之后的纯 HTML 片段。

四、text/uri-list:文件剪贴板的跨平台映射

text/uri-list 是整个 API 中最具平台特殊性的 MIME 类型:它被映射到操作系统的原生"已复制文件"剪贴板格式(Windows 的 CF_HDROP、macOS 的 NSFilenamesPboardType、Linux 的 text/uri-list),而不是当作普通文本存储。效果是双向的:

  • Electron 写入的条目可以被 Finder、资源管理器或文件管理器当作文件粘贴
  • 从这些原生应用复制的文件也能被 Electron 读回。

载荷格式是 RFC 2483 URI 列表:每行一个 file:// URI,以 CRLF 分隔。用 Node 的 url.pathToFileURL 把绝对路径转成 file:// URI。虽然 RFC 2483 允许任意 URI scheme,但该格式在本 API 中只处理文件——非 file:// URI 会被忽略。

读取侧,getType('text/uri-list') 解析的 Blob 文本就是 file:// URI 列表。由于这是特权的主进程 API,解析出的 URI 包含真实的绝对路径——这与渲染进程 navigator.clipboard 出于隐私考虑会清洗文件路径的行为不同。

const { clipboard, ClipboardItem } = require('electron')
const { pathToFileURL } = require('node:url')

// 把两个文件写入剪贴板,使其可以粘贴到系统文件管理器。
clipboard.write([
  new ClipboardItem({
    'text/uri-list': [
      pathToFileURL('/path/to/first.txt').href,
      pathToFileURL('/path/to/second.txt').href
    ].join('\r\n')
  })
])

// 读取剪贴板当前承载的文件。
async function readFiles () {
  const [item] = await clipboard.read()
  if (item.types.includes('text/uri-list')) {
    const blob = await item.getType('text/uri-list')
    if (blob instanceof Blob) {
      const uriList = await blob.text()
      return uriList.split(/\r?\n/).filter(Boolean)
    }
  }
  return []
}

源码印证了两端的具体实现:写入侧,WriteTotext/uri-list 载荷直接调用 ScopedClipboardWriter::WriteFilenames,由 Chromium 完成到各平台文件格式的转换(electron_api_clipboard_item.cc L521-L525);读取侧调用 ReadFilenames 后由 ui::FileInfosToURIList 序列化回 URI 列表(L298-L313)。官方测试 spec/api-clipboard-spec.tsreadUriListPaths() 辅助函数(fileURLToPath 解码回绝对路径)验证了"写入路径 → 读回路径"的往返一致性(L46-L58)。

五、写入管线:原子提交与 Blob/Promise 的异步解析

clipboard.write() 的语义在 clipboard 文档中明确:同一次 write() 调用提供的所有条目会原子地提交到系统剪贴板。这条管线横跨三层:

  1. JS 编排层 lib/browser/api/clipboard.tswrite(items) 先校验数组与每个元素的 instanceof ClipboardItem,然后逐个 await item[kToNative]()——此时才等待 Promise 载荷、把 Blob 读成 Buffer——最后把所有解析完的原生条目交给一次同步的原生 write,保证原子性。这也解释了文档中"BlobPromise 载荷在 clipboard.write() 被调用时才异步解析"的含义:解析发生在提交前的编排阶段。
  2. 原生写路径 electron_api_clipboard.cc + ClipboardItem::WriteTo:遍历每个 MIME 分派到 ScopedClipboardWriter 的对应方法——WriteText / WriteHTML / WriteRTF / WriteURL(书签)/ WriteFilenames(uri-list)/ WriteImageimage/* 字节经 AddImageSkiaRepFromBuffer 解码后 WriteImage);web 前缀格式走 WriteData 以便提交 org.w3.web-custom-format.map(读回时靠它识别出 web MIME 名),osclipboard;format= 与任意自定义 MIME 走 WriteUnsafeRawData 原样落地平台格式。
  3. macOS 特例electron application/findtext 对应独立的 NSFindPasteboard,不与系统剪贴板共享原子提交生命周期,因此在 WriteTo 中直接同步提交(L501-L506)。

另外,读写的对称闭环还体现在自定义格式上:clipboard.read() 会把没有标准 MIME 映射的平台格式也归入 electron application/osclipboard;format="<name>" 自定义格式暴露出来(Windows 上会把数字注册格式 ID 解析回字符串名,见 ResolvePlatformFormatName),因此一个原始 OS 格式在写和读两端用同一个 MIME 字符串往返。完整格式列表与 clipboard.has(mimetype) 的用法见 clipboard API 文档

六、测试参考与相关文档

七、要点速查

事项 结论
可用进程 仅 Main
载荷形态 string(UTF-8 编码)/ Blob(原始字节)/ Promise(write 时 await)/ 书签格式用 { title, url } 对象
types 构造侧 = 传入的 MIME 键;读侧 = 平台当前可用类型
getType(type) 一般解析 Blob;书签 MIME 解析 ClipboardBookmark;type 不在 types 中则 reject
text/uri-list 映射 OS 原生文件格式;RFC 2483,file:// URI + CRLF,只处理文件;读回含真实绝对路径
写回读侧条目 TypeError,需重新 new ClipboardItem
原子性 一次 write() 的全部条目经单个 ScopedClipboardWriter 原子提交
安全 对来自 IPC/渲染进程的数据必须白名单化 MIME 与载荷形状后再构造
登录后查看全文
热门项目推荐
相关项目推荐