更安全地访问剪贴板中的文字和图片
访问系统剪贴板的传统方式是
document.execCommand()
进行剪贴板互动虽然这种剪辑方式广受广泛支持,
粘贴需要付费:剪贴板访问是同步的,并且只能读取
并写入 DOM。
这很适合一小段文字,但在很多情况下
页面会给用户带来很糟糕的体验。在内容可以安全地粘贴之前,可能需要进行耗时的清理或图片解码。浏览器可能需要从粘贴的文件中加载或内嵌关联的资源。那就是
在等待磁盘或网络时阻止相应网页。想象添加权限
也要求浏览器在请求网页的同时
剪贴板访问权限。与此同时,为 document.execCommand()
设置的剪贴板互动权限定义较为宽泛,因浏览器而异。
通过 异步剪贴板 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);
}
或者,您也可以将 promise 写入 ClipboardItem
对象。
对于此模式,您需要事先知道数据的 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。具体而言,您需要传递
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 且仅在 几种浏览器。
相关链接
致谢
Asynchronous Clipboard API 由 Darwin 实现 Huang 和 Gary Kačmarčík。Darwin 还提供了演示。 感谢 Kyarik 和 Gary Kačmarčík 审核本文的部分内容。
主打图片由 Unsplash 用户 Markus Winkler 提供。