前情提要
之前基于双越老师的 wangEditor 开发了一个软件,今早客户反应在单元格里面粘贴网页上复制的内容会将内容插入到表格下面,然后想试着复现一下,结果中途第一次遇到了 DataTransfer 的 getData 问题。
问题描述
DataTransfer 的 getData 方法在有和没有断点的情况下,获取 text/html 类型的值不一样的问题,从而导致后续处理逻辑不一致的问题。
BUG 发现过程
我准备了一个包含表格及单元格里面有富文本模块的模版和一段测试用的富文本,模版及测试粘贴内容如下图所示:
程序模版: 
内容为一个网上随便找的包含标题和内容的简单富文本:

在富文本模块中ctrl+v后,富文本内容确实被插入到了表格下面。

按照之前读的源码理解,此时先执行的应该是 wangEditor 内部的 paste 逻辑,断点检查后发现确实执行了但是并没有在 paste 里面处理,那么就只能是在 insertData 里面处理的。继续打断点确定最终的插入逻辑是在 wangEditor 内部的 with-event-data.ts 插件里面的 insertData 的覆写。以下是源代码:
typescript
e.insertData = (data: DataTransfer) => {
const fragment = data.getData('application/x-slate-fragment')
if (fragment) {
const decoded = decodeURIComponent(window.atob(fragment))
const parsed = JSON.parse(decoded) as Node[]
e.insertFragment(parsed)
return
}
const text = data.getData('text/plain')
const html = data.getData('text/html')
// const rtf = data.getData('text/rtf')
if (html) {
e.dangerouslyInsertHtml(html)
return
}
if (text) {
const lines = text.split(/\r\n|\r|\n/)
let split = false
for (const line of lines) {
if (split) {
Transforms.splitNodes(e, { always: true })
}
insertText(line)
split = true
}
return
}
}
原本我想的非常简单,只需要检查我的自定义模块在执行最底层的 insertData 前都做了哪些处理就可以判断问题产生的原因及如何修复了,而也就是在这一步发现了 Datatransfer 的问题。
我在断点时根据调用栈找到了自定义模块也覆写了的 insertData 方法,打上断点,继续粘贴内容,F10一步一步的进行调用,随后出现的画面让我大吃一惊。。粘贴的内容居然出现在了富文本模块里面。但是标题的样式没了,这是什么情况?🤔 
为什么打了断点执行和不打断点最终的效果会不一样。
于是我取消了其余断点,留下了第14行的 if (html) 这里的断点,继续调试。然后奇怪的现象出现了,粘贴的内容又跑到了表格下面。
此时就锁定问题了:wangEditor 在其余逻辑没有中断处理时,最终的 insertData 里面会获取 DataTransfer 里面的 text 和 html,html 是高优先级,当 html 有内容时,就会执行内部的 dangerouslyInsertHtml 方法,内部的实现就是会把富文本转换为 slatejs 的节点,然后插入到表格下面。而当 html 没有内容时,就会执行下面的 if (text) 块,最终执行的是 insertText 方法,内部处理就是非常简单的插入文本,所以粘贴内容才会出现在富文本模块下面,并且标题的样式没了。
发现主要问题
为什么在打了多个断点后,getData("text/html") 的值会不一样?
-
多个断点截图,html 拿到的是空字符串

-
单个断点截图,html拿到了富文本值

尝试理解原理
遇事不决找 AI,于是问了下 Gemini,得到以下回答
插句题外话,这里不得不吐槽一下,刚刚看完《绝命毒师》连遇到个问题都和海森堡有关,于是满脑子的"say my name"。

细问之下,得到以下结果
简而言之就是浏览器在打断点期间,这个安全计时并未暂停,于是等我处理完上面的断点,走到 getData("text/html") 时,安全计时超时了也就拿到了空的内容。
那为什么 getData("text/plain") 还是可以拿到值?

这也就是为什么 text/plain 有值而 text/html 没有值。
如何解决
问题找到了,那如何解决。如果我非要在断点后也能拿到 html 的数据,有没有办法,这个安全计时的最大时间又是多少?
根据我的简单试错,这个安全计时大概是5秒左右。当手速快一点的情况下,从执行事件到 text/html 这一步在5秒内时,多个断点也是可以拿到数据的。 
那么问题又双叒叕来了,有没有超过5秒也可以拿到数据的方法呢?,有的兄弟,有的。
当你手速不够快,又想一步一步慢慢调的时候,就去网站设置里面把剪贴斯权限由询问改成允许。就可以一步一步F10慢慢调了。


看看效果

在第105行添加一个日志断点 console.time("html"),然后在106行加一个断点用来等下卡住,等过10秒再下一步,然后在114行添加日志断点 console.timeEnd("html"),再在117行卡住,就可以看到 html 还可以拿到值了。而控制台也显示了中途卡了11秒,这已经超过了5秒的安全计时,但依然可以拿到值。
小结
关于这个问题,由于比较懒,没有去研究在不同的浏览器上是否都会有这个安全机制,或者不同的浏览器的安全限时是否是一样的,或者设置了剪贴板为允许后的行动逻辑是否一样。通过拖拽产生的DataTransfer是否也有这个限制,这个限制是在浏览器层面的,还是在 ECMAScript 里面规定的等等。又或者如果不是因为断点,而是因为中途处理逻辑就是超过了5秒,那是否还能拿得到。后续有机会的话再进行补充。
总结
- 在 devtools 里面,如果因为断点导致从交互事件到 getData("text/html") 的时间超过5秒,那么就会拿到一个空字符串。
- getData("text/plain") 不受此限制,超过5秒也可以拿到值。
- 开发时通过将网页的剪贴板权限改成"允许"就可以突破这个安全计时限制。
BUG 改了吗?(与本贴内容无关)
改了,改了。非常简单,只需要在富文本模块重写 insertData 方法,如果事件执行时在富文本模块中,就获取 html,然后转换成 slate 节点,再把富文本模块整个替换掉就可以了。实现:
typescript
newEditor.insertData = (data) => {
const richTextEntry = SlateEditor.above<RichTextElement>(newEditor, {
match: n => DomEditor.checkNodeType(n, richTextModuleType),
mode: 'highest',
})
if (!richTextEntry) {
insertData(data)
return
}
const html = data.getData('text/html')
if (html) {
const fragment = htmlToContent(editor, html)
SlateTransforms.insertNodes(newEditor, fragment)
return
}
insertData(data)
}
效果如下,标题的样式也得到了保留:

表单模块也拿到了富文本的内容,业务系统就可以根据编辑器实例获取到模版的结构化值了: 