七谴 发表于 2023-8-29 11:15:08

前端实现复制文字和图片,原来这么简单!

1.功能需求

实习工作中,遇到一个需求,需要完成<button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button>的功能,当文字过长的时候,让用户手拖再ctrl+c这种方式体验就不是很好了,如果可以点击一下直接复制就是一种不错的优化用户体验的方式。

经过查阅文档,网络上完成这个功能大多使用两大类方法
第一种是以document.execCommand() 方法为主,无论是手写还是使用clipboard.js插件都是依赖的这个方法,但是在MDN 文档中已经显示过时了。
第二种是用了navigator.clipboard的方法,避免了过时问题,但是在复制图片的时候会有一定的浏览器兼容性问题
 2.document.execCommand('copy') 

这个方法其实就是在模拟用户选择元素然后右键复制的动作。尽管MDN已经显示这个方法过时了,但是仅针对copy这个指令,大部分主流浏览器都可以支持,所以这个方法仍然可以作为一种实现问题的方案。
2.1 基本用法

根据MDN文档学习本方法的传参和返回值
语法

bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)这个方法可以传3个参数,并且会返回一个布尔值
返回值

先从返回值开始,返回值相对比较简单,如果返回的值是false就表示浏览器不支持使用这个操作,反之浏览器支持该操作就返回true。
虽然这个返回值看似可以用来提前判断浏览器兼容性,但是文档中不推荐在调用一个命令前,尝试使用返回值去校验浏览器的兼容性
参数值

参数一共可以传3个,但是使用复制命令的时候只需要传第一个参数就可以。这里简单介绍一下3个参数

[*]aCommandName:一个字符串类型的参数,是命令的名称,比如复制用到的copy,剪切用到的cut
[*]aShowDefaultUI:一个布尔类型的参数,表示是否展示用户界面,一般为false,Mozilla 没有实现
[*]aValueArgument:一些命令(例如 insertImage)需要额外的参数(insertImage 需要提供插入 image 的 url),默认为 null。
简单举例

以本文主要讲的复制命令为例子:document.execCommand('copy')
指令兼容性问题

前文讲到,MDN不推荐在调用一个命令前,尝试使用返回值去校验浏览器的兼容性,那么就需要用另外的方法去检测浏览器是否支持某个指令,浏览器为我们提供了一个方法叫document.queryCommandSupported(),使用这个方法可以检测浏览器是否支持某个指令,这个方法比较简单,只有1个参数,参数就传指令字符串,方法的返回值是一个布尔值表示当前浏览器是否支持这个指令。
举例如下:
 
    if(document.queryCommandSupported && document.queryCommandSupported('copy')){
      //先检测是否支持document.queryCommandSupported和copy指令
      //如果都支持直接执行指令
      document.execCommand('copy')
    } 
MDN文档中提到,document.queryCommandSupported也被弃用了,但是为了兼容性依然保留可用,当我们使用document.execCommand的时候仍然可以用document.queryCommandSupported来检测是否支持。同时,它的浏览器兼容性也是比较好的,大部分主流浏览器都支持。
 
2.2 Selection Api

复制文本这个操作对比复制图片是相对比较简单的,一共包含2大步
一是选中要复制的元素
二是执行复制指令。
执行复制指令在前面的基本语法里已经讲到了,直接调用document.execCommand('copy')就可以了。剩下要做的便是先选中元素了。下面便介绍一下和选中元素相关的selection api

MDN文档上写道:Selection 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。如果要获取用于检查或修改的 Selection 对象,可以调用 window.getSelection() 方法。
这看起来就十分的官方和抽象,简单的来说Selection 对象所对应的是用户所选择的 ranges (区域),俗称 拖蓝。上图中的拖蓝就是selection对象中的一个区域。
通过getRangeAt方法可以获取到具体的选中区域
    let selection = window.getSelection() //获取selection对象
    let range = selection.getRangeAt(0)//获取第一个选中的区域 除了获取选区中的区域之外,我们还可以通过 document.createRange()创建一个新的区域,然后将该区域添加到选区中
<body>
    你好
    是的
</body> 
效果如下,当代码执行后,你好这个元素被直接选中

加入区域的api包括range.selectNode和range.selectNodeContents。其中selectNode表示选中整个节点而selectNodeContents表示选中节点中的内容,针对文字的复制需要选中节点的内容,而图片的复制需要选中节点本身。
用法如下
<body>
    你好
    是的
</body>    <button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button> 
 
2.3复制文字

通过以上的selection api可以完成 创建selection对象-->选中节点内容-->添加到区域-->执行一下copy指令就可以完成复制文字了
<body>
    你好
    是的
</body>    <button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button> 
2.4复制图像

复制图像的操作是和复制文字基本相同的,只是需要在加入区域时选中整个节点,也就是把selectNodeContents方法换成selectNode
<body>
    你好
    是的
</body>    https://www.cnblogs.com/./test.png    <button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button> 
3.clipboard.js

clipboard.js是一个第三方库,也是使用了前文所讲到的document.execCommand('copy')来实现的<button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button>,使用方便,但是只能用于文本的复制。
3.1安装和引入clipboard.js

使用npm安装
npm install clipboard --save安装后在html文件内引入
或者使用CDN引入(这里只写了一种CDN引入方式,可以选择多种不同CDN方,具体请看https://github.com/zenorocha/clipboard.js/wiki/CDN-Providers)
使用import的方式引入
import Clipboard from "clipboard";3.2基本使用

初始化

直接创建一个ClipboardJS对象,传的参数可以是选择器字符串或者是DOM元素或者是DOM元素列表
new ClipboardJS('.btn') // import方式为 new Clipboard('.btn') 
实现<button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button>文字功能

初始化完后,可以到要绑定的对应元素下添加data-clipboard-target属性,属性值是要复制的元素的选择器,这里要复制的元素是 ‘是的’ 那个div,所以属性值就写#yes。不进行其他配置时,我们点击按钮,触发点击事件后,就可以完成复制文字 ‘是的’ 了。
<body>
    你好
    是的
</body>    <button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button>
 点击后,是的这个元素被选中(拖蓝),使用ctrl+v可以完成文字的复制,效果已经达到。
此时有2个问题需要优化

[*]复制的内容必须是页面上的DOM元素,能不能是自己设定的?
[*]拖蓝的效果不是很好看,如何复制文字不显示选中效果?
这时就要用到一个新的属性data-clipboard-text,属性值就是希望动态复制的内容。对ClipboardJS绑定的元素设置这个属性就可以动态复制自己设定的内容,此时就不需要再设置data-clipboard-target属性了(如果同时写2个属性,data-clipboard-text优先)。以下代码是一个写死的简单展示,真实使用的时候属性值要用js设置成需要复制的值。
<body>
    你好
    是的
</body>    <button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button>
 上图显示点击之后,复制内容成功,这样没有选中元素的效果,不会拖蓝,交互效果更好的同时又能动态设置内容
3.3更多用法

data-clipboard-action属性

data-clipboard-action属性可以决定执行的操作,这个属性有2个可选值copy或者是cut,默认是copy也就是复制,前面的所有代码中都没有出现这个属性,是直接使用的默认值copy。cut剪切,只能在input和textarea标签中使用,显然之前的div标签是无法使用的。使用方法仍是对ClipboardJS绑定的元素设置这个属性。
<button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button>事件处理

事件处理可以让用户设置复制或剪切成功或者失败的回调,事件名分别是success和error。可以通过on在ClipboardJS实例对象身上绑定success和error事件处理的回调。以下示例写了最简单alert打印成功和失败
    const clipboard = new ClipboardJS('.btn') // import方式为 new Clipboard('.btn')
    clipboard.on('success',function(){
      alert('复制成功')
    })
    clipboard.on('error',function(){
      alert('复制失败')
    })
纯JS写法

如果不想改HTML,加入过多的属性,可以直接使用纯JS写法来初始化ClipboardJS对象构造函数中传入第二个参数,第二个参数为对象,如下的示例中仅用完成js就完成了动态设置复制内容。设置配置对象的text方法,返回值就是要复制的内容
    new ClipboardJS('.btn',{
      text: function(){
            return '动态复制的内容'
      }
    })设置配置对象的target方法,返回值就是要复制的元素
    new ClipboardJS('.btn',{
      target: function (){
            return document.querySelector('#hello')
      }
    })经过测试,当html中设置属性的同时,又在构造函数里加入配置项,以js构造函数配置项为准(优先级高)
<body>
    你好
    是的
</body>    <button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button>
销毁对象

如果使用的是单页应用程序,可能希望更精确地管理DOM的生命周期。可以使用destroy方法销毁对象
var clipboard = new ClipboardJS('.btn');
clipboard.destroy();3.4源码分析

看了之前的api,想了解一下这个所谓的简单的复制库是如何实现的,于是打开了源码开始分析一下
源码地址 https://github.com/zenorocha/clipboard.js
初始化


构造函数里面传2个参数,第一个trigger即触发点击的元素对象,第二个options配置项。从最简单的例子来看,只需要传一个trigger参数就可以实现功能,那就先不管options,直接看与trigger有关的listenClick方法。
listenClick方法调用了一个第三方库的listen方法绑定了click事件和对应的回调函数this.onClick,在onclick方法中,调用了ClipboardActionDefault方法,并且传了对应的几个配置项参数,action container,target,text,这些值都是this.xxx方法,这几个方法又是在哪定义的呢?
 

找了一下类内部,定义这些方法的地方是在前文构造函数里的this.resolveOptions方法里

resolveOptions方法里的defaultAction,defaultText等等方法都是类似的,都是调用了一个getAttributeValue方法去获取html模板上的属性值

getAttributeValue方法如下,比较简单 

ClipboardActionDefault

上面跳了这么多方法虽然不难,但是也有点绕,主要还是在干一件事,那就是通过定义来准备好ClipboardActionDefault这个方法的参数。这时候就要看一下ClipboardActionDefault这个方法在干什么。
简单来看,这个方法主要分4个if判断,前2个if就是一些条件的判断,判断action只能是复制或者剪切,还有就是判断要复制的目标节点的节点类型和readonly问题等等,此处不展开去研究,有兴趣者可以点击本部分开始处的源码链接下载。

后2个if判断中的内容如下,分别用于判断是否有text值和target值,这2个值也是通过本库的核心属性data-clipboard-text和data-clipboard-target在html中获取的(或者在js配置项里获取)。判断完后就调用了ClipboardActionCopy或者ClipboardActionCut方法去实现复制或者剪切功能。

ClipboardActionCopy

这个方法就开始进行文本的复制了,首先判断要复制的目标是普通的字符串(通过data-clipboard-text设置)还是节点(通过data-clipboard-target设置),如果是文本或者不是普通的输入元素,直接调用fakeCopyAction方法执行复制操作。

 
 fakeCopyAction先创建了虚拟元素,然后把这个元素插入dom里,最后执行选中+复制操作

创建虚拟元素的方法也比较简单,先通过原生方法createElement创建了一个textarea元素,然后把它隐藏。创建这种输入类元素的好处就是可以直接去修改它的value,最后一步操作就是把文本text赋值给textarea

 创建完虚拟元素就要处理选中问题了,这里调用了select方法,方法内部根据3种元素类型设置了不同的处理对策,select元素只要focus后赋值就好。输入元素可以调用原生的select方法来选中元素,而普通元素就需要使用之前讲到的selection api去获取range和添加range了
function select(element) {
    var selectedText;

    if (element.nodeName === 'SELECT') {
      //针对select元素的处理
      element.focus();

      selectedText = element.value;
    }
    else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
      //选中输入元素
      var isReadOnly = element.hasAttribute('readonly');

      if (!isReadOnly) {
            element.setAttribute('readonly', '');
      }

      element.select();
      element.setSelectionRange(0, element.value.length);

      if (!isReadOnly) {
            element.removeAttribute('readonly');
      }

      selectedText = element.value;
    }
    else {
      //普通元素选中
      if (element.hasAttribute('contenteditable')) {
            element.focus();
      }

      var selection = window.getSelection();
      var range = document.createRange();

      range.selectNodeContents(element);
      selection.removeAllRanges();
      selection.addRange(range);

      selectedText = selection.toString();
    }

    return selectedText;
}最后的command('copy')也就是对执行复制指令这个方法的简单封装,做了一下兼容性的处理。

4. navigator.clipboard

前面的document.execCommand和第三方库clipboard.js都非常的好用,但是他们可能面临被弃用的风险,那么该怎么解决复制粘贴这个问题呢? H5新推出的clipboard api是 处理复制粘贴相关的api,可以很好的解决这个问题。用promise的方式把数据写入剪贴板,避免了页面的卡顿。
4.1 复制文字

Clipboard对象

使用Clipboard api时我们不需要手动创建Clipboard对象,而是通过navigator.clipboard来获取

打印出Clipboard对象后可以看出,这个对象有4个方法,分为两大类,write和read类。其中与复制相关的是write类表示把数据写入剪贴板,和粘贴相关的是read类表示从剪贴板里面读取数据
 writeText方法

Clipboard对象中的writeText方法可以用于复制文字,也是非常简单易用的一个方法。
参数:传一个字符串参数,即要复制的内容
返回值: 一个promise对象,如果成功复制则是成功的promise,如果写入剪贴板失败(复制失败)则是失败的promise
示例如下:先创建了一个clipboard对象,然后直接调用writeText方法复制文字123
navigator.clipboard.writeText('123')根据之前的html结构,使用Clipboard api完成文字的复制
默认情况下,会为当前的激活的页面自动授予剪贴板的写入权限。出于安全方面考虑,这里我们还是先主动向用户请求剪贴板的写入权限,如果被授权,就可以调用上面的方法直接完成复制了。
<body>
    你好
    是的
</body>    https://www.cnblogs.com/./test.png    <button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button> 
4.2 复制图像

write方法

write方法除了支持文本数据之外,还支持将图像数据写入到剪贴板,调用该方法后会返回一个 Promise 对象。
以下是简单的使用案例,先通过 Blob API 创建 Blob 对象,然后使用该 Blob 对象来构造 ClipboardItem 对象,最后再通过 write 方法把数据写入到剪贴板,复制了文字(当前页面的地址)
<button onclick="copyPageUrl()">拷贝当前页面地址</button>复制图片案例

了解了write的基本用法,那使用Clipboard对象复制图片也有办法了,只要先把图像变成Blob对象,然后构造 ClipboardItem 对象,最后再调用write方法就好。
可是,如何把一个img标签里的图片转换成Blob对象呢?
首先从Blob对象的构造函数开始,MDN文档写了Blob构造函数所需要的参数
var aBlob = new Blob( array, options );array 是一个由ArrayBuffer, ArrayBufferView, Blob, DOMString 等对象构成的 Array ,或者其他类似对象的混合体,它将会被放进 Blob。DOMStrings 会被编码为 UTF-8。
options 是一个可选的BlobPropertyBag字典,它可能会指定如下两个属性:

[*]type,默认值为 "",它代表了将会被放入到 blob 中的数组内容的 MIME 类型。
[*]endings,默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入。它是以下两个值中的一个:"native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 "transparent",代表会保持 blob 中保存的结束符不变
 
根据文档中显示,我们需要先准备一个对应的数组,然后才能构造Blob对象,也就是要把图片转成二进制数据。
分步骤实现把图片转换成Blob

[*]把img图像画在canvas画布上
[*]调用canvas的toDataURL方法,获取图片的base64编码
[*]调用atob方法,把base64编码的数据转换成二进制数据
[*]根据转换后的二进制数据,创建一个视图,此视图将把缓冲内的数据格式化为一个 8 位无符号整数数组,也就是获得了一个ArrayBufferView数组(关于ArrayBuffer和ArrayBufferView的内容详细可查阅 JavaScript 类型化数组)
 
下方代码完成了基本的功能实现:
 


 微信输入框显示,可以完成复制
这个方法在浏览器兼容性上仍存在一些问题,比如火狐可能就不支持ClipboardItem对象,此时只能用前文写的document.execCommand方法了。
<body>
    你好
    是的
</body>    https://www.cnblogs.com/./test.png    <button class="btn" data-clipboard-text="动态设置的内容" data-clipboard-action="copy">点击复制</button> 
 

来源:https://www.cnblogs.com/zhouchenkai/p/17641211.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 前端实现复制文字和图片,原来这么简单!