Javascript一键复制文本进剪贴板
场景
在搭建组件库的文档时,一个常见的需求是点击按钮可以把页面的代码复制进剪贴板。
目前 @vueuse/core
这个 Vue 的组合式 API 工具库提供了 useClipboard
方法来支持复制剪贴板功能,使用浏览器 Clipboard API
实现。
核心代码是 await navigator!.clipboard.writeText(value)
在使用 Vitepress + @vueuse/core
搭建文档站的过程中,出现了一个现象,在开发环境中点击按钮复制代码的功能正常,但是在进行打包部署至生产环境后,点击按钮会提示复制失败,两个环境使用的是同一版本的 Chrome 浏览器。
核心代码
通过阅读 @vueuse/core
的源码,可以发现其isSupported
判断功能使用 Permissions API
。
核心的判断方法 permissionStatus = await navigator!.permissions.query('clipboard-write')
。
用于判断用户是否有对剪贴板的写入权限,而在生产环境中,isSupported
判断的结果是不支持,而在开发环境中则是支持。
经过分析,发现跑打包后代码的浏览器 F12 中 'clipboard' in navigator === false
回头查阅 Clipboard API
的MDN文档有一项提示
Secure context: This feature is available only in secure contexts (HTTPS), in some or all supporting browsers.
以及 stackoverflow 上的问题讨论
This requires a secure origin — either HTTPS or localhost (or disabled by running Chrome with a flag). Just like for ServiceWorker, this state is indicated by the presence or absence of the property on the navigator object.
结论是 Clipboard API
仅支持在 安全上下文(Secure contexts) 中使用,在这里指的是基于 https
协议或者 localhost/127.0.0.1
本地环境访问的服务。
然而实际场景中确实存在需要部署在普通 http
环境中的服务,尤其是一些在企业内部的项目,需要寻找 Clipboard API
的替代方案。
方案
在 Clipboard API
出现之前,主流的剪切板操作使用 document.execCommand
来实现;
兼容思路是,判断是否支持 clipboard,不支持则退回 document.execCommand
;
document.execCommand
实现一键点击复制的流程
- 记录当前页面中 focus/select 的内容
- 新建一个 textarea
- 将要复制的文本放入
textarea.value
中 - 将 textarea 插入页面 document,并且设置样式使其不影响现有页面的展示
- 选中 textarea 的文本
document.execCommand
复制进剪贴板- 移除 textarea
- 从记录中还原页面中原选中内容
实现代码 copy-code.ts
export async function copyToClipboard(text: string) {
try {
return await navigator.clipboard.writeText(text)
} catch {
const element = document.createElement('textarea')
const previouslyFocusedElement = document.activeElement
element.value = text
// Prevent keyboard from showing on mobile
element.setAttribute('readonly', '')
element.style.contain = 'strict'
element.style.position = 'absolute'
element.style.left = '-9999px'
element.style.fontSize = '12pt' // Prevent zooming on iOS
const selection = document.getSelection()
const originalRange = selection
? selection.rangeCount > 0 && selection.getRangeAt(0)
: null
document.body.appendChild(element)
element.select()
// Explicit selection workaround for iOS
element.selectionStart = 0
element.selectionEnd = text.length
document.execCommand('copy')
document.body.removeChild(element)
if (originalRange) {
selection!.removeAllRanges() // originalRange can't be truthy when selection is falsy
selection!.addRange(originalRange)
}
// Get the focus back on the previously focused element, if any
if (previouslyFocusedElement) {
;(previouslyFocusedElement as HTMLElement).focus()
}
}
}
使用