2026/10/17 6:55:04

7131、6293、4762、1579:同一份稿子在四个编辑器里的四个长度

7131、6293、4762、1579:同一份稿子在四个编辑器里的四个长度 7131、6293、4762、1579同一份稿子在四个编辑器里的四个长度昨晚我发一篇稿子脚本的日志里跳出来四个数字。源文件 7131 博客园 7131 简书 6293DOM/ 8779服务端 知乎 4762 掘金 1579同一份稿子同样的读写方式四个平台读出四个长度。没有一个等于源文件的字符数——除了一个。这串数字摊在日志里特别扎眼因为它们本来是用来校验的。我写这些脚本的初衷是灌完正文之后读一下编辑器里有多少字跟源文件对不上就报警。结果四个平台里三个对不上而且差得很离谱——掘金那个差了 4.5 倍。如果我把校验条件写成编辑器长度必须等于源文件长度那三个平台会永远报警。如果我放宽到大于 500 就算成功那真的失败了我又抓不到。这篇文章就是把这四个数字拆开看清楚它们各自是怎么算出来的以及在每种编辑器里到底该拿什么当灌进去了没有的判据。一、先说结论长度根本不是一个可移植的度量四个数字之所以不同不是哪个平台算错了是因为它们测的本来就不是同一个东西。平台数字它实际在测什么博客园7131原生textarea的value.length简书DOM6293contenteditable的innerText.length简书服务端8779存进数据库的 HTML 字符串长度知乎4762渲染后文章正文innerText的长度掘金1579CodeMirror当前已渲染到 DOM 里的那部分文本五个数字五种语义。拿它们互相比较是没有意义的就像拿体重秤的读数和体温计的读数比大小。更麻烦的是同一份内容在纯文本和带标签之间本来就没有固定换算比例。markdown 源码里那些语法符号——井号、反引号、表格的竖线——渲染之后会消失而p、strong、code这些 HTML 标签又会往回加。所以服务端存 HTML → 比 markdown 源码长8779 7131渲染后的纯文本 → 比 markdown 源码短4762 7131这两个方向是反的而且偏移量取决于你这篇文章里代码块多不多、标题多不多、有没有表格。它是文章结构的函数不是常数。二、博客园唯一那个对得上的博客园的编辑器几乎是这类事情里最舒服的情况一个货真价实的textarea idmd-editor里面装的就是原始 markdown。vareldocument.querySelector(#md-editor);varrawel.value;// 原生 textarea 的全部内容console.log(raw.length);// 7131原生表单控件.value就是它的全部内容不打折不渲染不虚拟化。7131 7131一个字符都不差。为什么它能这么老实因为 markdown 编辑器不需要所见即所得——它存的就是源码预览是另一块面板的事。所见即所得才需要把 DOM 树当数据源。所以第一条经验遇到原生textarea直接用.value。它是唯一不需要任何解释的数字。顺带说一个我踩过的坑博客园这个textarea必须先真实点击聚焦再灌内容。我第一版脚本没聚焦Input.insertText直接把整篇稿子灌进了上一个聚焦元素里——那次是标题框于是标题变成了整篇正文。这个坑跟长度无关但它是同一个根因你以为你在跟编辑器对话其实你在跟浏览器的焦点管理器对话。三、简书同一个平台两个数字都对简书有意思的地方在于它给了我两个数字而两个都是真的。粘贴之后我立刻读 DOMvarareadocument.querySelector(.kalamu-area);varflatarea.innerText;// 先把 DOM 拍平成文本console.log(flat.length);// 6293然后脚本触发自动保存服务端回过来一个 JSON{id:144908349,content_updated_at:1791997831,content_size_status:fine}抓包取到服务端实际存的内容长度8779。6293 和 8779差了 2486 个字符。这不是某一方错了是它们在数不同的东西6293是浏览器把 DOM 拍平成纯文本的结果。此时 markdown 已经被引擎解析标题的#、代码块的 、加粗的**都不再作为字符存在。8779是带 HTML 标签的字符串。每个段落外面裹着p代码块裹着precode标签本身算字符。如果 DOM 里的纯文本是 6293加上标签到 8779是完全合理的。这里有一个重要的推论DOM 侧的读数不可信–服务端侧的读数才可信。因为最终存进数据库、最终会被读者看到的是服务端那一份。我在这个平台的校验逻辑现在只看两件事PUT /author/notes/id的响应码是 200响应体里content_size_status是fine。数字本身不再参与判断最多打进日志供人看。写到这里插一个跟长度无关但很痛的坑这个PUT接口要求 body 里的id是字符串并且必须带autosave_control字段。传数字 id或者漏了那个字段服务端一律回400 缺少文章内容参数—— 明明 content 好好地带在请求里。这种报错文案和真实原因完全不搭界的情形在自动化里比长度问题难缠十倍。四、知乎4762 才是文章本身的长度知乎的 4762 是我换了一个读法得到的。这个平台我是这么做的先在写文章页用 Draft.js 的resetWithValue()把 HTML 灌进去然后点发布最后另开一个标签打开文章页读vareldocument.querySelector(.RichText);varshownel.innerText;console.log(shown.length);// 4762注意这一步的位置它不是在编辑器里读的是在发布之后的成品页上读的。这两个位置差别巨大。在编辑器里读你读到的是编辑器此刻的内部状态在成品页读你读到的是服务端存量 前端渲染结果的合取。前者可能发生前端看着有了、服务端没存的问题简书这类contenteditable编辑器就经常如此——用document.execCommand(insertText)灌进去的文本React 不认编辑器的自动保存会拿空状态覆盖掉你在 DOM 里看到的内容。所以 4762 这个数字虽然跟源文件差了近 2400却是我最愿意相信的一个——它是终点不是中途站。这也是我在上一篇文章里讲过的判据分级弱判据 页面上写着什么中判据 DOM 状态 / 接口响应强判据 结果的对外可见状态另开一个标签用读者的身份去看这篇文章在不在、多少字——这是最贵的一种校验多一次导航、多一次等待也是唯一不会被自欺的一种。五、掘金1579 是最有意思的那个最后说掘金因为它最反直觉。CM_LEN 1625 CM_LEN 1577 CM_LEN 1579 CM_LEN 1625这是我隔几分钟跑了几次读到的值。同一篇稿子同一个页面四次读数都不一样。原因掘金的编辑器是 CodeMirror而 CodeMirror 用的是虚拟滚动。一篇 7131 字符、300 多行的文章它只把当前视口里那十几二十行真正渲染成 DOM 节点其余的在滚动条出现时才生成。所以innerText读到的永远只是此刻屏幕上可见的那一小截。往下滚一点读数变大往上滚读数变小。我那四次读数的差异就是滚动位置不同造成的。这意味着什么如果你在一个 CodeMirror 编辑器上用innerText.length做校验你得到的不是一个数是一个跟滚动位置相关的随机变量。它有下界至少要能填满一屏没有稳定的上界。我当时写的判据是cmLen 500就算灌入失败。这个阈值侥幸生效了——1579 500脚本判断灌进去了。但如果那天我发的文章短一些呢或者某次它正好滚到顶部只渲染了 400 字呢这个判据会在毫无征兆的情况下误报而我不会知道。正确做法有两种// 一直接问编辑器实例绕过 DOMvarcmdocument.querySelector(.CodeMirror).CodeMirror;console.log(cm.getValue().length);// 二退而求其次读行数console.log(document.querySelectorAll(.CodeMirror-line).length);第一种最好——CodeMirror 实例挂了CodeMirror属性在容器上getValue()拿的是完整文档对象跟渲染无关。顺带这个属性名跟库同名第一次看到容易以为是个 bug。六、跳出长度什么才是可移植的判据四个平台走完你会发现长度这个度量从一开始就选错了。它便宜、直观、写代码的时候顺手但它测的东西在不同平台上语义不一致而且会受到渲染模型、虚拟滚动、标记语言的共同扰动。我现在用的替代品是一个笨办法校验特征串而不是校验数量。// 源文件最后一行的前 24 个字符varMARK判断一件事成没成永远去看它对外部世界造成了什么改变;// 灌完之后在目标元素里找这个串varseentargetEl.innerText;varokseen.includes(MARK);它好在几个地方第一它对渲染掉了什么免疫。markdown 符号被解析没了没关系我要找的是正文本身的一段话它在任何渲染模型里都会保留。第二它对虚拟滚动免疫。CodeMirror 只渲染了视口内的行那我就滚到底部或者干脆用getValue()。只要那一句话在就说明整篇稿子灌进去了——因为它是最后一段。第三它能发现灌了一半。这是长度校验做不到的。假设粘贴在中途断了前 3000 字进去了后 4000 字丢了——长度读数是 3000如果你的阈值是 500你会判定成功。而特征串选在结尾找不到就是没灌完一个字都不会漏。第四它跟平台的存盘格式无关。不管对方存 markdown、存 HTML 还是存 Draft.js 的 block 树那段中文的形式都一样。代价只有一个它不能告诉你灌了多少。它能回答的是一个是非题不是一道算术题。但说实话在发布流水线里我需要回答的本来就是是非题。“这篇稿子完整地进去了吗”——是或否。至于进去之后是 6293 还是 8779那是排版的事不是我该在这一步关心的事。七、一份对照表最后留个速查表下次写新平台的脚本可以照着填。编辑器类型别这么读该这么读代表平台原生textarea—取value再取length博客园CodeMirrorinnerText.length虚拟滚动cm.getValue().length掘金Draft.js编辑器里的 DOM发布后另开标签读成品页知乎contenteditable ReactDOM execCommand灌入服务端响应体 TA 的 content 长度简书Cherry Markdowntextarea的 setValue分块逐行键入后读行数腾讯云以及一条铁律能用服务端回声就别用本地测量。服务端回的那个数是它真的存下来的东西本地测量的数是浏览器此刻展示的东西。这两者之间隔着一整套框架状态同步随时可能不同步。本文所有数字均来自 2026-10-15 一次真实的四平台发文日志源文件 7131 字符。