# Clipboard操作

剪切板,有时候也叫剪贴板,一个意思。浏览器允许js脚本读写剪切板, 自动复制或者粘贴内容,比如在实现一些一键复制时, 点击复制按钮, 就可以指定内容复制进剪切板。

实现剪切板的操作有这么几种

# 1、Document.execCommand()

这是操作剪切板的传统方法,各种浏览器都支持

它支持复制、剪切、粘贴这三个操作

document.execCommand('copy')
document.execCommand('cut')
document.execCommand('paste')
<body>
  <input type="text" id="text_input" value="1234567890" />
  <input type="text">
  <button onclick="handleCopy()">copy</button>
  <script type="text/javascript">
    const input = document.querySelector('#text_input');
    function handleCopy() {
      input.select();
      document.execCommand('copy');
    }
  </script>
</body>

复制操作最好放在由用户手动触发的回调里面,不然可能会失败。

function handlePaste() {
  text_output.focus();
  document.execCommand('paste');
}

但是这个paste操作在比如Chrome这类浏览器中时不允许调用触发的。

# 缺点

Document.execCommand()方法虽然方便,但是有一些缺点。

首先,它只能将选中的内容复制到剪贴板,无法向剪贴板任意写入内容。

其次,它是同步操作,如果复制/粘贴大量数据,页面会出现卡顿。有些浏览器还会跳出提示框,要求用户许可,这时在用户做出选择前,页面会失去响应。

为了解决这些问题,浏览器厂商提出了异步的 Clipboard API。

还有就是上面提到的, 某些浏览器不允许 paste调用。

# 2、异步ClipboardAPI

异步ClipboardAPI是下一代的剪切板操作方法 比传统的 execCommand()方法更强大、更合理。它的所有操作都是异步的, 返回Promise对象, 不会造成页面卡顿。 而且, 他可以将任何内容(比如图片)放入剪切板

navigator.clipboard只读属性返回Clipboard对象, 所有的操作都通过这个对象进行。

const clipboard = navigator.clipboard

如果返回undefined, 那么说明当前浏览器不支持这个API。

由于用户可能把敏感数据(比如密码)放在剪切板, 允许脚本任意读取会产生安全风险, 所以这个API的安全限制比较多。

首先,Chrome 浏览器规定,只有 HTTPS 协议的页面才能使用这个 API。不过,开发环境(localhost)允许使用非加密协议。

其次, 调用时需要明确获得用户的许可。 权限的具体实现使用了Permissions API, 和剪切板相关的有两个权限, clipboard-read,clipboard-write。写权限(复制内容到剪切板)自动授予脚本, 而读权限(读取剪切板内容)必须用户明确同意给予。

# 3、Clipboard对象

Clipboard对象提供了4个方法, 用来读写剪切板。 他们都是异步方法, 返回Promise对象。

# 3.1 Clipboard.readText()

Clipboard.readText() 方法用于复制剪切板里面的文本数据

<button onclick="readTextFromClipboard()">read text from clipboard</button>
<script>
  function readTextFromClipboard(e) {
    try {
      navigator.clipboard.readText().then(res => {
        const p = document.createElement('p');
        p.textContent = res;
        document.body.appendChild(p);
      });
    } catch (error) {
      console.error(`failed to read content from clipboard, ${error}`);
    }
  }
</script>

上面代码中, 当用户点击按钮之后, 就会输出剪切板里面的文本。如果没有授权过, 浏览器还会弹出这样子一个对话框,

Chrome Demo

询问用户是否同意脚本读取剪切板。如果用户不同意, 那么就会走入到catch里面, 捕获错误。

# 3.2 Clipboard.read()

Clipboard.read() 方法用于复制剪切板里面的数据, 可以是文本数据, 也可以是二进制数据(比如图片), 这个方法也需要用户明确给予许可。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>read.html</title>
  </head>
  <body>
    <button onclick="getClipboardContents()">read</button>
    <script>
      async function getClipboardContents() {
        try {
          const clipboardItems = await navigator.clipboard.read();
          console.log(clipboardItems);
          for (const item of clipboardItems) {
            for (const type of item.types) {
              const blob = await item.getType(type);
              readBlobData(blob, type);
              console.log(blob);
              //   console.log(URL.createObjectURL(blob));
            }
          }
        } catch (error) {
          console.error(error);
        }
      }

      function readBlobData(blob, type) {
        let reader = new FileReader();
        reader.onloadend = function () {
          console.log(reader.result);
          const p = document.createElement('p');
          switch (type) {
            case 'text/plain':
              p.textContent = '读取结果: ' + reader.result;
              break;
            case 'text/html':
              p.innerHTML = reader.result;
              break;
            case 'image/png':
              const img = new Image();
              img.src = reader.result;
              p.appendChild(img);
              break;

            default:
              break;
          }

          document.body.appendChild(p);
          reader.abort();
          reader = null;
        };
        if (type === 'image/png') {
          reader.readAsDataURL(blob);
        } else {
          reader.readAsText(blob, 'UTF-8');
        }
      }
    </script>
  </body>
</html>


Clipboard.read() 返回一个Promise<ClipboardItems> , ClipboardItems 是包含ClipboardItem的一个数组集合,结构如下图

ClipboardItem.types 是剪切板里面的成员可用的MIME类型, 比如上面的内容, 可以时候纯文本粘贴, 也可以使用HTML格式粘贴, 所以有两个MIME类型, text/plaintext.html

ClipboardItem.getType(type) 用于读取剪切板的具体数据, 参数是对应的MIME类型, 返回该类型对应的数据, 数据格式一般是Blob。

我们可以看一下具体的效果, 我们随便复制一段代码

上面的就是纯文本的内容, 下面的就是html格式的内容(通过innerHTML插入的,他的内容就是带内联样式的html)。

我们借助 FileReader 读取到了Blob的内容, 具体只是可以查看我的另外一个文章Blob。🎉 😄

# 3.3 Clipboard.writeText()

Clipboard.writeText() 用于将内容写入剪切板。

function writeTextToClipboard(str) {
  try {
    navigator.clipboard.writeText('hello, world!');
  } catch (error) {
    console.error(error);
  }
}

然后就可以粘贴了

# 3.4 Clipboard.write()

Clipboard.write()方法用于将任意数据写入剪贴板,可以是文本数据,也可以是二进制数据。

该方法接受一个 ClipboardItem 实例作为参数,表示写入剪贴板的数据。建议查看示例代码 (opens new window)

async function handleWriteImageAndText() {
  try {
    const imgURL = 'https://dummyimage.com/300.png';
    const data = await fetch(imgURL);
    const blob = await data.blob();
    const textBlob = new Blob(['hello, world'], {
      type: 'text/plain'
    });
    const htmlBlob = new Blob(['<p style="color: red;">hello
      type: 'text/html'
    });
    await navigator.clipboard.write([
      new ClipboardItem({
        [blob.type]: blob,
        [textBlob.type]: textBlob,
        [htmlBlob.type]: htmlBlob,
      })
    ]);
    console.log('Image copied.');
  } catch (err) {
    console.error(err.name, err.message);
  }
}
async function handleWriteText() {
    try {
      if (!input_for_copy.value) return;
      const blob = new Blob([input_for_copy.value], { type: 'text/plain' });
      await navigator.clipboard.write([
        new ClipboardItem({
          [blob.type]: blob
        })
      ]);
      write_text.textContent = write_text.textContent + ' copy success!';
    } catch (err) {
      console.error(err.name, err.message);
    }
}

WARNING

注意,Chrome 浏览器目前只支持写入 PNG 格式的图片。

ClipboardItem()是浏览器原生提供的构造函数,用来生成ClipboardItem实例,它接受一个对象作为参数,该对象的键名是数据的 MIME 类型,键值就是数据本身。我们可以往一个 ClipboardItem 实例添加多种格式的内容, 提供给不同的长河

# 4、copy, cut事件

当用户向剪切板复制数据时, 将触发 copy 事件。

<div class="source">hello world</div>
<script>
  const source = document.querySelector('.source');
  // 选中source的文字然后按 ctrl + c 复制, 将触发copy事件
  // 这里是将复制的内容转为大写
  source.addEventListener('copy', e => {
    console.log(e);
    const selection = window.getSelection();
    e.clipboardData.setData(
      'text/plain',
      selection.toString().toUpperCase()
    );
    e.preventDefault();
  });
</script>

window.getSelection (opens new window)

示例中, 事件对象的 clipboardData 属性包含了剪切板数据,还有其他的一些比较实用的属性和方法。

  • Event.clipboardData.setData(type, data):修改剪贴板数据,需要指定数据类型。
  • Event.clipboardData.getData(type):获取剪贴板数据,需要指定数据类型。
  • Event.clipboardData.clearData([type]):清除剪贴板数据,可以指定数据类型。如果不指定类型,将清除所有类型的数据。
  • Event.clipboardData.items:一个类似数组的对象,包含了所有剪贴项,不过通常只有一个剪贴项。

再来一个拦截用户复制操作的的示例

document.addEventListener('copy', e => {
  e.preventDefault();
  // navigator.clipboard.writeText('hello, world!');
  const textBlob = new Blob(['嘿嘿'], {
    type: 'text/plain'
  });
  navigator.clipboard.write([
    new ClipboardItem({
      [textBlob.type]: textBlob
    })
  ]);
});	

当用户复制时,preventDefault 会阻止复制的默认操作,然后写入我们自定义的内容到剪切板。

cut事件则是在用户进行剪切操作时触发,它的处理跟copy事件完全一样,也是从Event.clipboardData属性拿到剪切的数据。

# 5、paste 事件

当用户ctrl + v 粘贴的时候会触发的事件,

举一个实用的例子, 比如不允许密码框复制输入密码

<body>
  <p>asdasdasdasd</p>
  <input type="text" />
  <input type="text" id="password" />
  <script>
    const psd = document.querySelector('#password');
    psd.addEventListener('paste', e => {
      e.preventDefault();
      console.log(e);
      navigator.clipboard.readText().then(res => {
        console.log(res);
      });
    });
    psd.addEventListener('copy', e => {
      console.log(e);
      e.preventDefault();
      e.stopPropagation();
    });
  </script>
</body>

e.preventDefault(); 会阻止复制输入密码,当然也可以防止密码被复制。

# 6、clipboard.js

文档 🔖 (opens new window)

npm i clipboard -S

大致看了眼源码,用的也是Document.execCommand() 这个解决方案。需要借助一个DOM 节点来实现。

用法不难, 看文档即可, 最简单实用的可能就是这个方法

new ClipboardJS('#dynamic_btn', {
  text: function (trigger) {
    console.log(trigger);
    return 'sss';
  }
});

如果需要动态设置, 还是给这个元素设置自定义属性, 然后读取这个属性, 模拟click即可。

# 7、总结

如果对浏览器版本适配要求不高, 还是使用 navigator.clipboard这个原生API比较实用。 当然clipboard.js 这个三方库的star数也不少, 用的人很多。

上次更新: 1/22/2025, 9:39:13 AM