JS 实现复制到剪贴板 | 七日打卡

1,722 阅读3分钟

前言

前阵子接到一个开发任务,是复制网页上的图片,并粘贴到的聊天工具中。这是一个很常见的操作,在日常工作中我们经常通过 QQ 截图、 Ctrl + C 将图片复制到聊天框。

github 上几个高 star 的 js 复制库,比如 clipboard.jscopy-to-clipboard 等,其实现原理都是选中元素之后,调用 document.exceCommand('copy') 这个 API 实现复制,但是通常只支持文本复制,而且 API 已经过时了。因此,我决定封装一个小巧的组件,在 React 中实现图片复制。 document.exceCommand mdn

接收剪贴板内容

虽然我们并不需要实现 剪贴板->粘贴 的过程,但是为了理清 复制->剪贴板 需要的数据,我们可以简单了解下输入框是如何接收数据的?接收的又是什么类型的数据?

  • 基本原理是通过监听 paste event,拦截到 clipboardData 的数据(注意 paste 事件只能在 contenteditable: true 的元素上触发)
  • 再通过 reader.readAsDataURL 实现生成预览图片,或者直接上传图片数据到后端
// 代码参考:https://cloud.tencent.com/document/product/269/37448
document
    .getElementById("testPasteInput")
    .addEventListener("paste", function (e) {
      let clipboardData = e.clipboardData;
      let file;
      if (
        clipboardData &&
        clipboardData.files &&
        clipboardData.files.length > 0
      ) {
        file = clipboardData.files[0];
        var reader = new FileReader();
        reader.onload = function (e) {
          var image = document.getElementById("result");
          image.src = e.target.result;
          image.style.display = "block";
        };
        reader.readAsDataURL(file);
      }
      if (typeof file === "undefined") {
        console.warn("file 是 undefined,请检查代码或浏览器兼容性!");
        return;
      }
    });

通过代码,我们了解到,我们在输入框通过监听事件接收到来自剪贴板的 File/Blob 类型的内容。

了解了消息接收的原理之后,我们要解决的就是如何将目标内容加到剪贴板中

目前可用的主要有两个 API:

  • excecommand
  • Clipboard API

使用 excecommand 复制内容到剪贴板

选中元素

使用 excecommand('copy') 的关键是,我们要实现选中元素的效果。说到选中,我们通常会想起 input 的 select() 事件,但并不是所有元素都有这个接口。一个更加通用的 API 是 Range API。我们可以利用这个 API 选中页面中的元素或者动态生成一个元素并选中。

function selectFakeInput(input: HTMLELEMENT) {
  let selection = window.getSelection();
  let range = document.createRange();
  range.selectNode(input);
  selection!.removeAllRanges();
  selection!.addRange(range);
  return () => {
    selection!.removeAllRanges();
  };
}

写入剪贴板

 let success = document.execCommand("copy");
 if (!success) {
 	throw Error("复制失败!");
 }

实践发现这个 API 实现文本复制的效果较好,图片复制通常会失败

使用 Clipboard API 复制内容到剪贴板

将目标内容转化为 Blob 类型

//text
function textToBlob(target: string = "") {
  return new Blob([target], { type: "text/plain" });
}
// image src(图片源需可跨域)
function imageToBlob(target: string = "") {
   const response = await fetch(target);
   return await response.blob();
}

询问浏览器是否支持

async function isSupportClipboardWrite() {
  try {
    const permission = await navigator.permissions?.query?.({
      //@ts-ignore
      name: "clipboard-write",
      allowWithoutGesture: false,
    });
    return permission?.state === "granted";
  } catch (error) {
    return false;
  }
}

写入剪贴板

const data = [new window.ClipboardItem({ [blob.type]: blob })];
await navigator.clipboard.write(data);

React 组件的封装

这里,我们尝试使用 hooks 封装。

export const useCopyImage = (props: ClipboardImageHooksProps) => {
  const [status, setStatus] = useState<ChangeStatus>(null);
  const [err, setErr] = useState<Error>(null);
  const copyImage = useCallback(async (target: ImageCopyTarget) => {
    try {
      setErr(null);
      setStatus("loading");
      const canWrite = await isSupportClipboardWrite();
      if (
        !canWrite ||
        !window.ClipboardItem ||
        !navigator.clipboard?.write
      ) {
        throw Error("broswer not supported!");
      }
      const blob = await imageToBlob(target);
      const data = [new window.ClipboardItem({ [blob.type]: blob })];
      await navigator.clipboard.write(data);
      setStatus("done");
    } catch (err) {
      setStatus("error");
      setErr(err);
    }
  }, []);
  return { status, error: err, copy:copyImage };
};

封装后可直接使用

import { useCopyText } from "rc-clipboard-copy";
const App = () => {
  const { copy, error, status } = useCopyText({});
  return (
    <div>
      <button onClick={() => copy("hello word 2")}>copy text</button>
    </div>
  );
};

总结

详细的实现见 github 仓库

参考资料