浏览器自动化踩坑记:为什么 fill 填不进富文本编辑器
用脚本往富文本编辑器里填内容,是自动化里最容易被低估的一步。
页面上看起来一切正常:光标进去了,文字一行行出现,编辑器里明明有内容。可是一提交,系统提示「正文不能为空」,或者旁边那个「统计字数」显示 0,发表按钮灰着,怎么点都不动。
你截图看,字就在那儿。
一、眼见不一定为实
问题出在一个很朴素的误解上:富文本编辑器不是输入框。
普通 <input> 和 <textarea> 里,DOM 就是数据本身。你把 value 设成一段文字,数据就在那儿,浏览器读、表单提交、脚本读,全都是同一份。
富文本编辑器不是。它是两层结构:
- 上层是渲染:一个
contenteditable的容器,负责让你看见字、看见光标。 - 下层是模型:编辑器自己维护的一份文档结构(TinyMCE 的 Node 树、ProseMirror 的
EditorState、Quill 的Delta、Slate 的 node 数组)。
提交时,平台读的是下层那张数据模型,不是上层那堆 DOM。
所以当你直接改 DOM——innerHTML、textContent、双击后 fill——你只改到了投影,模型还是空的。页面上有字,编辑器认为自己什么都没收到。字数统计自然还是 0。
二、正常输入时发生了什么
用键盘敲一个字,浏览器里跑的是一条完整链路:
keydown → beforeinput → 浏览器修改 DOM → input → 编辑器监听并反解 DOM → 更新模型
编辑器真正在意的是中间的 beforeinput 和 input 事件,以及选区变化。它靠这些信号知道「用户动了哪里、动了什么」。
自动化工具如果只是 element.innerHTML = "...",这条链一步都没走。编辑器收不到任何信号,模型纹丝不动。
fill 类操作之所以在普通表单上好使、在富文本上翻车,差别就在这儿:它设置的是属性,编辑器监听的是事件。
三、三条出路
1. 用编辑器自己的 API(首选)
几乎所有成熟的编辑器都会暴露读写入口。找到它,一步到位,格式也能完整保留。
| 编辑器 | 写入 | 读出 |
|---|---|---|
| TinyMCE | tinymce.activeEditor.setContent(html) |
.getContent() |
| Monaco | editor.setValue(text) |
.getValue() |
| CodeMirror 6 | view.dispatch({changes}) |
view.state.doc.toString() |
| Quill | quill.setText() / dangerouslyPasteHTML() |
.getContents() |
if (typeof tinymce !== "undefined" && tinymce.activeEditor) {
tinymce.activeEditor.setContent("<p>正文</p>");
console.log("已注入", tinymce.activeEditor.getContent().length, "字符");
}
判断标准很简单:注入后立刻 getContent() 读回来,长度大于 0 才算成功。 不要用肉眼看,肉眼在骗你。
2. 走源码模式(最稳的兜底)
很多编辑器都带一个「HTML源码」或「<>」按钮。点开之后,你面对的是一个普通的 textarea——那里没有模型,纯文本就是数据。
把 HTML 粘进去,再切回可视化模式,编辑器会自己解析这段源码并重建模型。这条路绕开了「模拟输入」的全部麻烦,对标题、代码块、表格都能原样保留。
3. 派发真实输入事件(没有 API 时的下策)
实在找不到 API 也没有源码入口,才考虑模拟输入。两个可用的手段:
// 老但有效:会触发 beforeinput / input
document.execCommand("insertText", false, "要输入的文字");
// React 受控组件专用:绕开 React 对 value 的劫持
const setter = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype, "value"
).set;
setter.call(input, "要输入的文字");
input.dispatchEvent(new Event("input", { bubbles: true }));
第二种是 React 体系的经典坑:直接 input.value = x 能被框架的 setter 拦下来,必须拿原生 setter 改写,再手动派发 input 事件,onChange 才会触发。
四、怎么快速判断该用哪条
拿到一个陌生编辑器,按顺序试:
- 控制台敲
typeof tinymce、typeof monaco、typeof Quill,看装了哪个。 - 找不到全局对象,就在工具栏里找「源码」「HTML」「<>」按钮。
- 两者都没有,再看页面里有没有隐藏的
textarea在同步内容——很多编辑器会把结果回写到表单字段里,那个字段才是最终提交的数据。 - 前三步全落空,才退到模拟输入事件。
五、验收要看应用自己的信号
这是这次踩坑最值钱的一条经验。
自动化的验收标准,不能是「我看到框里有字」,得是应用自己认不认:
- 编辑器自报的字数(
getContent().length、页面上的统计) - 提交按钮的
disabled状态 - 校验提示有没有消失
因为这些信号来自模型层。模型空了,它们就异常;模型对了,它们才正常。肉眼看到的 DOM,恰恰是最不可靠的那一层。
六、小结
一句话:自动化要模拟的是「用户意图」,不是「最终的 DOM 状态」。
富文本编辑器的存在,把这个区别放大了。它提醒我们,网页上的「内容」不等于「HTML 字符串」——真正的数据活在一个你看不见的模型里,而那个模型只认事件和 API,不认赋值。
下次再遇到「明明填了却说空」,先别怀疑选择器。去模型层看看。
- 点赞
- 收藏
- 关注作者
评论(0)