阅读 3413

想要复制图像?Clipboard API 了解一下

在写了 这个 29.7 K 的剪贴板 JS 库有点东西! 这篇文章之后,收到了小伙伴提的两个问题:

1.clipboard.js 这个库除了复制文字之外,能复制图像么?

2.clipboard.js 这个库依赖的 document.execCommand API 已被废弃了,以后应该怎么办?

(图片来源:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand)

接下来,本文将围绕上述两个问题展开,不过在看第一个问题之前,我们先来简单介绍一下 剪贴板 📋。

剪贴板(英语:clipboard),有时也称剪切板、剪贴簿、剪贴本。它是一种软件功能,通常由操作系统提供,作用是使用复制和粘贴操作短期存储数据和在文档或应用程序间转移数据。它是图形用户界面(GUI)环境中最常用的功能之一,通常实现为匿名、临时的数据缓冲区,可以被环境内的大部分或所有程序使用编程接口访问。 —— 维基百科

通过以上的描述我们可以知道,剪贴板架起了一座桥梁,使得在各种应用程序之间,传递和共享信息成为可能。然而美中不足的是,剪贴板只能保留一份数据,每当新的数据传入,旧的便会被覆盖。

了解完 剪贴板 📋 的概念和作用之后,我们马上来看一下第一个问题:clipboard.js 这个库除了复制文字之外,能复制图像么?

关注「全栈修仙之路」阅读阿宝哥原创的 4 本免费电子书(累计下载 2.1万+)及 50 几篇 “重学TS” 教程。

一、clipboard.js 能否复制图像?

clipboard.js 是一个用于将 文本 复制到剪贴板的 JS 库。没有使用 Flash,没有使用任何框架,开启 gzipped 压缩后仅仅只有 3kb

(图片来源:https://clipboardjs.com/#example-text)

当你看到 “A modern approach to copy text to clipboard” 这个描述,你是不是已经知道答案了。那么实际的情况是怎样呢?下面我们来动手验证一下。在 这个 29.7 K 的剪贴板 JS 库有点东西! 这篇文章中,阿宝哥介绍了在实例化 ClipboardJS 对象时,可以通过 options 对象的 target 属性来设置复制的目标:

// https://github.com/zenorocha/clipboard.js/blob/master/demo/function-target.html
let clipboard = new ClipboardJS('.btn', {
  target: function() {
    return document.querySelector('div');
  }
});
复制代码

利用 clipboard.js 的这个特性,我们可以定义以下 HTML 结构:

<div id="container">
   <img src="http://cdn.semlinker.com/abao.png" width="80" height="80"/>
   <p>大家好,我是阿宝哥</p>
</div>
<button class="btn">复制</button>
复制代码

然后在实例化 ClipboardJS 对象时设置复制的目标是 #container 元素:

const clipboard = new ClipboardJS(".btn", {
  target: function () {
    return document.querySelector("#container");
  }
});
复制代码

之后,我们点击页面中的 复制 按钮,对应的效果如下图所示:

观察上图可知,页面中的图像和文本都已经被复制了。对于文本来说,大家应该都很清楚。而对于图像来说,到底复制了什么?我们又该如何获取已复制的内容呢?针对这个问题,我们可以利用 HTMLElement 对象上的 onpaste 属性或者监听元素上的 paste 事件。

这里我们通过设置 document 对象的 onpaste 属性,来打印一下粘贴事件对应的事件对象:

document.onpaste = function (e) {
  console.dir(e);
}
复制代码

当我们点击 复制 按钮,然后在页面执行 粘贴 操作后,控制台会打印出以下内容:

通过上图可知,在 ClipboardEvent 对象中含有一个 clipboardData 属性,该属性包含了与剪贴板相关联的数据。详细分析了 clipboardData 属性之后,我们发现已复制的图像和普通文本被封装为 DataTransferItem 对象。

为了更方便地分析 DataTransferItem 对象,阿宝哥重新更新了 document 对象的 onpaste 属性:

在上图中,我们可以清楚的看到 DataTransferItem 对象上含有 kindtype 属性分别用于表示数据项的类型(string 或 file)及数据对应的 MIME 类型。利用 DataTransferItem 对象提供的 getAsString 方法,我们可以获取该对象中保存的数据:

相信看完以上的输出结果,小伙伴们就很清楚第一个问题的答案了。那么如果想要复制图像的话,应该如何实现呢?其实这个问题的答案与小伙伴提的第二个问题的答案是一样的,我们可以利用 Clipboard API 来实现复制图像的问题及解决 document.execCommand API 已被废弃的问题。

接下来,我们的目标就是实现复制图像的功能了,因为要利用到 Clipboard API,所以阿宝哥先来介绍一下该 API。

二、Clipboard API 简介

Clipboard 接口实现了 Clipboard API,如果用户授予了相应的权限,就能提供系统剪贴板的读写访问。在 Web 应用程序中,Clipboard API 可用于实现剪切、复制和粘贴功能。该 API 用于取代通过 document.execCommand API 来实现剪贴板的操作。

在实际项目中,我们不需要手动创建 Clipboard 对象,而是通过 navigator.clipboard 来获取 Clipboard 对象:

在获取 Clipboard 对象之后,我们就可以利用该对象提供的 API 来访问剪贴板,比如:

navigator.clipboard.readText().then(
  clipText => document.querySelector(".editor").innerText = clipText);
复制代码

以上代码将 HTML 中含有 .editor 类的第一个元素的内容替换为剪贴板的内容。如果剪贴板为空,或者不包含任何文本,则元素的内容将被清空。这是因为在剪贴板为空或者不包含文本时,readText 方法会返回一个空字符串。

在继续介绍 Clipboard API 之前,我们先来看一下 Navigator API: clipboard 的兼容性:

(图片来源:https://caniuse.com/mdn-api_navigator_clipboard)

异步剪贴板 API 是一个相对较新的 API,浏览器仍在逐渐实现它。由于潜在的安全问题和技术复杂性,大多数浏览器正在逐步集成这个 API。对于浏览器扩展来说,你可以请求 clipboardRead 和 clipboardWrite 权限以使用 clipboard.readText() 和 clipboard.writeText()。

好的,接下来阿宝哥来演示一下如何使用 clipboard 对象提供的 API 来操作剪贴板,以下示例的运行环境是 Chrome 87.0.4280.88

三、将数据写入到剪贴板

3.1 writeText()

writeText 方法可以把指定的字符串写入到系统的剪贴板中,调用该方法后会返回一个 Promise 对象:

<button onclick="copyPageUrl()">拷贝当前页面地址</button>
<script>
   async function copyPageUrl() {
     try {
       await navigator.clipboard.writeText(location.href);
       console.log("页面地址已经被拷贝到剪贴板中");
     } catch (err) {
       console.error("页面地址拷贝失败: ", err);
     }
  }
</script>
复制代码

对于上述代码,当用户点击 拷贝当前页面地址 按钮时,将会把当前的页面地址拷贝到剪贴板中。

3.2 write()

write 方法除了支持文本数据之外,还支持将图像数据写入到剪贴板,调用该方法后会返回一个 Promise 对象。

<button onclick="copyPageUrl()">拷贝当前页面地址</button>
<script>
   async function copyPageUrl() {
     const text = new Blob([location.href], {type: 'text/plain'});
     try {
       await navigator.clipboard.write(
         new ClipboardItem({
           "text/plain": text,
         }),
       );
       console.log("页面地址已经被拷贝到剪贴板中");
     } catch (err) {
       console.error("页面地址拷贝失败: ", err);
     }
  }
</script>
复制代码

在以上代码中,我们先通过 Blob API 创建 Blob 对象,然后使用该 Blob 对象来构造 ClipboardItem 对象,最后再通过 write 方法把数据写入到剪贴板。介绍完如何将数据写入到剪贴板,下面我们来介绍如何从剪贴板中读取数据。

对 Blob API 感兴趣的小伙伴,可以阅读 你不知道的 Blob 这篇文章。

四、从剪贴板中读取数据

4.1 readText()

readText 方法用于读取剪贴板中的文本内容,调用该方法后会返回一个 Promise 对象:

<button onclick="getClipboardContents()">读取剪贴板中的文本</button>
<script>
   async function getClipboardContents() {
     try {
       const text = await navigator.clipboard.readText();
       console.log("已读取剪贴板中的内容:", text);
     } catch (err) {
       console.error("读取剪贴板内容失败: ", err);
     }
   }
</script>
复制代码

对于上述代码,当用户点击 读取剪贴板中的文本 按钮时,如果当前剪贴板含有文本内容,则会读取剪贴板中的文本内容。

4.2 read()

read 方法除了支持读取文本数据之外,还支持读取剪贴板中的图像数据,调用该方法后会返回一个 Promise 对象:

<button onclick="getClipboardContents()">读取剪贴板中的内容</button>
<script>
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("已读取剪贴板中的内容:", await blob.text());
      }
    }
  } catch (err) {
      console.error("读取剪贴板内容失败: ", err);
    }
  }
</script>
复制代码

对于上述代码,当用户点击 读取剪贴板中的内容 按钮时,则会开始读取剪贴板中的内容。到这里 clipboard 对象中涉及的 4 个 API,阿宝哥都已经介绍完了,最后我们来看一下如何实现复制图像的功能。

五、实现复制图像的功能

在最后的这个示例中,阿宝哥将跟大家一步步实现复制图像的核心功能,除了复制图像之外,还会同时支持复制文本。在看具体代码前,我们先来看一下实际的效果:

在上图对应的网页中,我们先点击 复制 按钮,则图像和文本都会被选中。之后,我们在点击 粘贴 按钮,则控制台会输出从剪贴板中读取的实际内容。在分析具体的实现方式前,我们先来看一下对应的页面结构:

<div id="container">
   <img src="http://cdn.semlinker.com/abao.png" width="80" height="80" />
   <p>大家好,我是阿宝哥</p>
</div>
<button onclick="writeDataToClipboard()">复制</button>
<button onclick="readDataFromClipboard()">粘贴</button>
复制代码

上面的页面结构很简单,下一步我们来逐步分析一下以上功能的实现过程。

5.1 请求剪贴板写权限

默认情况下,会为当前的激活的页面自动授予剪贴板的写入权限。出于安全方面考虑,这里我们还是主动向用户请求剪贴板的写入权限:

async function askWritePermission() {
  try {
    const { state } = await navigator.permissions.query({
      name: "clipboard-write",
    });
      return state === "granted";
  } catch (error) {
      return false;
  }
}
复制代码

5.2 往剪贴板写入图像和普通文本数据

要往剪贴板写入图像数据,我们就需要使用 navigator.clipboard 对象提供的 write 方法。如果要写入图像数据,我们就需要获取该图像对应的 Blob 对象,这里我们可以通过 fetch API 从网络上获取图像对应的响应对象并把它转化成 Blob 对象,具体实现方式如下:

async function createImageBlob(url) {
  const response = await fetch(url);
  return await response.blob();
}
复制代码

而对于普通文本来说,只需要使用前面介绍的 Blob API 就可以把普通文本转换为 Blob 对象:

function createTextBlob(text) {
  return new Blob([text], { type: "text/plain" });
}
复制代码

在创建完图像和普通文本对应的 Blob 对象之后,我们就可以利用它们来创建 ClipboardItem 对象,然后再调用 write 方法把这些数据写入到剪贴板中,对应的代码如下所示:

async function writeDataToClipboard() {
  if (askWritePermission()) {
    if (navigator.clipboard && navigator.clipboard.write) {
        const textBlob = createTextBlob("大家好,我是阿宝哥");
        const imageBlob = await createImageBlob(
          "http://cdn.semlinker.com/abao.png"
        );
        try {
          const item = new ClipboardItem({
            [textBlob.type]: textBlob,
            [imageBlob.type]: imageBlob,
          });
          select(document.querySelector("#container"));
          await navigator.clipboard.write([item]);
          console.log("文本和图像复制成功");
        } catch (error) {
          console.error("文本和图像复制失败", error);
        }
      }
   }
}
复制代码

在以上代码中,使用了一个 select 方法,该方法用于实现选择的效果,对应的代码如下所示:

function select(element) {
  const selection = window.getSelection();
  const range = document.createRange();
  range.selectNodeContents(element);
  selection.removeAllRanges();
  selection.addRange(range);
}
复制代码

通过 writeDataToClipboard 方法,我们已经把图像和普通文本数据写入剪贴板了。下面我们来使用 navigator.clipboard 对象提供的 read 方法,来读取已写入的数据。如果你需要读取剪贴板的数据,则需要向用户请求 clipboard-read 权限。

5.3 请求剪贴板读取权限

这里我们定义了一个 askReadPermission 函数来向用户请求剪贴板读取权限:

async function askReadPermission() {
  try {
    const { state } = await navigator.permissions.query({
      name: "clipboard-read",
    });
    return state === "granted";
  } catch (error) {
    return false;
  }
}
复制代码

当调用 askReadPermission 方法后,将会向当前用户请求剪贴板读取权限,对应的效果如下图所示:

5.4 读取剪贴板中已写入的数据

创建好 askReadPermission 函数,我们就可以利用之前介绍的 navigator.clipboard.read 方法来读取剪贴板的数据了:

async function readDataFromClipboard() {
  if (askReadPermission()) {
    if (navigator.clipboard && navigator.clipboard.read) {
      try {
        const clipboardItems = await navigator.clipboard.read();
        for (const clipboardItem of clipboardItems) {
          console.dir(clipboardItem);
          for (const type of clipboardItem.types) {
            const blob = await clipboardItem.getType(type);
            console.log("已读取剪贴板中的内容:", await blob.text());
          }
        }
      } catch (err) {
         console.error("读取剪贴板内容失败: ", err);
      }
     }
   }
}
复制代码

其实,除了点击 粘贴 按钮之外,我们还可以通过监听 paste 事件来读取剪贴板中的数据。需要注意的是,如果当前的浏览器不支持异步 Clipboard API,我们可以通过 clipboardData.getData 方法来读取剪贴板中的文本数据:

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('已获取的文本数据: ', text);
});
复制代码

而对于图像数据,则可以通过以下方式进行读取:

const IMAGE_MIME_REGEX = /^image\/(p?jpeg|gif|png)$/i;

document.addEventListener("paste", async (e) => {
  e.preventDefault();
  if (navigator.clipboard) {
    let clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
       for (const type of clipboardItem.types) {
         if (IMAGE_MIME_REGEX.test(type)) {
           const blob = await clipboardItem.getType(type);
           loadImage(blob);
           return;
         }
        }
     }
   } else {
       const items = e.clipboardData.items;
       for (let i = 0; i < items.length; i++) {
         if (IMAGE_MIME_REGEX.test(items[i].type)) {
         loadImage(items[i].getAsFile());
         return;
       }
    }
  }
});
复制代码

以上代码中的 loadImage 方法用于实现把复制的图片插入到当前选区已选择的区域中,对应的代码如下:

function loadImage(file) {
  const reader = new FileReader();
  reader.onload = function (e) {
    let img = document.createElement("img");
    img.src = e.target.result;

    let range = window.getSelection().getRangeAt(0);
    range.deleteContents();
    range.insertNode(img);
  };
  reader.readAsDataURL(file);
}
复制代码

在前面代码中,我们监听了 document 对象的 paste 事件。除了该事件之外,与剪贴板相关的常见事件还有 copycut 事件。篇幅有限,阿宝哥就不继续展开介绍了,感兴趣的小伙伴可以自行阅读相关资料。好的,至此本文就已经结束了,希望阅读完本文之后,大家对异步的 Clipboard API 会有些了解,有写得不清楚的地方,欢迎你随时跟阿宝哥交流哟。

关注「全栈修仙之路」阅读阿宝哥原创的 4 本免费电子书(累计下载2.1万+)及 9 篇源码分析系列教程。

六、参考资源