起底"复制粘贴",复制粘贴底层是什么数据?如何自定义复制粘贴数据当你在复制粘贴时,剪切板发生了什么?如何自定义剪切板数
背景
最近在处理一个文档内粘贴数据丢失的问题的bug时遇到这个问题,将带有css样式的文字复制粘贴至文档中发现格式丢失,也包括超链接的href在粘贴至文档中被丢失。
在对比主流文档粘贴表现后,便追根溯源去找到复制时的元数据看看是否存在丢失情况
主流文档测试
首先用两组标签来测试,一组带有css样式的标题文字,一组a标签的超链接。对比将对应标签选中后复制
后粘贴
不同文档内的表现
<h1 style="color: red">paste test</h1>
<h3><a href="https://www.baidu.com" target="_blank">to baidu</a></h3>
![]() | ![]() |
---|---|
![]() | ![]() |
可以看到,不同文档内有不同的表现,但大体上是完整粘贴,标签的字体、颜色,以及超链接背后的文字后能保留了原格式(这里飞书应该是bug?)
元数据
原理
归根结底,复制粘贴一种系统剪切板的数据IO,也就是数据之间的交换,复制=输入,粘贴=输出,对不同来源的数据做不同的处理,这里也能归纳出主流文档的方式,复制源的数据肯定是相同的,那么不同的就是粘贴的接入方对于数据的接纳和处理方式,所以下文会将这份数据称作元数据
复制产生的元数据
在产生一次复制行为后,需要从剪切板中找到这次复制行为的元数据,这里可以使用一个新的api:clipboard
,这是一个暴露在在Navigator
上的全局属性,他提供的开发者自主对系统剪切板的读(read)和写(write)的能力,相较于过去的document.execCommand('copy')
式方法,这种深入剪切板元数据的管理方式带来了更多的可能性
Navigator.clipboard
的兼容性并不算太友好,详情可查询此处
接着我们来读取剪切板中被写入的数据
navigator.clipboard.read().then((data) => {
console.log('读到的数据格式为', data)
})
可以看到,在产生一次复制文本行为后,剪切板中被写入了两份数据,一份是
text/plain
的纯文本, 一份是text/html
的带有格式的html数据, 这其实因为当你从网页中复制内容,浏览器会自动保存该内容的多个格式版本,目的是为了增强粘贴的兼容性。
接下来我们将剪切板的数据可视化
在手动对网页中的h1标签复制后,右侧可以看出从剪切板读出的数据,这次复制的元数据是源标签以及被带上的样式
text/plain
为纯文本数据, 通过readText()读到的数据正式来自于此,通常我们将文本粘贴至普通输入框、亦或者其他没有特殊处理的客户端内时,读取的就是这份数据,所以会发现粘贴进去是不带源数据格式的, 而text/html
则代表富文本数据,通过输出可以看到他包含了html标签以及文本所带有的css样式,如果我们选择接收此类数据,则代表需要处理他响应的css相关信息
主流文档复制产生的元数据
既然看过了在浏览器默认行为下web网页产生的元数据,那么我们再来看看目前主流文档是如何处理复制元数据的,取样腾讯文档&飞书文档,在文档中分别复制一份文本,然后使用api读取剪切板
![]() | ![]() |
---|
通过图示读取的数据标签不难看出,在剪切板中的这份数据中含有相当多的自定义属性,并不是简单的使用的浏览器默认行为的复制数据,
复制产生的另类元数据
不知道大家有没有好奇过,当你从一个网页复制一份数据,然后粘贴至另一个网页中,为什么粘贴后的数据表现能表现一致,如上述文档类型的编辑器是因为保存为纯html标签后实现了跨页面数据一致性,那么对于复杂类型的网页是如何处理的呢?
这里典型的就是流程图,流程图的复制粘贴后数据依旧保持一致性,即使是跨页面粘贴也是如此,总不可能他们将流程图的节点转为html节点后进行粘贴吧?我们可以看看复制后的元数据
可以看出对流程图节点进行复制后,写入剪切板的实际上是json数据流,而且由于他仅写入一种text/plain
的纯文本数据,所以我们复制后在输入框粘贴便可以发现正好粘贴的就是这份元数据,所以这也带来了一个问题,即这份剪切板中的数据只能在自己的网站进行流通,如果其他厂商的编辑器并不选择解析你的这份数据,那么你将这份数据粘贴至其他厂商的编辑器,最终只能以纯json文本的形式粘贴
而腾讯文档的处理则不同,他的流程图在网页中以svg标签表现,所以当我复制节点后,再尝试通过剪切板去读这份节点,就会发现这份数据
这里会感慨腾讯流程图的兼容性,当我们对单一节点进行复制时,他将流程图的节点同时转为了富文本数据以及图片数据,以便适配不同客户端读取的不同选择,同时为了避免数据泄露并没有写入
text/plain
数据,避免了用户在其他场景粘贴时出现节点的乱码数据
其实这里能发现一个现象,即当我拿着某个网站的流程图元数据去腾讯文档粘贴后,节点的元数据只能被读作纯文本写入编辑器,而当我复制腾讯文档中的流程图节点,去该网站粘贴时,则能以流程图节点展示,想必这也与两者所选择的技术方案分不开,
这也引出我们下一个主题:如何向剪切板写入自定义的复制元数据
自定义元数据的写入
直入主题,向剪切板写入数据会使用到navigator.clipboard.write(data)
, 与上文提到的read相辅相成, 详情可以查看此处,
在前文读取中也能看到,每次读取出来的剪切板数据是ClipboardItem
类型,自然我们写入数据也是需要写入同样的数据
const writeClipboard = () => {
const customText = '嘿嘿嘿,复制什么出来的都是我'
const data = [
new ClipboardItem({
'text/plain': new Blob([customText], { type: 'text/plain' })
})
]
navigator.clipboard.write(data)
}
这样当然并不算完整,还记得我们每次复制时都产生了两种类型的数据吗?纯文本格式的text/plain
以及适配富文本格式的text/html
, 对于不同客户端来说他们会选择不同的数据源,所以我们在写入自定义元数据时需要将两种类型的数据同时写入
const writeClipboard = () => {
const customText = '嘿嘿嘿,复制什么出来的都是我'
const richCustomText = `<h1 style="color: red">${customText}</h1>`
const data = [
new ClipboardItem({
'text/plain': new Blob([customText], { type: 'text/plain' }),
'text/html': new Blob([richCustomText], { type: 'text/html' })
})
]
navigator.clipboard.write(data)
}
最后我们需要拦截默认的复制行为,将其转换为我们的自定义写入方法
onMounted(() => {
window.addEventListener('copy', (e) => {
e.preventDefault()
writeClipboard()
})
})
在这之后我们测试产生复制行为后写入的数据,以及将复制的结果粘贴至腾讯文档中
![]() | ![]() |
---|
这里也能看到,对于文档编辑器而言,读取的都是富文本元数据并且成功写入
自定义元数据后的接收与处理
对于在网页中复制而言,复制时保存了两种元数据,那么粘贴时到底是写入哪份数据是如何决定的呢?实际上这取决于该落地的应用程序读取规则,在粘贴事件发生时从clipboard
中读取全部的元数据,而后取自己所需要的。当然如果没有特殊处理的话是以系统规则为准
- 对于普通的输入框而言,当你粘贴一段文本时,取的是剪切板的的
text/plain
数据 - 对于没有开发者介入的富文本编辑器,粘贴时则会默认取
text/html
数据,从而实现在富文本编辑器中粘贴带格式的文本 - 而对于高度自定义的文档编辑器,取什么数据以及如何处理则由开发者完全掌控,以流程图为例子来开,剪切板的写入与读取完全看厂商自己的选择,
线上问题:复制表格后至文档内丢失
最后回到我们生产环境的问题,我们发现用户在某种特殊的场景下看,从未知来源的table标签中复制了一份完整数据,在粘贴至文档编辑器后发现数据发生了丢失,仅仅只保留了文本数据与排版格式
于是我们建立一份表格数据做测试
<table border="1">
<tr>
<td style="color: red">这是第一行</td>
<td><a href="https://www.baidu.com">这是超链接</a></td>
</tr>
<tr>
<td>这是第二行</td>
<td><a href="https://www.baidu.com">这是超链接</a></td>
</tr>
</table>
在手动复制这份元数据,我们打印了这份元数据,
结合元数据,我们发现在用户视角粘贴过后的表格非常符合纯文本格式,仅保留了格式与纯文本,而相关的超链接信息、以及样式信息部分丢失,所以只需要开发者去接收处理粘贴数据即可
一点题外话
剪切板算是一个不太大众的问题,如果不涉及富文本编辑、或者高度自定义的文档,那么大概率是不需要操心剪切板数据的交互,通常情况下document.execCommand('copy')
即能满足,而一旦需要处理剪切板,就代表着无尽的麻烦,
navigator.clipboard
仅支持https域名下使用,并且无论读写都需要浏览器的显式授权- 极差的浏览器兼容性代表着你需要为剪切板数据适配一套纯execCommand方案
- 不接受浏览器的默认复制粘贴行为则代表着你需要定义一套用于跨端、跨网页交互的数据结构,同时需要考虑数据在非同源网站下的兼容情况
最后
麻烦很多,方案也很多,希望这篇文章能对大家有一点小小的帮助
转载自:https://juejin.cn/post/7409913195660689418