前端复制方案全覆盖!验证真机与生产代码完美一致!✨

2023-10-25 14:11:28 浏览数 (1)

前言

网上讲粘贴复制的很多,讲清楚复制异步数据得很少,在真机上真正验证过得凤毛麟角,正巧工作上遇到了复制接口返回的数据这个问题,求助了很多人,没有太好的解决办法,最终通过修改交互实现了这个复制功能,故写篇文档记录一下,也分享给大家。

主流复制方案

原生js API实现

document.execCommand

概述

document暴露 execCommand 方法

该方法允许运行命令来操纵可编辑内容区域的元素

我们在使用时,常常通过以一个不可见的input 或者textrea,获取value值

用document.exexCommand('copy')复制进粘贴板

缺点

MDN已经提示这个API,已经废弃

新版本浏览器兼容性尚不可知,基于高可用的原则,现在并不推荐在开发中使用。

但是,如果需要复制的是非常大段的内容,则 execCommand() 方法可能会引起卡顿,因为 execCommand() 方法是一个同步方法,必须等复制操作结束,才能继续执行后面的代码。

为了兼容移动端各个浏览器,传统的select() 在移动端会失效

需要做兼容处理,处理代码比较恶心,在开发中也不建议使用,下面我发一个我们在生产中使用的版本,供大家参考

兼容移动端代码

下面这段代码已在各个浏览器,各个手机主流机型验证过,经得住时间的考验

代码语言:javascript复制
function copy(text) {
        let input = document.createElement('input');
        input.value = text;
        input.readOnly = true;
        document.body.appendChild(input);
        input.select();
        let result = document.execCommand('copy');
        if (result) {
            setTimeout(()=>{document.body.removeChild(input);input=null},0)
            return true;
        } else {
            const range = document.createRange();
            range.selectNode(input);
            const selection = window.getSelection();
            if (selection.rangeCount > 0) {
                selection.removeAllRanges();
                selection.addRange(range);
                document.execCommand('copy');
                setTimeout(() => {document.body.removeChild(input);}, 0)
                return true;
            }
        }
        return false;
    }

引入第三方库 基于clipboard.js实现

概述

行业内最成熟的库就是clipboard.js。

在这个基础上做了简单封装的vue-clipboard2,vue-clipboard3。

底层库一样,都是换汤不换药。

安装方式

二选一即可

代码语言:javascript复制
npm install clipboard --save

<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.6/dist/clipboard.min.js"></script></script>)

使用方式

在点击的按钮上new clipboard对象,可以传入复制的值。

然后设置监听事件。也可选择在dom上传入属性,具体使用可以参考文档。

github.com/zenorocha/c…

优点

第三方库,内部针对各个浏览器都做了兼容性处理,可用性更高,且在不断更新,这个在ios 安卓设备无明显兼容性问题

代码展示

代码语言:javascript复制
var clipboard = new ClipboardJS('.btn',{
   text: () => 'xxx',
});
clipboard.on('success', function (e) {
    console.log(e);
    //打印动作信息(copy或者cut)
    console.info('Action:', e.action);
    //打印复制的文本
    console.info('Text:', e.text);
    //打印trigger
    console.info('Trigger:', e.trigger);
    clipboard.destroy()
});
clipboard.on('error',function(e){
});

Clipboard API

execCommand替代方案Clipboard

概述

剪贴板 Clipboard API 提供了响应剪贴板命令(剪切、复制和粘贴)与异步读写系统剪贴板的能力。

从权限 Permissions API 获取权限之后,才能访问剪贴板内容;

如果用户没有授予权限,则不允许读取或更改剪贴板内容。

该 API 被设计用来取代使用 document.execCommand() 的剪贴板访问方式。

优点

新的API,调用简单,兼容性问题少

基于Promise,不用像execCommand一样还得选中范围

看了一下兼容性也挺不错的

兼容性分析

兼容性比较低,在 can I use上查了一下

ios系统需要13.1以上,安卓系统需要6以上已能支持91.59%的用户

使用注意点

出于安全策略限制,只能在https域名和本地域名下使用。

在http下和非本地域名下 执行navigator.clipboard返回undefined

代码演示

代码语言:javascript复制
navigator.clipboard.writeText(value);
navigator.clipboard.writeText(value).then(() => {});

异步数据如何复制

业务场景

场景是这样,用户点击按钮,去调用接口,把接口返回的内容复制到粘贴板上。

我天真的使用了之前已经在成熟的方案一方案二,结果被测试啪啪打脸。

重要事情说三遍

document.execCommand,clipboard.js均不支持异步数据的复制

document.execCommand,clipboard.js均不支持异步数据的复制

document.execCommand,clipboard.js均不支持异步数据的复制

遇到的问题

真机上的表现

document.execCommand android 可以复制成功,ios 复制不生效

clipboard.js android ios 均需要点击两次才能完成复制

网友们的方案

方案一:

建立两个dom,一个dom1执行获取数据操作,一个dom2执行复制操作,点击dom1获取数据之后,默认去触发dom2的复制事件。

真机测试,无法粘贴,需要点击2次。才能复制。

方案二:

利用async await 将代码改写成同步代码,当时看到这个方案,就觉得不靠谱,属于自自欺人,实际还是验证了下,确实不行,真机测试,无法粘贴,需要点击2次。才能复制。

根本原因

通过大量调研:总结出一句话

复制操作之前如果调用接口,浏览器出于安全策略,不会执行复制操作

之后的demo也验证了我的结论,如果复制之前执行setTimeout再复制数据无任何问题。

复制之前调用接口,再复制接口返回数据,就会出现复制失效。

再次点击按钮,发现执行了两次复制操作,可见我们注册复制事件已经成功了。

从程序执行角度来说,代码是没有问题的,只是复制操作被拦截了,各个浏览器表现不一致。

解决方案

修改交互

将异步数据需要调用的接口,提前调用,在点击复制按钮之前,直接使用已经获得的数据。

使用Clipboard API

技术调研

通过解决这个bug,发现出几个问题

  • 前端领域,网络上的博客普遍质量不高,讲原理的多于讲实践的,生搬硬套得多于写原创的。
  • 求助网络,不如求助网友,虽然网友提供的思路没有采纳,但是给了很大的支持。

因此出于这个原因,我调研了前端三种主流复制的方案,并自己做了验证。

三种方案在真机上表现

三种技术方案对比

复制权限控制

苹果对剪切板的权限实际上没有作任何控制,这意味着任何应用都是无限制的读取剪切板内容不需要用户的授权

主流安卓机器浏览器,复制之前都需要判断浏览器是否赋予写入剪切板权限,读取剪切板权限。

与我们复制功能强相关的权限就是写入剪切板权限

权限种类

一般权限种类有

  • 拒绝
  • 询问
  • 仅在使用中允许
  • 始终允许

以qq浏览器为例

  • 当用户选择拒绝,所有复制API全部失效
  • 当用户选择询问,会自动拉起询问弹窗,是否开启写入粘贴板权限
  • 当用户选择仅在使用中允许和始终允许,则之后复制功能正常,不会询问

所以需要我们在调用复制代码之前考虑增加权限判断

如何获取权限

以google浏览器为例,可以先查权限

权限的值为

  • granted 允许
  • denied 拒绝
  • prompt 询问
代码语言:javascript复制
navigator.permissions.query({
  name: 'clipboard-read'
}).then(permissionStatus => {
  // permissionStatus.state 的值是 'granted'、'denied'、'prompt':
  console.log(permissionStatus.state);
});
navigator.permissions.query({
  name: 'clipboard-write'
}).then(permissionStatus => {
  // permissionStatus.state 的值是 'granted'、'denied'、'prompt':
  console.log(permissionStatus.state);
});

兼容性

permissions.query 的兼容性

可以看出兼容性非常不好,谷歌43以上都支持,safari全不支持,安卓浏览器不支持,部门安卓浏览器权限支持不明确

加上这是google浏览器自定义的标准,目前属于一个实验性属性,业内还没有形成一个统一的标准,建议慎重使用

总结

前端究竟如何处理复制功能

1.如果在app内页面,可推动app提供复制内容的方法,前端直接去调用

2.修改交互。将异步数据需要调用的接口,提前调用,在点击复制按钮之前,直接使用已经获得的数据。

或者在按钮之上,再增加弹窗,提示用户复制,在用户点击弹窗确认再执行复制,从交互上分离复制和获取数据功能。

3.三种复制方法,原生JS,可以参考我写的方法,可兼容基本的IOS和安卓浏览器,适合简单场景。clipboard.js第三方库,兼容性较好,适合大型项目。Clipboard API 新的API,兼容性较好,可兼容同步异步数据,也推荐使用。

4.如果是PC端页面,推荐使用原生js去实现,代码量较少,引入简单。

一点思考

当我们遇到要做复制功能时,首先应该考虑此功能和业务的相关性。

如果是一个很重要的功能,就像淘宝app内的复制口令码,在淘宝app内直接打开商品。银行app里的复制卡号,属于强交互功能,可以参考我下面的方案一二

如果只是一个不影响业务的部分,或者内部使用的系统,可以尝试新的API.

附录

这是我做实验用的代码,大家也可直接复制,去自己真机验证

代码语言:javascript复制
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <title>复制功能测试</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<h1>三种复制粘贴方式验证</h1>
<h3>原生jsAPI document execCommand('copy')</h3>
<button class="btn" id="btn1">复制同步数据1</button>
<button class="btn" id="btn2">复制异步数据2</button>


<h3>第三方库函数 clipboard.js</h3>
<button class="btn" id="btn3">复制同步数据3</button>
<button class="btn" id="btn4">复制异步数据4</button>

<h3>最新API 剪贴板 Clipboard API</h3>
<button class="btn" id="btn5">复制同步数据5</button>
<button class="btn" id="btn6">复制异步数据6</button>

<h4>在此处粘贴</h4>
<input class="input" style="margin-top: 20px;display: block;height: 50px;">
<!-- 3. 引入库文件 -->
<!--  <script src="../dist/clipboard.min.js"></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.6/clipboard.min.js"></script>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>

<script>
    function copy(text) {
        let input = document.createElement('input');
        input.value = text;
        input.readOnly = true;
        document.body.appendChild(input);
        input.select();
        let result = document.execCommand('copy');
        if (result) {
            setTimeout(()=>{document.body.removeChild(input);input=null},0)
            return true;
        } else {
            const range = document.createRange();
            range.selectNode(input);
            const selection = window.getSelection();
            if (selection.rangeCount > 0) {
                selection.removeAllRanges();
                selection.addRange(range);
                document.execCommand('copy');
                setTimeout(() => {document.body.removeChild(input);}, 0)
                return true;
            }
        }
        return false;
    }
    //注意seTimeout和真实请求接口区别
    function ajax2(res){
        return new Promise((resolve, reject)=>{
                  setTimeout(() => {
                    resolve(res)
                  }, 1000);
        })
    }
    function ajax(res){
        return new Promise((resolve, reject)=>{
            $.get('http://jsonplaceholder.typicode.com/posts/2',(data)=>{
                resolve(res)
            })
        })
    }
    document.getElementById('btn1').onclick = function (){
        copy(1)
    }
    document.getElementById('btn2').onclick = function (){
        ajax(2).then(res=>{
            copy(res)
        })
    }
    function newClipboardJS(dom,id){
        var clipboard = new ClipboardJS(`${dom}`,{
            text: ()=> id
        });

        clipboard.on('success', function (e) {
            //打印动作信息(copy或者cut)
            console.info('Action:', e.action);
            //打印复制的文本
            console.info('Text:', e.text);
            // document.getElementById('aaa').innerHTML = document.getElementById('aaa').innerHTML   `<div>${e.text}</div>`
            //打印trigger
            console.info('Trigger:', e.trigger);
            clipboard.destroy()
        });
    }

    document.getElementById('btn3').onclick = function (){
        newClipboardJS('#btn3',3)

    }
    document.getElementById('btn4').onclick = function (){
        ajax(4).then(res=>{
            newClipboardJS('#btn4',res)
        })

    }
    async function clipboard(id){
        try {
            navigator.clipboard.writeText(id).then(function() {
                alert("复制成功");
            }, function() {
                alert("复制失败");
            });
        } catch(e){
            alert("hello");
        }
        
    }

    document.getElementById('btn5').onclick = function (){
        clipboard(5)
    }
    document.getElementById('btn6').onclick = function (){
        ajax(6).then(res=>{
            clipboard(res)
        })
    }
</script>
</body>
</html>

0 人点赞