如果您已经使用电脑一段时间,您可能知道剪贴板可以存储多种类型的数据(图像、富文本内容、文件等)。作为一名软件开发人员,我开始感到沮丧,因为我对剪贴板如何存储和组织不同类型的数据没有很好的理解。
我最近决定揭开剪贴板的神秘面纱,并利用我的学习成果写了这篇文章。我们将重点介绍网络剪贴板及其 API,但我们也会涉及它如何与操作系统剪贴板交互。
我们首先会探索 Web 的剪贴板 API 及其历史。剪贴板 API 在数据类型方面有一些有趣的限制,我们将了解一些公司如何解决这些限制。我们还将研究一些旨在解决这些限制的提案(最值得注意的是Web 自定义格式)。
如果你曾经好奇过网络剪贴板是如何工作的,那么这篇文章正适合你。
使用异步剪贴板 API
如果我从网站复制一些内容并将其粘贴到 Google Docs 中,则它的一些格式会保留,例如链接、字体大小和颜色。
但是如果我打开 VS Code 并将其粘贴到那里,则只会粘贴原始文本内容。
剪贴板通过允许将信息存储在与 MIME 类型相关的多种表示中来满足这两种用例。W3C 剪贴板规范规定,要写入和读取剪贴板,必须支持以下三种数据类型:
text/plain
用于纯文本。text/html
用于 HTML。image/png
对于 PNG 图像。
因此,当我之前粘贴时,Google Docs 会读取text/html
表示并使用它来保留富文本格式。VS Code 只关心原始文本并读取text/plain
表示,有道理。
通过异步剪贴板 API 的方法读取特定read
表示非常简单:
javascript
const items = await navigator.clipboard.read();
for (const item of items) {
if (item.types.includes("text/html")) {
const blob = await item.getType("text/html");
const html = await blob.text();
// Do stuff with HTML...
}
}
通过 向剪贴板写入多个表示write
有点复杂,但仍然相对简单。首先,我们Blob
为要写入剪贴板的每个表示构造:
javascript
const textBlob = new Blob(["Hello, world"], { type: "text/plain" });
const htmlBlob = new Blob(["Hello, <em>world<em>"], { type: "text/html" });
一旦我们有了 Blob,我们就将它们传递给ClipboardItem
键值存储中的新对象,其中数据类型作为键,Blob 作为值:
javascript
const clipboardItem = new ClipboardItem({
[textBlob.type]: textBlob,
[htmlBlob.type]: htmlBlob,
});
注意:我喜欢ClipboardItem
接受键值存储。它与使用使非法状态无法表示的数据结构的想法非常吻合,如Parse, don't valid中所述。
最后,我们write
用新构造的来调用ClipboardItem
:
javascript
await navigator.clipboard.write([clipboardItem]);
那么其他数据类型怎么样?
HTML 和图像很酷,但像 JSON 这样的通用数据交换格式怎么样?如果我正在编写一个支持复制粘贴的应用程序,我可以想象想要将 JSON 或一些二进制数据写入剪贴板。
让我们尝试将 JSON 数据写入剪贴板:
javascript
// Create JSON blob
const json = JSON.stringify({ message: "Hello" });
const blob = new Blob([json], { type: "application/json" });
// Write JSON blob to clipboard
const clipboardItem = new ClipboardItem({ [blob.type]: blob });
await navigator.clipboard.write([clipboardItem]);
运行此程序时,会引发异常:
javascript
Failed to execute 'write' on 'Clipboard':
Type application/json not supported on write.
嗯,这是怎么回事?好吧,的规范write
告诉我们,除text/plain
、text/html
和之外的数据类型image/png
必须被拒绝:
如果_类型_不在强制数据类型列表中,则拒绝[...]并中止这些步骤。
有趣的是, MIME 类型在2012 年至2021 年application/json
期间一直位于强制数据类型列表中,但在w3c/clipboard-apis#155中从规范中删除。在此更改之前,强制数据类型列表要长得多,有 16 种强制数据类型用于从剪贴板读取,8 种强制数据类型用于写入剪贴板。更改后,只剩下、和。text/plain``text/html``image/png
由于安全问题,浏览器选择不支持许多强制类型,因此进行了此更改。规范中强制数据类型部分的警告反映了这一点:
警告!出于安全考虑,不受信任的脚本可以写入剪贴板的数据类型受到限制。
不受信任的脚本可以尝试通过将已知会触发漏洞的数据放在剪贴板上来利用本地软件中的安全漏洞。
_好的,所以我们只能将一组有限的数据类型写入剪贴板。但是"不受信任的_脚本"又是怎么回事呢?我们能否以某种方式运行"受信任"脚本中的代码,从而允许我们将其他数据类型写入剪贴板?
isTrusted 属性
也许"受信任"部分是指isTrusted事件的属性。isTrusted
是一个只读属性,只有当事件由用户代理分派时才设置为 true。
javascript
document.addEventListener("copy", (e) => {
if (e.isTrusted) {
// This event was triggered by the user agent
}
})
"由用户代理发送"意味着它是由用户触发的,例如用户按下命令 C
。这与通过以下方式编程分派的合成事件形成对比dispatchEvent()
:
javascript
document.addEventListener("copy", (e) => {
console.log("e.isTrusted is " + e.isTrusted);
});
document.dispatchEvent(new ClipboardEvent("copy"));
//=> "e.isTrusted is false"
让我们看看剪贴板事件,看看它们是否允许我们将任意数据类型写入剪贴板。
剪贴板事件 API
ClipboardEvent
为复制、剪切和粘贴事件调度A ,它包含一个clipboardData
类型的属性DataTransfer
。DataTransfer
剪贴板事件 API 使用该对象来保存数据的多种表示形式。
在事件中写入剪贴板copy
非常简单:
javascript
document.addEventListener("copy", (e) => {
e.preventDefault(); // Prevent default copy behavior
e.clipboardData.setData("text/plain", "Hello, world");
e.clipboardData.setData("text/html", "Hello, <em>world</em>");
});
在事件中从剪贴板读取也paste
同样简单:
javascript
document.addEventListener("paste", (e) => {
e.preventDefault(); // Prevent default paste behavior
const html = e.clipboardData.getData("text/html");
if (html) {
// Do stuff with HTML...
}
});
现在要回答一个大问题:我们可以将 JSON 写入剪贴板吗?
javascript
document.addEventListener("copy", (e) => {
e.preventDefault();
const json = JSON.stringify({ message: "Hello" });
e.clipboardData.setData("application/json", json); // No error
});
没有抛出异常,但这真的将 JSON 写入剪贴板了吗?让我们通过编写一个粘贴处理程序来验证这一点,该处理程序遍历剪贴板中的所有条目并将其记录下来:
javascript
document.addEventListener("paste", (e) => {
for (const item of e.clipboardData.items) {
const { kind, type } = item;
if (kind === "string") {
item.getAsString((content) => {
console.log({ type, content });
});
}
}
});
添加这两个处理程序并调用复制粘贴将导致记录以下内容:
javascript
{ "type": "application/json", content: "{\"message\":\"Hello\"}" }
成功了!看起来它clipboardData.setData
不像异步方法那样限制数据类型write
。
但是...为什么?为什么我们可以用 读写任意数据类型,clipboardData
但使用异步剪贴板 API 时却不行?
历史clipboardData
相对较新的异步剪贴板 API 于2017 年添加到规范中,但比这clipboardData
要早_得多。__ _2006 年的剪贴板 API 的 W3C 草案定义了clipboardData
及其setData
方法getData
(这表明当时尚未使用 MIME 类型):
setData()
这需要一个或两个参数。第一个必须设置为"text"或"URL"(不区分大小写)。
getData()
这需要一个参数,允许目标请求特定类型的数据。
但事实证明,它clipboardData
比 2006 年的草案还要古老。请看"本文档的状态"部分的这段引文:
在很大程度上,[本文档]描述了 Internet Explorer 中实现的功能......
本文档的目的是[...]指定当前浏览器中实际运行的功能,或[成为]提高互操作性的简单目标,而不是添加新功能。
这篇2003 年的文章详细介绍了当时 Internet Explorer 4 及更高版本中如何clipboardData
在未经用户同意的情况下读取用户的剪贴板。由于 Internet Explorer 4 于 1997 年发布,因此clipboardData
在撰写本文时,该界面似乎至少已有 26 年历史。
MIME 类型于 2011 年进入规范:
dataType_参数_是一个字符串,例如但不限于 MIME 类型...
如果脚本调用 getData('text/html')...
当时,规范尚未确定应该使用哪种数据类型:
虽然可以使用任何字符串作为 setData() 的类型参数,但建议坚持使用常见类型。
问题\] 我们应该列出一些"常见类型"吗? 能够使用_任何_字符串进行 和 的`setData`操作`getData`至今仍然有效。以下操作完全没问题: ```javascript document.addEventListener("copy", (e) => { e.preventDefault(); e.clipboardData.setData("foo bar baz", "Hello, world"); }); document.addEventListener("paste", (e) => { const content = e.clipboardData.getData("foo bar baz"); if (content) { console.log(content); // Logs "Hello, world!" } }); ``` 如果您将此代码片段粘贴到您的 DevTools 中,然后点击复制并粘贴,您将看到消息"Hello, world"记录到您的控制台中。 剪贴板事件 API 允许我们使用任何数据类型的原因`clipboardData`似乎有历史原因。*"不要破坏网络"*。 ### 重温 isTrusted 让我们再次考虑一下[强制数据类型](https://link.juejin.cn?target=https%3A%2F%2Fwww.w3.org%2FTR%2Fclipboard-apis%2F%23mandatory-data-types-x "https://www.w3.org/TR/clipboard-apis/#mandatory-data-types-x")部分中的这句话: 为了安全预防措施,不受信任的脚本可以写入剪贴板的数据类型受到限制。 那么,如果我们尝试在合成(不受信任的)剪贴板事件中写入剪贴板,会发生什么? ```javascript document.addEventListener("copy", (e) => { e.preventDefault(); e.clipboardData.setData("text/plain", "Hello"); }); document.dispatchEvent(new ClipboardEvent("copy", { clipboardData: new DataTransfer(), })); ``` 运行成功,但不会修改剪贴板。这是[规范中解释的](https://link.juejin.cn?target=https%3A%2F%2Fwww.w3.org%2FTR%2Fclipboard-apis%2F%23integration-with-other-scripts-and-events "https://www.w3.org/TR/clipboard-apis/#integration-with-other-scripts-and-events")预期行为: 合成剪切和复制事件_不得_修改系统剪贴板上的数据。 合成粘贴事件_不得_让脚本访问真实系统剪贴板上的数据。 因此,只有用户代理发送的复制和粘贴事件才允许修改剪贴板。这完全有道理------我不希望网站随意读取我的剪贴板内容并窃取我的密码。 *** ** * ** *** 总结一下我们迄今为止的发现: * 2017 年推出的异步剪贴板 API 限制了可以写入剪贴板和从剪贴板读取的数据类型。但是,只要用户授予了权限(并且文档处于[焦点状态](https://link.juejin.cn?target=https%3A%2F%2Fwww.w3.org%2FTR%2Fclipboard-apis%2F%23privacy-async "https://www.w3.org/TR/clipboard-apis/#privacy-async")) ,它就可以随时读取和写入剪贴板。 * 旧版剪贴板事件 API 对于哪些数据类型可以写入剪贴板和从剪贴板读取没有实际限制。但是,它只能在由用户代理触发的复制和粘贴事件处理程序中使用(即当`isTrusted`为 true 时)。 如果您想将不仅仅是纯文本、HTML 或图像的数据类型写入剪贴板,似乎使用 Clipboard Events API 是唯一的出路。在这方面,它的限制要少得多。 但是,如果您想构建一个将非标准数据类型写入剪贴板的"复制"按钮,该怎么办?如果用户未触发复制事件,您似乎无法使用剪贴板事件 API。对吗? ## 构建一个可写入任意数据类型的复制按钮 我尝试了不同的 Web 应用程序中的"复制"按钮,并检查了写入剪贴板的内容。结果很有趣。 Google Docs 有一个复制按钮,可以在其右键菜单中找到。  此复制按钮将三种表示写入剪贴板: * `text/plain`, * `text/html`, 和 * `application/x-vnd.google-docs-document-slice-clip+wrapped` 注意:第三种表示包含 JSON 数据。 他们正在向剪贴板写入自定义数据类型,这意味着他们没有使用异步剪贴板 API。他们如何通过点击处理程序做到这一点? 我运行了分析器,点击了复制按钮,然后检查了结果。结果发现,点击复制按钮会触发对 的调用`document.execCommand("copy")`。  这让我很惊讶。我的第一个想法是_"这不是_`_execCommand_`*旧的、过时的将文本复制到剪贴板的方法吗?"*。 是的,但是谷歌使用它是有原因的。它`execCommand`的特别之处在于它允许您以编程方式分派受信任的复制事件,_就好像_用户自己调用了复制命令一样。 ```javascript document.addEventListener("copy", (e) => { console.log("e.isTrusted is " + e.isTrusted); }); document.execCommand("copy"); //=> "e.isTrusted is true" ``` 注意:Safari 需要主动选择 才能`execCommand("copy")`分派复制事件。可以通过向 DOM 添加非空输入元素并在调用 之前选择它来伪造该选择`execCommand("copy")`,之后可以从 DOM 中删除输入。 好的,使用它`execCommand`可以让我们在响应点击事件时将任意数据类型写入剪贴板。太棒了! 那粘贴呢?我们可以使用吗`execCommand("paste")`? ## 构建粘贴按钮 让我们尝试一下 Google Docs 中的粘贴按钮,看看它的作用是什么。  在我的 Macbook 上,出现一个弹出窗口,告诉我需要安装扩展程序才能使用粘贴按钮。  但奇怪的是,在我的 Windows 笔记本电脑上,粘贴按钮就可以正常工作。 奇怪。不一致从何而来?好吧,可以通过运行以下命令来检查粘贴按钮是否有效`queryCommandSupported("paste")`: ```javascript document.queryCommandSupported("paste"); ``` 在我的 Macbook 上,我使用的`false`是 Chrome 和 Firefox,但`true`使用的是 Safari。 Safari 非常注重隐私,要求我确认粘贴操作。我认为这是个好主意。它明确表明该网站将从您的剪贴板读取内容。  在我的 Windows 笔记本电脑上,我使用了`true`Chrome 和 Edge,但`false`使用的是 Firefox。Chrome 的不一致令人惊讶。为什么 Chrome`execCommand("paste")`在 Windows 上允许,但在 macOS 上却不允许?我找不到有关此问题的任何信息。 令我惊讶的是,Google 在不可用时不会尝试回退到异步 Clipboard API `execCommand("paste")`。尽管他们无法`application/x-vnd.google-[...]`使用该 API 读取表示,但 HTML 表示包含可以使用的内部 ID。 ```javascript Copied text ``` *** ** * ** *** 另一个带有粘贴按钮的 Web 应用程序是 Figma,他们采用了完全不同的方法。让我们看看他们在做什么。 ## 在 Figma 中复制和粘贴 Figma 是一个基于 Web 的应用程序(其原生应用程序使用[Electron](https://link.juejin.cn?target=https%3A%2F%2Fwww.electronjs.org%2F "https://www.electronjs.org/"))。让我们看看他们的复制按钮会写入剪贴板的内容。  Figma 的复制按钮将两种表示形式写入剪贴板:`text/plain`和`text/html`。这起初让我很惊讶。Figma 如何用纯 HTML 表示它们的各种布局和样式功能? 但查看 HTML,我们看到两个具有和属性的空`span`元素:```data-metadata``data-buffer``` ```javascript