复制内容到剪贴板
平时我们在浏览一些网站的时候会看到有一键复制的功能,这个功能极大的提升了用户的体验,但是大部分一键复制的功能都是复制的文本,或许 项目经理甲方他突发奇想,欸~为什么我在浏览器可以右键复制图片到剪贴板,你能不能做一个这个功能呢?虽然甲方很讨厌,但是程序员绝不能轻易的说不!经过我不断的copy修改查阅,总算是将其做出来了,下面我会以React插件和原生API两中方法去说明。
一、原生API
首先给大家说一下原生API的实现,原生的API主要是以下两种:
这里我会分别解释这两种API的用法以及优缺点。
1.document.execCommand
这个方法是一个老方法了,很多复制的插件都是使用的这个API,这个插件最大的优点在于使用方便并且兼容性比较好,但是很明显老也是他的缺点,我们可以在MDN的官网上看到如下提示:
很明显它已是垂暮之年,很快就要退休了,不过我们还是会对它的用法做一些介绍。
execCommand支持三种操作分别是复制、剪切、粘贴:
document.execCommand('copy')(复制)document.execCommand('cut')(剪切)document.execCommand('paste')(粘贴)
该方法会返回一个Boolean,如果是 false 则表示操作不被支持或未被启用。在使用的时候我们需要是鼠标选中文字,然后再调用方法,这里我们以一个小的demo举例。
const inputElement = document.querySelector('#input');
inputElement.select();
document.execCommand('copy');
在上面的例子中首先是获取到一个输入框,然后调用输入框的select方法选中文字,这部分操作相当于用户使用鼠标选中文字,然后我们调用复制方法document.execCommand('copy');这样被选中的文字就被复制到剪贴板中了。
2.Clipboard
这个API是比较新的一个API并且不支持IE,所以在使用的时候需要注意它的兼容性。这个API处在全局的navigator下面并且它是一个只读属性。这个API非常强大,除了复制文本也可以复制其他类型的数据,按理来说其他二进制好像也可以,但是我只尝试过图片。这个对象下面有四个方法,分别是:
read()从剪贴板读取数据(比如图片),返回一个Promise对象。在检索到数据后,promise 将兑现一个ClipboardItem对象的数组来提供剪切板数据。readText()从操作系统读取文本;返回一个Promise,在从剪切板中检索到文本后,promise 将兑现一个包含剪切板文本数据的DOMString。write()写入任意数据至操作系统剪贴板。这是一个异步操作,在操作完成后,返回的 promise 的将被兑现。writeText()写入文本至操作系统剪贴板。返回一个Promise,在文本被完全写入剪切板后,返回的 promise 将被兑现。
正因为这个API方法的返回值是promise,所以我们可以安心的复制其他类型的文件,这样就不会因为文件过大而导致页面卡顿。
注意事项
⛳正因为该接口功能过于强大,所以浏览器也对其作出了限制,首先是这个API必须在https或者开发环境(localhost)才能使用,其次就是使用读剪贴板功能时会需要用户确认,否则无法使用。
🎈可能有的小伙伴使用了剪贴板应用可以剪贴板的多项纪录,但是改API只会对第一条进行获取和修改
在使用之前我们首先要判断一下浏览器是否支持Clipboard,其次就是向用户申请权限,这里我封装的了一个方法。
const authentication = () => {
if ("clipboard" in navigator) {
return new Promise((resolve, reject) => {
navigator.permissions.query({ name: "clipboard-read" }).then(
(result) => {
if (result.state == "granted" || result.state == "prompt") {
resolve(true);
} else {
resolve(false);
}
},
(error) => {
reject(error);
}
);
});
} else {
alert("该浏览器暂不支持,请使用最新版本的GoogleChrome浏览");
return Promise.resolve(false);
}
};
这个方法会返回一个promise,我们可以通过then方法来获取到鉴权结果,如果鉴权通过那么我们就可以开始进行下一步操作了。
读取剪贴板数据
首先是readText方法,这个使用起来会很简单。
navigator.clipboard.readText().then((res) => {
console.log(res);
});
res就是读取到的剪贴板的文本。
我们也可以使用功能更强大的read方法来获取数据:
navigator.clipboard.read().then((clipboardItems) => {
clipboardItems.forEach((clipboardItem) => {
clipboardItem.types.forEach(async (type) => {
// 有时会有两个类型 text/plain和text/html
// 具体原因暂不清楚
const blob = await clipboardItem.getType(type);
if (type === "text/plain" || type === "text/html") {
// if (type === "text/plain") {
setIsImg(false);
var reader = new FileReader();
reader.readAsText(blob, "utf-8");
reader.onload = function (e) {
console.info(reader.result);
setText(reader.result);
};
}
if (type === "image/png") {
setIsImg(true);
setImgSrc(URL.createObjectURL(blob));
}
});
});
});
首先我们通过read方法得到一个promise,然后再then方法中我们可以拿到一个数组,这个数组只有一项,所以第一个遍历其实不太需要完全可以写成clipboardItems[0]的形式。我们拿到这个clipboardItems数组中的clipboardItem后可以看到这个对象是有一个types属性的,这个属性复制文字的时候有时候会有text/plain和text/html两个属性,有时又只有text/plain,具体原因暂不清楚。
然后我们可以使用clipboardItem对象的getType方法获取到bolb值,在上面的代码中我还使用了一个state——isImg来判断读取到的值是不是图片,如果是文本类型我们可以使用FileReader来将得到的blob转成string。如果是图片类型则会使用URL.createObjectURL将blob转成一个url同时设置给img的src属性,这样就可以显示图片了。
复制数据到剪贴板
复制数据到剪贴板其实和上面也是大同小异,具体使用看如下例子
首先还是最基本的文本:
navigator.clipboard.writeText("<empty clipboard>").then(
() => {
console.log("粘贴文本成功");
},
() => {
console.log("clipboard write failed");
}
);
只需要将你要复制的文本传入该方法即可,如果成功则会进入resolve,如果失败则是reject。
然后就是我们的重头戏了,如何使用write方法复制图片到剪贴板,我们还是看代码
const imgURL = "https://dummyimage.com/300.png";
const data = await fetch(imgURL);
const blob = await data.blob();
await navigator.clipboard.write([
new window.ClipboardItem({
[blob.type]: blob,
}),
]);
这里我使用的是阮一峰老师博客的例子,首先我们通过请求拿到了图片数据,然后得到图片的blob类型数据。然后我们给write方法传入一个数组,这个数组的成员是一个对象,对象是属性名是类型,值是blob类型的数据。有一些细心的同学会发现这种格式很熟悉对不对,其实这个格式就是我们使用read方法返回的promise中resolve传递出来的值——clipboardItems。其实这里也可以写成then的形式,依然是成功了走resolve失败了走reject。
Tips: 这里使用ClipboardItem构造函数的时候需要使用window.的形式,否则编辑器会提示ClipboardItem未定义。
3.踩坑
其实在使用write方法的时候有个问题,如果我们点击write方法的文档可以看到官方给出的例子并不是我上面的写法。
function setClipboard(text) {
let data = new DataTransfer();
data.items.add("text/plain", text);
navigator.clipboard.write(data).then(function() {
/* success */
}, function() {
/* failure */
});
}
这种方法经过我试验是不可行的,在使用的时候会报如下错误:
很明显write方法的参数是需要有迭代器接口的,而DataTransfer实例并没有这个接口。这里我也顺便提一下DataTransfer,它本来是拖拽时间对象下的一个属性,用于存放数据,如果做过拖拽排序等功能的朋友一定对它不陌生,这个对象在这里也是类似的功能——保存数据,但是因为write方法对参数的要求导致它并不可用,所以官网给出的例子是错误的。
二、React插件——copy-to-clipboard
这个插件本质上使用的是上面提到的execCommand方法,这一点我们可以从它的源码上看出来。
它的使用也是非常简单的
import copy from "copy-to-clipboard";
const flag = copy(123);
flag && console.log("复制成功")
三、如何实现静默复制
我们经常会见到这样一个现象,就是点击一个按钮一键复制号码,用户名等,那这些功能是怎么实现的呢,其实就是使用上面的API实现的。下面我将介绍这两种API的实现方式。
Clipboard
相信大家第一时间想到的API应该都是Clipboard了。没错,这个API是实现该方式的最佳选择,使用方便并且支持Promise,只需要用户授权后我们就可以很方便的实现功能。
// 首先我们使用上面的鉴权函数去鉴权
authentication.then(res=>{
//获取到授权后我们开始将指定文字复制到剪贴板
if(res) {
const userName = "张三";
navigator.clipboard.writeText(userName).then(()=>{
alert("复制用户名成功");
},()=>{
alert("复制用户名失败");
})
}
})
可以看到代码非常简洁使用起来也非常方便,所以这种方式是非常适合我们的。但是,这种方式有一个最大的特点,那就是Clipboard在生产环境中时,必须使用https协议才可以使用。如此看来Clipboard其实也不是很方便,因为目前还是用很多开发者和公司都时用的http协议,所以这个时候我们就得使用老方法了。
execCommand
execCommand方法的优缺点上面以及提到了一些,如果我们选择execCommand来做静默复制其实没那么方便。因为execCommand需要将复制的值选中然后再调用execCommand方法去复制,这样看起来还是需要用户交互。那有没有什么办法可以不需要交互就可以直接复制呢?当然有,我们需要借助js的dom来实现,话不多啥上代码:
// 可以使用上面的鉴权方法来判断该使用哪个API,这里不多赘述只展示execCommand的用法
// 创建一个输入框元素
const inputElement = document.createElement('input');
// 更新input的value
inputElement.value = userName;
// 随便找个父级容器 建议找一个层级较深的DOM元素,避免过多的重绘
const dom = document.querySelector('.content');
// 将创建的input添加到容器中
dom.appendChild(inputElement);
// 使用select方法将值选中
inputElement.select();
// 调用copy方法复制内容
const flag = document.execCommand('copy');
// 将输入框的值隐藏
inputElement.style.display = 'none';
// 根据返回值判断是否返回成功
flag?message.success('复制链接成功'):message.err('复制链接失败');
// 删除生成的input元素
inputElement.remove();
这种方式其实是从下载文件上找到的灵感,在使用的时候需要注意的是,可不要想着不将元素添加到DOM中就调用select方法进行复制,这样是没有效果的哦,哪怕是在复制之前将元素设置为display:none也是不行的。不过需要注意的是这个方法是一个同步的方法,如果复制的内容过多会导致卡顿,所以大家在使用的时候需要注意一下。