剪贴板读写(复制、粘贴等)操作
浏览器中与系统剪贴板交互一共有两套方案:已经废弃的 Document.execCommand(),以及现行标准异步 Clipboard API。
document.execCommand()
document.execCommand() 用于对当前文档执行编辑类指令。复制、剪切操作作用于页面选中的内容,可以是输入框选中文本,或是 contenteditable 可编辑区域。
document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
方法返回布尔值,代表该指令是否执行成功。
- aCommandName:命令名字符串,常用:
copy、cut、paste、delete、forwardDelete、selectAll。 - aShowDefaultUI:布尔值,是否展示原生交互界面,Firefox 并未实现该参数,通常填
false。 - aValueArgument:部分指令需要的额外参数。
// copy、cut 在部分浏览器依旧可以运行;paste 通过脚本调用通常会被浏览器安全策略拦截
document.execCommand('copy')
document.execCommand('cut')
document.execCommand('paste')
已废弃:document.execCommand() 已经从 Web 标准中移除。浏览器为兼容旧项目还保留部分实现,不保证长期可用。新项目优先使用 Clipboard API。
复制操作 copy
document.execCommand('copy') 将页面选中的文本写入系统剪贴板。在用户点击这类真实用户事件回调内调用,通常不需要额外权限。
const inputDom = document.querySelector('#input')
inputDom.select()
document.execCommand('copy')
该指令只能复制页面上被选中的内容。如果希望复制任意指定字符串,需要临时创建 DOM 元素完成选中文本,执行复制后再移除元素:
function fnExecCommandCopy(str) {
let inputDom = document.createElement("input")
document.body.appendChild(inputDom)
inputDom.value = str
inputDom.focus()
inputDom.select()
if (document.execCommand("copy")) {
console.log("复制成功")
} else {
console.log("不支持复制")
}
inputDom.blur()
document.body.removeChild(inputDom)
}
含换行的文本用 textarea 比 input 更稳妥。
注意事项:
- 复制逻辑尽量放在用户触发的事件回调(click 等),纯脚本自动执行复制,多数浏览器会拒绝。
execCommand是同步执行;复制大数据量会阻塞页面渲染。如果浏览器弹出权限确认框,页面会卡住等待用户操作完成。
剪切操作 cut
document.execCommand('cut') 将选中内容写入剪贴板,同时删除页面选中的内容。
const inputDom = document.querySelector('#input')
inputDom.select()
document.execCommand('cut')
粘贴操作 paste
document.execCommand('paste') 读取剪贴板内容,并写入当前获得焦点的可编辑元素中。
const inputDom = document.querySelector('#input')
inputDom.focus()
document.execCommand('paste')
出于安全策略,现行浏览器普遍阻止脚本直接调用 paste,该方式大多数场景下无法生效。
Clipboard API
Clipboard 接口是现行标准,通过 navigator.clipboard 获取实例。仅在安全上下文(HTTPS、localhost)下可用,用来替代废弃的 execCommand。
使用要点:
- 先做能力判断
if (navigator.clipboard),低版本浏览器可以回退execCommand。 - 生产环境 HTTPS 才可以正常调用;本地开发
localhost允许访问。 - 权限模型基于 Permissions API,权限标识符:
clipboard-read(读剪贴板)、clipboard-write(写剪贴板)。Firefox、Safari 对这两个名字的query支持不完整,查不到就直接调用读写并用catch处理失败。 - 写入剪贴板: 在用户真实交互(点击等)回调中,多数浏览器直接允许;Safari 等浏览器限制更严格,同样依赖用户手势,且页面通常需要处于前台、拥有焦点。
- 读取剪贴板: Chromium 通常会弹窗向用户索要授权。
- 在开发者工具控制台直接运行代码会因权限上下文不同而失败:控制台属于另一个执行上下文,不继承网页权限。
Permissions API
Permissions API 用来查询权限状态。网页侧真正通用的是 query();剪贴板权限不能靠脚本主动 request() 弹出授权,只有实际读写剪贴板时才会询问用户。
query():查询某个 API 当前权限状态。request()、revoke():曾经出现在草案里,现行浏览器基本不实现;网页标准里也没有requestAll()。
查询剪贴板读取权限示例(Chromium;其他引擎可能不认这个 name):
navigator.permissions.query({name:'clipboard-read'}).then(function(result) {
if (result.state === 'granted') {
console.log('granted 已授权')
} else if (result.state === 'prompt') {
console.log('prompt 需要弹窗询问用户')
} else {
console.log('denied 拒绝授权')
}
})
浏览器扩展程序:可以直接在 manifest.json 声明 clipboardRead、clipboardWrite 权限。网页脚本不能通过 JavaScript 主动唤起授权弹窗。
writeText() 写入纯文本
专门写入字符串文本,返回 Promise。成功 resolve,权限拒绝、报错会进入 catch。
navigator.clipboard.writeText("霁光浮瓦碧参差。").then((res) => {
console.log('writeText:写入成功!')
}).catch((err) => {
console.error('写入失败', err)
})
write() 写入多类型数据(文本、图片等二进制)
write() 支持写入多种 MIME 类型,参数是 ClipboardItem 对象数组。
注意:
ClipboardItem的属性值需要传入Blob对象,部分浏览器(尤其是 Safari)要求传入 Promise。
navigator.clipboard.write([
new ClipboardItem({
"text/plain": new Blob(['要复制的文本'], { type: "text/plain" })
})
]).then((res) => {
console.log('write:写入成功!')
}).catch((err) => {
console.error('写入失败', err)
})
同时写入文本与图片示例:
(async function(){
const image = await fetch('tupian_27781.png').then(response => response.blob())
const text = new Blob(['一夕轻雷落万丝,\n霁光浮瓦碧参差。\n有情芍药含春泪,\n无力蔷薇卧晓枝。'], {type: 'text/plain'})
const data = new ClipboardItem({
'text/plain': text,
'image/png': image
})
await navigator.clipboard.write([data])
console.log('write:写入成功!')
})()
readText() 读取剪贴板纯文本
返回 Promise,成功拿到剪贴板文本。浏览器会触发权限询问。
navigator.clipboard.readText().then((res) => {
console.log(`readText:${res}`)
}).catch((err) => {
console.error('读取失败', err)
})
read() 读取多格式数据(文本、图片)
返回 ClipboardItem 数组,可以处理图片、富文本等多种剪贴板内容。通过 item.getType(type) 获取对应 MIME 的 Blob。
navigator.clipboard.read().then(data => {
data.forEach(item => {
item.types.forEach(async (type) => {
let readRes = await item.getType(type)
switch(type) {
case 'text/plain': {
let tx = await readRes.text()
console.log(tx)
break
}
case 'image/png': {
console.log(URL.createObjectURL(readRes))
break
}
default: {
console.log('未处理的数据类型!')
break
}
}
})
})
}).catch(err => {
console.error('无法读取剪贴板: ', err)
})
剪贴板事件
浏览器存在一组剪贴板事件:快捷键 Ctrl、Cmd 加 C、X、V,右键菜单复制粘贴,以及 execCommand 指令都会触发。注意:异步 Clipboard API 的 writeText、readText、write、read 不会触发这一套事件。
事件清单:
copy:用户发起复制时触发(可拦截并改写将要写入剪贴板的数据)cut:用户发起剪切时触发paste:用户发起粘贴时触发beforecopy、beforecut、beforepaste:复制、剪切、粘贴之前触发,属于非标准事件,各浏览器实现不一致,新项目不要依赖。
注册监听:
document.addEventListener('copy', (event) => {});
document.addEventListener('cut', (event) => {});
document.addEventListener('paste', (event) => {});
事件对象 event.clipboardData 是一个 DataTransfer 对象,可以读取、修改本次剪贴板的数据。调用 event.preventDefault() 可以阻止浏览器默认剪贴行为。
示例:拦截 copy 事件,改写复制到剪贴板的内容:
document.addEventListener('copy', (event) => {
event.preventDefault()
event.clipboardData.setData('text/plain', 'COPY ME!!!')
event.clipboardData.setData('text/html', '<p>COPY ME!!!</p>')
})
clipboardData 常用方法:
setData(type, data):设置指定 MIME 类型的剪贴板数据。getData(type):读取指定类型数据。clearData([type]):清除剪贴板数据;不传参数清除全部。items:类数组,存放剪贴项集合,通常只有一项。