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