Electron ClipboardItem 深入解析:W3C 风格剪贴板条目的构造、读写与跨平台实现
本文围绕 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()取回载荷。
需要牢记的两个约束:
- 不可继承:Electron 的内置类无法在用户代码中子类化(参见 FAQ:Class Inheritance 一节);
- 读取侧条目不可写回:从源码看,
clipboard.read()返回的条目内部持有来源剪贴板缓冲而非载荷(is_read_side_标志),若把它直接传给clipboard.write()会抛出TypeError(见 clipboard-item.ts 的 kToNative 校验)。
构造器 new ClipboardItem(items)
itemsRecord<string, string | ClipboardBookmark | Blob | Promise<Blob | string>>— 键为 MIME 类型,值为该类型的载荷。参数结构与 W3CClipboardItem(items)构造器的items参数一致。每个 MIME 类型都接受string或Blob:string按 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 门面的参数校验
从源码看,用户接触到的 ClipboardItem 是 lib/browser/api/clipboard-item.ts 中 JS 类的一个异步门面,它包裹原生绑定 process._linkedBinding('electron_browser_clipboard_item'):
- 构造时只做"浅校验":
items必须是普通对象(非数组、非 null),且至少包含一个 MIME 键,否则同步抛出TypeError(L43-L50); - 每个载荷的逐类型校验被推迟到原生构造器执行——因为同步构造器无法知道一个
Promise最终会解析成什么,逐载荷校验发生在clipboard.write()真正提交前的kToNative()阶段(L89-L110)。
原生侧的类型分发逻辑在 electron_api_clipboard_item.cc 的构造函数中,C++ 载荷用 std::variant 表示三种形态(electron_api_clipboard_item.h):
| MIME 类别 | C++ 存储形态 | 说明 |
|---|---|---|
text/plain、text/html、text/rtf、electron 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)
typestring— 要取回的 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 |
ReadURL → ClipboardUrlInfo |
{ title, url } 对象 |
electron application/findtext(仅 macOS) |
同步读 NSFindPasteboard |
Buffer |
text/uri-list |
ReadFilenames 得到绝对路径,再 FileInfosToURIList 序列化为 RFC 2483 |
Buffer(file:// 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 []
}
源码印证了两端的具体实现:写入侧,WriteTo 对 text/uri-list 载荷直接调用 ScopedClipboardWriter::WriteFilenames,由 Chromium 完成到各平台文件格式的转换(electron_api_clipboard_item.cc L521-L525);读取侧调用 ReadFilenames 后由 ui::FileInfosToURIList 序列化回 URI 列表(L298-L313)。官方测试 spec/api-clipboard-spec.ts 用 readUriListPaths() 辅助函数(fileURLToPath 解码回绝对路径)验证了"写入路径 → 读回路径"的往返一致性(L46-L58)。
五、写入管线:原子提交与 Blob/Promise 的异步解析
clipboard.write() 的语义在 clipboard 文档中明确:同一次 write() 调用提供的所有条目会原子地提交到系统剪贴板。这条管线横跨三层:
- JS 编排层 lib/browser/api/clipboard.ts:
write(items)先校验数组与每个元素的instanceof ClipboardItem,然后逐个await item[kToNative]()——此时才等待 Promise 载荷、把Blob读成Buffer——最后把所有解析完的原生条目交给一次同步的原生write,保证原子性。这也解释了文档中"Blob和Promise载荷在clipboard.write()被调用时才异步解析"的含义:解析发生在提交前的编排阶段。 - 原生写路径 electron_api_clipboard.cc + ClipboardItem::WriteTo:遍历每个 MIME 分派到
ScopedClipboardWriter的对应方法——WriteText/WriteHTML/WriteRTF/WriteURL(书签)/WriteFilenames(uri-list)/WriteImage(image/*字节经AddImageSkiaRepFromBuffer解码后WriteImage);web前缀格式走WriteData以便提交org.w3.web-custom-format.map(读回时靠它识别出webMIME 名),osclipboard;format=与任意自定义 MIME 走WriteUnsafeRawData原样落地平台格式。 - 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 文档。
六、测试参考与相关文档
- 官方行为测试:spec/api-clipboard-spec.ts —— 覆盖
image/*经nativeImage的往返、text/uri-list文件路径往返、书签读写、以及"剪贴板只有文本时不暴露 image 类型"等边界,可作为断言行为的权威参考; - 自定义/
web前缀格式与 Linuxclipboard.selection命名空间的说明:docs/api/clipboard.md; ClipboardBookmark对象结构定义:docs/api/structures/clipboard-bookmark.md;- 类型定义(TypeScript 用户可直接查
Electron.ClipboardItem/Electron.ClipboardBookmark声明):typings/internal-electron.d.ts。
七、要点速查
| 事项 | 结论 |
|---|---|
| 可用进程 | 仅 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 与载荷形状后再构造 |
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00