一键复制:使用JS实现的最佳方法

发表时间: 2023-05-26 09:44

写在前面

在IM聊天对话窗口、网页代码编译器或代码块展示等场景中,为了方便用户快速获取内容,往往会提供一个“一键复制”的功能。


比如知音楼右键消息的复制/网页编译器代码内容右键的复制/某论坛对于代码的一键复制按钮:



目前Web API提供过两种方法,支持js复制网页中目标内容(操作剪贴板),有先进的有即将废弃的。由于比较新的方法容易引起兼容性问题,所以旧方法濒临废弃但不能完全舍弃。


下面将会详细介绍一下这两种方法,并分别针对文本复制和图片复制,详细介绍我经多次尝试总结的兼容性方法。


API梳理

navigator.clipboard.write系列

相关MDN外链地址

https://developer.mozilla.org/zh-CN/docs/Web/API/Clipboard_API

(请复制链接到浏览器中打开)


兼容性



上图可观,在较新浏览器版本下,仅火狐的安卓端完全不支持Clipboard API提供的写入方法。但要注意的是,由于这两个方法涉及用户权限获取,一些客户端提供的WebView通常会直接禁掉对它们的使用,这种情况虽与浏览器的版本关系不大,但这些方法也无法正常使用。


使用方式


Clipboard API提供了两种将不同类型的内容写入剪贴板的方法:


writeText()

从方法名可以看出,writeText()方法仅支持写入文本,无其他数据类型的需求可直接使用该方法。

navigator.clipboard.writeText("想复制的文本作为参数")  .then(() => {    /* 剪贴板写入成功 */  }, () => {    /* 剪贴板写入失败 */  });

write()

write()方法支持写入任意格式的数据至剪贴板,但传参的数据需要经过特殊处理,参考MDN文档有提供一些方法。


对于
图片数据,常用的处理方法是利用Clipboard API提供的ClipboardItem接口,以MIME类型为key和blob为value的对象作为其参数,new一个相关实例,再作为write()方法的参数,写入剪贴板。

// 已准备好图片blob数据后 执行navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })])  .then(() => {    /* 剪贴板写入成功 */  }, () => {    /* 剪贴板写入失败 */  });


需要注意的是以上两种方法均为异步执行,执行成功与失败都有相应的回调。


介绍完复制要用的基础API,下面分别详细介绍下复制文本内容和图片内容的具体方法。
以下,推荐方法基于Clipboard API,向下兼容方法基于 document.execCommand(‘copy’)。


document.execCommand(‘copy’)

相关MDN外链地址

https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand

(请复制链接到浏览器中打开)


兼容性


document.execCommand(‘copy’)向document提供的execCommand方法传参"copy"所得的方法,该方法已被官方标记为弃用,较老版本的浏览器仍支持,可能在未来某版本中就会将它完全抹掉。



因此该方法仅适合在确定需要兼容不支持新方法的浏览器/WebView中使用。

验证目标浏览器是否支持该方法:
打开控制台输入document.execCommand(‘copy’)后回车,返回true就是支持,false就是不支持。

题外话:
读取用户剪贴板内容由于严重涉及到用户隐私,且使用

document.execCommand(‘paste’)粘贴没有一个明确获取用户权限的过程,该方法目前已完全被浏览器禁掉,因此在控制台输入它后返回的是false。



使用方式

基于Clipboard API的方法是通过将目标内容作为参数,调用方法后将内容拷贝到剪贴板。



但从文档得出,

document.execCommand(‘copy’)方法拷贝的是当前页面选中的内容,也就是拷贝用鼠标/触摸/调用js某些方法等方式在页面选中(不论是文本还是图片)的内容,并非使用传参的方式。

// 选中页面内容后执行document.execCommand('copy');


关于复制所用到的基础API已梳理完毕,下面将从推荐方法和向下兼容的方法两方面来分别详细说明一下复制文本和复制图片所需要的具体方法。


推荐方法


下面将分别介绍使用Clipboard API提供的writeText和write实现文本和图片复制的具体方法。


推荐方法适用于打开该页面的浏览器比较新且底层有提供权限的场景。


复制文本

没啥需要详细说明的,直接代码:

if(navigator.clipboard) { // 首先确定Clipboard API存在   navigator.clipboard.writeText("要复制的目标文本")    .then(() => {      /* 剪贴板写入成功 */    }, () => {      /* 剪贴板写入失败 */      // 可能存在navigator.clipboard存在 但无write权限的情况      // 可尝试用向下兼容的方法 在此重试复制操作 保障复制成功    });}

复制图片


复制图片相较文本稍复杂些,需要将图片数据预处理成write()方法可接收的数据。


常规思路和代码:


1. 图片转换成blob

const url = 'https://cos.ap-shanghai.myqcloud.com/96fb-shanghai-180-sharedv4-01-1303031839/15df-1400798275/5eb2-46648b08a1dc9f874767187454986dc0/7e25d051dd2cfebfb832023708295f96-292349.png?imageMogr2/'; // 一张示例图const image = await fetch(url);const blob = await image.blob();

2. 用ClipboardItem处理图片blob数据

const item = new ClipboardItem({ 'image/png': blob });

3. 执行write()方法复制

// 先确定Clipboard API存在navigator.clipboard.write([itemImage]).then(  () => {    /* 写入剪贴板成功 */  },  (error) =>{    /* 写入剪贴板失败 可尝试使用向下兼容方法重试 */  })

此处处理图片数据使用的是Clipboard API的ClipboardItem接口,但目前它只能处理png格式的图片,也就是blob类型为"image/png"的数据,如果用其他格式的图片,则会报错:



上述情况需要多处理一步,利用canvas统一不同格式的图片,异步转成blob,再复制:

/* 创建一个 Image 对象 */const img = new Image();img.src = url;img.crossOrigin = '';/* 监听图片加载 */img.onload = () => {  /* 创建一个Canvas对象 */  const canvas = document.createElement('canvas');  canvas.width = img.width;  canvas.height = img.height;  /* 在Canvas上绘制图片 */  const ctx = canvas.getContext('2d');  ctx.drawImage(img, 0, 0);  canvas.toBlob((blob) => { // 将Canvas转换为Blob对象 异步执行    /* 写入剪贴板 */    navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]).then(      () => {        /* 写入剪贴板成功 */      },      (error) => {        /* 写入剪贴板失败 可尝试使用向下兼容方法重试 */      }    )  }, img.type);}

题外话:

文档中提到,想要通过Clipboard API读取或更改剪贴板,需要用户授予权限才能成功,事实上使用时会发现,writeText和write方法在浏览器本身未禁止提供权限的情况下,不需再额外请求用户权限就可以正常使用;而用来粘贴的readText和read方法,由于有需要读取用户剪贴板这种极度隐私的行为,因此会在(以Chrome为例)左上角弹窗请求用户权限,如果用户未响应或禁止,那么此方法下的粘贴行为不会成功。


向下兼容


下面将分别介绍使用document.execCommand (‘copy’)实现文本和图片复制的具体方法。


由于目前仍存在很多不支持Clipboard API的环境,因此向下兼容是不可或缺的。


复制文本

上文有提到,古老的复制方法要基于页面选中的内容,而一键复制,即用户无感知复制过程,仅需要一个已成功拷贝到剪贴板的结果。


因此就不能手动选中了,可以用Web API提供的select()方法(文本专用)选中复制目标内容,再调用复制方法进行拷贝操作。


已知select()方法仅在input类型的元素中提供,也就是如文档所说的或type值为text的<input>。</span>


思路与代码:


1. 创建临时textarea元素,选中目标内容

/* 定义要复制的目标字符串 */const text = '目标内容';/* 选中内容 */const txa = document.createElement('textarea');txa.innerHTML = text;document.body.appendChild(txa);txa.select(); // 调用select()选中文本内容

2. 复制,后移除临时textarea元素

/* 拷贝操作 */document.execCommand('copy'); // 到此为止 复制操作完成/* 移除临时textarea */document.body.removeChild(txa);

复制图片

由于选中页面文本使用select()方法仅支持input类型元素,因此无法实现选中图片。


这就需要请出Web API提供的
getSelection()和createRange()方法,可通过设置当前页面的选中范围并获取这个范围,来达到获取选中图片的效果,其本质是选中一个DOM中的图片结点。


思路与代码:


1. 新建Selection对象创建选区,获取图片结点
HTML部分(适用于要复制的图片已经存在于DOM):

<img id="img1" src="https://cos.ap-shanghai.myqcloud.com/96fb-shanghai-180-sharedv4-01-1303031839/15df-1400798275/5eb2-46648b08a1dc9f874767187454986dc0/7e25d051dd2cfebfb832023708295f96-292349.png?imageMogr2/" />

注意:直接使用已经存在于DOM的图片结点,会产生一个小问题,就是如果对该图片有特殊的样式设置,比如设置了于原图不同的宽高或者设置了圆角,那么复制出来的图片也会保留这些样式。


因此如果确定使用这种方式,那可以在图片外边包一层来设置样式,别把个性化样式写在img上就可以了。


如果需求中要复制的图片当前不存在于DOM中,则同样可以创建一个临时的imgNode,完事再给它remove掉。

const imgNode = document.createElement('img');imgNode.src = 'https://cos.ap-shanghai.myqcloud.com/96fb-shanghai-180-sharedv4-01-1303031839/15df-1400798275/5eb2-46648b08a1dc9f874767187454986dc0/7e25d051dd2cfebfb832023708295f96-292349.png?imageMogr2/';document.body.appendChild(imgNode);

JS部分:

const sel = window.getSelection(); // 新建选区const imgNode = document.getElementById('img1'); // 获取图片结点

2. 新建Range对象,将Range设置为包含整个图片结点及其内容,并向选区中添加Range对象

sel.removeAllRanges(); // 清除当前页面所有已选中范围:会导致手动选中的内容也被取消const range = document.createRange(); // 新建范围range.selectNode(imgNode); // 将范围包含整个图片结点内容sel.addRange(range); // 向选中中添加该范围

3. 复制,后移除选区中的Range

document.execCommand('copy'); // 复制sel.removeAllRanges(); // 清除已选中范围 若无这一步 复制后图片将仍处于选中状态

题外话:用向下兼容的方式复制出来的图片在粘贴时可能不如推荐方法复制的图片适应性好,因为它本质复制的是’text/html’格式的内容,而非’image/*'格式,因此想要完美粘贴图片,对于你目标粘贴的位置 对这种格式的数据的解析也有一定要求。


当然我有实现一个支持粘贴’text/html’格式的可编辑框,不过它的实现方式不在本文讨论范围内。


关于该方法一个需要注意的点:在执行document.execCommand(‘copy’)前,需要在当前页面有过至少一次点击操作,也就是如果页面加载完成后立即自动调用,复制操作会无效。无相关文档考证,猜测这是浏览器一个类似于禁止用户无操作情况下就自动播放音频的保护机制。


到此所有方法及其细节介绍完毕。


小结

以上就是本次要介绍的关于“使用js实现一键复制”的全部内容,感谢您看到这里。


其实目前有了chatGPT,好像本文这类总结显得不那么重要了,因为只要调整好你的问题,chatGPT可能就会给一个参考性非常强的答案。我也拿着本文想要讨论的问题问了chatGPT,果然它首先就推荐了本文用到的基于Clipboard API的方法,再深入问一下兼容方案,它也开始提到 document.execCommand(‘copy’)方法,不过按照它的步骤走一遍,好像并不是完全无语法错误的答案,更不符合我本次的要求。


作者:牛思佳

来源:微信公众号:好未来技术

出处
:https://mp.weixin.qq.com/s/gTCiWGNwfIrgykpSJW69hA