记一次VueUse掉坑惨案
起因
小杨最近在为我们前端开发组开发一款 web3 实用方法展示页面
既然是展示页面,小杨理所当然的需要在页面中展示源码,于是小杨想到了 Element-plus 的展示形式,类似这种:
上方展示方法使用方式,下方通过点击按钮展示源码,并且支持复制源码功能。
有了原型,小杨很快实现了该组件
-
源码采用 Markdown 的形式编写,使用
vite-plugin-md
插件处理 Markdown 文件,将 Markdown转为Vue组件直接引入 -
复制功能采用 VueUse 中提供的
useClipboard
组合式api实现
好的,大功告成,本地测试下,ok,没问题,上流水线直接发布给组内其他人试试吧!!!
发布完成,群内炫耀,不,群内通知。
五分钟后......
同事A:“小杨,你这个复制功能不能用啊,总是提示复制失败”
同事B:“我这也是”
同事C:”行不行,小杨“
我:”什么,我看看,我写的代码怎么可能有问题呢!你们的电脑不行吧,或是浏览器不行!“
然后,小杨偷偷打开了刚才的发布网站,一点复制按钮,嘿,你别说,还真报错了,试试其他的页面看行不行,一试,你猜怎么着,还都不行,都无法复制。
我:”好像是有点小问题,我看看啊......“
复制问题排查
既然有问题,那项目启动下,本地看看日志是什么报错,说干就干。
你猜怎么着,嘿,本地启动复制功能好用的很,我鼠标都点烂了也没一次报错。
怎么回事?
难道是Vite打包有问题?难道是部署有问题?难道浏览器有问题?
于是我重新触发了打包部署,并且换了一个浏览器测试,这回应该可以了吧 ---
好吧,还是不行,复制还是有问题,还是报错,本地还是没问题,还是一点错都不报。
查查 google 吧,于是我开始百度。
VueUse 的 useClipboard
功能为什么在生产环境报错,而本地不报错?
搜了半天没结果,那还是看看 issue 吧,其中有一个 issue 一下子进入了我的视线,茫茫 issue 中一眼就看中了你(你问为什么是它,因为就它一个是中文啊):
以下是问题引用:
由于新版浏览器的安全策略,clipboard只有在安全域名下才可以访问,http域名下会显示undefined,但使用https开头的域名,或localhost,就可以访问navigator.clipboard 大佬能做一下兼容吗,内网开发很多都是ip地址访问页面,功能用不了 document.execCommand('copy')
而且 issue 下边有已被采纳的 pr,顺着pr,看看他改了点什么东西。
改动主要为以下几个:
- 增加了一个
legacy
的配置项,默认是false
,不开启降级 copy
方法中再执行复制时判断了当前浏览器是否支持navigator.clipboard
属性,如果不支持则降级使用legacyCopy
方法执行复制legacyCopy
方法,其中采用document.execCommand('copy)
方式实现了复制功能。
按照他这个 pr,就是对复制功能在当前浏览器不支持时,做了一次兼容处理,那么我只需要在使用useClipboard
时,给它传一个 legacy: true
就可以实现降级处理了。
其实在官方文档中也有描述,只是太隐蔽了,很难发现,如下图:
要不是我标出来,这谁能注意到这段话,还是英文文档。
原理探究
useClipboard
源码解读
为什么这个功能需要一个legacy
呢?为什么本地启动可以,而到了线上又不行了呢?
本着刨根问底的原则,我阅读了 useClipboard
的源码,主要函数如下:
// 是否支持 navigator.clipboard 属性
const isClipboardApiSupported = useSupported(() => (navigator && 'clipboard' in navigator))
// 是否在不支持时使用降级策略,legacy 默认是false,所以我们需要显示传递 legacy 为 true
const isSupported = computed(() => isClipboardApiSupported.value || legacy)
// 复制主函数
async function copy(value = toValue(source)) {
if (isSupported.value && value != null) {
if (isClipboardApiSupported.value)
await navigator!.clipboard.writeText(value)
else
legacyCopy(value)
text.value = value
copied.value = true
timeout.start()
}
}
// 复制降级函数
function legacyCopy(value: string) {
const ta = document.createElement('textarea')
ta.value = value ?? ''
ta.style.position = 'absolute'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
ta.remove()
}
不难发现,复制功能的实现主要是依赖于navigator.clipboard
属性,当浏览器不支持该属性时,如果用户显示声明了legacy: true
, 则使用降级策略,使用 document.execCommand('copy')
实现复制功能,而如果用户没有显示声明该属性,则什么也不做,这里笔者感觉有点疑惑,为什么不默认使用降级策略呢,我们用这个函数就是为了复制,能实现功能才是最重要的,所以默认使用降级策略我认为是比较好的,你觉得呢?
navigator.clipboard
解读
既然主要是使用navigator.clipboard
属性实现的复制功能,那我们就来一起看看这个api。
先看下权威的MDN,刚打开赫然就看到了安全上下文的提示:
此功能只支持在安全上下文(HTTPS)中使用,而我的网站是部署在HTTP协议下的,这也就解释了为什么我的网站复制一直报错的问题
可是我在本地为什么是好用的呢? 继续寻找,安全上下文的蛛丝马迹。
果然,在安全上下文一节中我找到了它的定义,如下:
其中一句很关键:
本地传递的资源,如那些带有
http://127.0.0.1
、http://localhost
和http://*.localhost
网址(如http://dev.whatever.localhost/
)和file://
网址的资源也是认为经过安全传递的。
原来如此,这也就解释了为什么我的网站在本地启动时复制功能是可以使用的了,因为我都是使用 http://localhost
方式访问的,如果我使用本地 ip 的形式访问,复制功能也是报错的。
下面看下navigator.clipboard
的具体用法:
该属性有四个方法可用:
- read
- readText
- write
- writeText
可以看到,该属性实现了剪切板的读写操作,我们不仅可以读写文本,还可以读写图片或是其他信息,不过读写read
、readText
、write
的兼容性并不好,谨慎使用,但是writeText
方式兼容性很好,可以放心使用,具体参考:MDN-clipboard
document.execCommand('copy)
方法解读
说完了navigator.clipboard
,接下来我们说说它的降级方案:document.execCommand('copy)
document.execCommand()
是操作剪贴板的传统方法,各种浏览器都支持。
看一下降级函数怎么写的:
function legacyCopy(value: string) {
const ta = document.createElement('textarea')
ta.value = value ?? ''
ta.style.position = 'absolute'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
ta.remove()
}
- 首先,创建一个
textarea
元素, - 然后将需要复制的内容赋值给当前元素
- 设置当前元素在视口不可见以防影响页面
- 将该元素添加到文档流中
- 选中该元素内的文字(关键步)
- 执行
document.execCommand('copy)
将选中内容赋值到剪切板 - 移除元素
逻辑很清晰,这里需要注意的是必须选中当前元素内的文字,否则无法实现复制功能。
document.execCommand(‘copy')
方法虽然方便,但是有一些缺点:
首先,它只能将选中的内容复制到剪贴板,无法向剪贴板任意写入内容。
其次,它是同步操作,如果复制/粘贴大量数据,页面会出现卡顿。有些浏览器还会跳出提示框,要求用户许可,这时在用户做出选择前,页面会失去响应。
这也就是为什么浏览器厂商要推出新的 navigator.clipboard
的原因,也是useClipboard
将其选为降级方案的原因。
总结
最后,小杨弄明白了 useClipboard
的源码,同时也知道了navigator.clipboard
和document.execCommand()
,以后再遇到复制问题,我相信小杨也能侃侃而谈了。
最后的最后,别忘了在调用useClipboard
时加上legacy: true
,最后附上一段代码,防止掉坑~~~
const { copied, copy } = useClipboard({ legacy: true });
await copy('文章看完了,别忘了评论、点赞、关注哦,你真棒~~');
转载自:https://juejin.cn/post/7236656669752590391