更安全、更顺畅地访问剪贴板中的文字和图片
访问系统剪贴板的传统方法是通过 document.execCommand()
进行剪贴板互动。虽然这种剪切和粘贴方法得到广泛支持,但其代价是:剪贴板访问是同步的,并且只能对 DOM 执行读写操作。
这对于少量文本没有问题,但在很多情况下,阻止网页进行剪贴板传输会给用户带来糟糕的体验。可能需要执行耗时的清理或图片解码,才能安全地粘贴内容。浏览器可能需要从粘贴的文档加载或内嵌链接的资源。这会导致在等待磁盘或网络时阻塞页面。想象一下,向混合权限中添加权限,要求浏览器在请求剪贴板访问权限时屏蔽相应网页。同时,针对剪贴板交互的 document.execCommand()
设置的权限较为宽松,并且因浏览器而异。
Async Clipboard API 可以解决这些问题,它提供了一个定义完善且不会阻塞网页的权限模型。在大多数浏览器上,Async Clipboard API 仅限处理文本和图片,但具体支持情况各有不同。请务必仔细研究以下各个部分的浏览器兼容性概览。
复制:将数据写入剪贴板
writeText()
如需将文本复制到剪贴板,请调用 writeText()
。由于此 API 是异步的,因此 writeText()
函数会返回一个解析或拒绝的 Promise,具体取决于传递的文本是否复制成功:
async function copyPageUrl() {
try {
await navigator.clipboard.writeText(location.href);
console.log('Page URL copied to clipboard');
} catch (err) {
console.error('Failed to copy: ', err);
}
}
write()
实际上,writeText()
只是通用 write()
方法的便捷方法,您还可以利用该方法将图片复制到剪贴板。与 writeText()
一样,它是异步的,并返回 Promise。
如需将图片写入剪贴板,您需要将图片设置为 blob
。如需实现此目的,一种方法是使用 fetch()
从服务器请求图片,然后对响应调用 blob()
。
出于各种原因,从服务器请求图片可能不可取。幸运的是,您还可以将图片绘制到画布并调用画布的 toBlob()
方法。
接下来,将 ClipboardItem
对象数组作为参数传递给 write()
方法。目前,一次只能传递一张图片,但我们希望将来能够支持多张图片。ClipboardItem
接受一个对象,该对象的 MIME 类型是图片的键,以及 blob 作为值。对于从 fetch()
或 canvas.toBlob()
获取的 blob 对象,blob.type
属性会自动包含图片的正确 MIME 类型。
try {
const imgURL = '/images/generic/file.png';
const data = await fetch(imgURL);
const blob = await data.blob();
await navigator.clipboard.write([
new ClipboardItem({
// The key is determined dynamically based on the blob's type.
[blob.type]: blob
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}
或者,您也可以向 ClipboardItem
对象写入一个 promise。对于此模式,您需要事先知道数据的 MIME 类型。
try {
const imgURL = '/images/generic/file.png';
await navigator.clipboard.write([
new ClipboardItem({
// Set the key beforehand and write a promise as the value.
'image/png': fetch(imgURL).then(response => response.blob()),
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}
复制事件
如果用户启动了剪贴板复制操作并且没有调用 preventDefault()
,copy
事件包含一个 clipboardData
属性,并且该项已采用正确的格式。如果您要实现自己的逻辑,则需要调用 preventDefault()
,以防止默认行为改为使用您自己的实现方式。在这种情况下,clipboardData
将为空。
假设有一个包含文本和图片的网页,当用户全选并启动剪贴板复制操作时,您的自定义解决方案应舍弃文本,仅复制图片。您可以参考以下代码示例来实现此目的。本示例未涵盖在 Clipboard API 不受支持的情况下如何回退到早期版本的 API。
<!-- The image we want on the clipboard. -->
<img src="kitten.webp" alt="Cute kitten.">
<!-- Some text we're not interested in. -->
<p>Lorem ipsum</p>
document.addEventListener("copy", async (e) => {
// Prevent the default behavior.
e.preventDefault();
try {
// Prepare an array for the clipboard items.
let clipboardItems = [];
// Assume `blob` is the blob representation of `kitten.webp`.
clipboardItems.push(
new ClipboardItem({
[blob.type]: blob,
})
);
await navigator.clipboard.write(clipboardItems);
console.log("Image copied, text ignored.");
} catch (err) {
console.error(err.name, err.message);
}
});
对于 copy
事件:
对于 ClipboardItem
:
粘贴:从剪贴板读取数据
readText()
如需读取剪贴板中的文本,请调用 navigator.clipboard.readText()
并等待返回的 promise 进行解析:
async function getClipboardContents() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted content: ', text);
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
}
read()
navigator.clipboard.read()
方法也是异步方法,会返回一个 promise。如需从剪贴板读取图片,请获取 ClipboardItem
对象列表,然后遍历它们。
每个 ClipboardItem
都可以将其内容保存在不同的类型中,因此您需要再次使用 for...of
循环遍历类型列表。对于每种类型,请使用当前类型作为参数调用 getType()
方法,以获取相应的 blob。与之前一样,此代码未与图片相关联,适用于未来的其他文件类型。
async function getClipboardContents() {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
const blob = await clipboardItem.getType(type);
console.log(URL.createObjectURL(blob));
}
}
} catch (err) {
console.error(err.name, err.message);
}
}
处理粘贴的文件
能够使用剪贴板键盘快捷键(例如 ctrl + c 和 ctrl + v)对用户来说非常有用。Chromium 会在剪贴板上提供只读文件,如下所述。 当用户点击操作系统的默认粘贴快捷方式,或者在用户点击浏览器菜单栏中的修改,然后点击粘贴时,这会触发此操作。无需进一步的连接代码。
document.addEventListener("paste", async e => {
e.preventDefault();
if (!e.clipboardData.files.length) {
return;
}
const file = e.clipboardData.files[0];
// Read the file's contents, assuming it's a text file.
// There is no way to write back to it.
console.log(await file.text());
});
粘贴事件
如前所述,我们计划引入支持 Clipboard API 的事件,但目前您可以使用现有的 paste
事件。它可以很好地与用于读取剪贴板文本的新异步方法结合使用。与 copy
事件一样,请务必调用 preventDefault()
。
document.addEventListener('paste', async (e) => {
e.preventDefault();
const text = await navigator.clipboard.readText();
console.log('Pasted text: ', text);
});
处理多个 MIME 类型
大多数实现会将多种数据格式放到剪贴板中,以便执行单个剪切或复制操作。这有两个原因:作为应用开发者,您无法了解用户想要将文本或图片复制到的应用的功能,并且许多应用支持将结构化数据粘贴为纯文本。通常,如果用户使用修改菜单项的名称(例如“粘贴和匹配样式”或“粘贴不带格式”)。
以下示例展示了如何执行此操作。此示例使用 fetch()
获取图片数据,但也可能来自 <canvas>
或 File System Access API。
async function copy() {
const image = await fetch('kitten.png').then(response => response.blob());
const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
const item = new ClipboardItem({
'text/plain': text,
'image/png': image
});
await navigator.clipboard.write([item]);
}
安全与权限
剪贴板访问一直是浏览器的一项安全问题。如果没有适当的权限,页面可能会以静默方式将各种恶意内容复制到用户的剪贴板,粘贴这些内容会产生灾难性的结果。
假设某个网页会以静默方式将 rm -rf /
或解压缩炸弹图片复制到剪贴板。
让网页不受限制地读取剪贴板中的内容会更加棘手。用户经常将密码和个人详细信息等敏感信息复制到剪贴板,然后在用户不知情的情况下被任何页面读取。
与许多新 API 一样,仅通过 HTTPS 提供的网页支持 Clipboard API。为防止滥用,只有当页面是活跃标签页时,才允许访问剪贴板。活动标签页中的网页无需请求权限即可向剪贴板中写入内容,但从剪贴板读取数据始终需要相应权限。
Permissions API 中添加了复制和粘贴权限。当页面处于活跃状态时,系统会自动向页面授予 clipboard-write
权限。您必须请求 clipboard-read
权限,为此,您可以尝试从剪贴板读取数据。以下代码展示了后者:
const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
const permissionStatus = await navigator.permissions.query(queryOpts);
// Will be 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);
// Listen for changes to the permission state
permissionStatus.onchange = () => {
console.log(permissionStatus.state);
};
您还可以使用 allowWithoutGesture
选项控制是否需要使用用户手势来调用剪切或粘贴操作。此值的默认值因浏览器而异,因此您应始终将其包含在内。
这时 Clipboard API 异步性质便派上用场了:如果用户尝试读取或写入剪贴板数据,系统会在用户尚未授予权限时自动提示。由于 API 是基于 promise 的,因此这是完全透明的,如果用户拒绝剪贴板权限,会导致 promise 拒绝,以便页面可以做出适当的响应。
由于浏览器仅允许在页面处于活动状态时访问剪贴板,所以您会发现,如果直接粘贴到浏览器的控制台中,此处的部分示例将无法运行,因为开发者工具本身就是活跃标签页。有个技巧:使用 setTimeout()
推迟剪贴板访问,然后在调用函数之前快速点击页面内使其聚焦于焦点:
setTimeout(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
}, 2000);
权限政策集成
如需在 iframe 中使用该 API,您需要使用权限政策启用该 API,其中定义了一种机制,允许选择性地启用和停用各种浏览器功能和 API。具体而言,您需要传递 clipboard-read
或 clipboard-write
或者同时传递这两个参数,具体取决于应用的需求。
<iframe
src="index.html"
allow="clipboard-read; clipboard-write"
>
</iframe>
功能检测
如需在使用 Async Clipboard API 的同时支持所有浏览器,请测试 navigator.clipboard
并回退到早期的方法。例如,下面展示了如何实现粘贴以包含其他浏览器。
document.addEventListener('paste', async (e) => {
e.preventDefault();
let text;
if (navigator.clipboard) {
text = await navigator.clipboard.readText();
}
else {
text = e.clipboardData.getData('text/plain');
}
console.log('Got pasted text: ', text);
});
这还不止于此。在 Async Clipboard API 出现之前,网络浏览器中混合了不同的复制和粘贴实现。在大多数浏览器中,可以使用 document.execCommand('copy')
和 document.execCommand('paste')
触发浏览器自身的复制和粘贴操作。如果要复制的文本是 DOM 中不存在的字符串,必须将其注入 DOM 并选中:
button.addEventListener('click', (e) => {
const input = document.createElement('input');
input.style.display = 'none';
document.body.appendChild(input);
input.value = text;
input.focus();
input.select();
const result = document.execCommand('copy');
if (result === 'unsuccessful') {
console.error('Failed to copy text.');
}
input.remove();
});
样本歌曲
在下面的演示中,您可以体验一下 Async Clipboard API。在 Glitch 上,您可以混剪文本演示或图片演示来做实验。
第一个示例演示了如何将文本移入和移出剪贴板。
如需尝试对 API 使用图片,请使用此演示。回想一下,目前仅支持 PNG 格式,而且只有少数浏览器支持这种格式。
相关链接
致谢
Aasync Clipboard API 由 Darwin Huang 和 Gary Kačmarčík 实现。Darwin 还提供了演示。 感谢 Kyarik 和 Gary Kačmarčík 审核本文部分内容。
主打图片,由 Markus Winkler 制作 (Unsplash)。