js复制内容到剪贴板

9,574 阅读9分钟

复制内容到剪贴板

平时我们在浏览一些网站的时候会看到有一键复制的功能,这个功能极大的提升了用户的体验,但是大部分一键复制的功能都是复制的文本,或许 项目经理甲方他突发奇想,欸~为什么我在浏览器可以右键复制图片到剪贴板,你能不能做一个这个功能呢?虽然甲方很讨厌,但是程序员绝不能轻易的说不!经过我不断的copy修改查阅,总算是将其做出来了,下面我会以React插件和原生API两中方法去说明。

一、原生API

首先给大家说一下原生API的实现,原生的API主要是以下两种:

  1. document.execCommand
  2. Clipboard

这里我会分别解释这两种API的用法以及优缺点。

1.document.execCommand

这个方法是一个老方法了,很多复制的插件都是使用的这个API,这个插件最大的优点在于使用方便并且兼容性比较好,但是很明显也是他的缺点,我们可以在MDN的官网上看到如下提示:

image-20220628173115888.png 很明显它已是垂暮之年,很快就要退休了,不过我们还是会对它的用法做一些介绍。

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非常强大,除了复制文本也可以复制其他类型的数据,按理来说其他二进制好像也可以,但是我只尝试过图片。这个对象下面有四个方法,分别是:

  1. read()从剪贴板读取数据(比如图片),返回一个 Promise 对象。在检索到数据后,promise 将兑现一个 ClipboardItem 对象的数组来提供剪切板数据。
  2. readText()从操作系统读取文本;返回一个 Promise,在从剪切板中检索到文本后,promise 将兑现一个包含剪切板文本数据的 DOMString
  3. write()写入任意数据至操作系统剪贴板。这是一个异步操作,在操作完成后,返回的 promise 的将被兑现。
  4. 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/plaintext/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 */
  });
}

这种方法经过我试验是不可行的,在使用的时候会报如下错误:

image-20220628190106376.png

很明显write方法的参数是需要有迭代器接口的,而DataTransfer实例并没有这个接口。这里我也顺便提一下DataTransfer,它本来是拖拽时间对象下的一个属性,用于存放数据,如果做过拖拽排序等功能的朋友一定对它不陌生,这个对象在这里也是类似的功能——保存数据,但是因为write方法对参数的要求导致它并不可用,所以官网给出的例子是错误的。

二、React插件——copy-to-clipboard

这个插件本质上使用的是上面提到的execCommand方法,这一点我们可以从它的源码上看出来。

image-20220628190807674.png

它的使用也是非常简单的

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也是不行的。不过需要注意的是这个方法是一个同步的方法,如果复制的内容过多会导致卡顿,所以大家在使用的时候需要注意一下。