CKEditor 5 Word Count 字数统计插件:安装、配置与源码级原理解读
【免费下载链接】ckeditor5Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.项目地址: https://gitcode.com/GitHub_Trending/ck/ckeditor5
导读
本文聚焦 CKEditor 5 官方的 Word Count(字数与字符数统计)功能。该功能由packages/ckeditor5-word-count包实现,可实时统计编辑器中的单词数与字符数,适用于写作进度跟踪、内容长度校验、微博/评论类输入框的字符上限提示等场景。阅读本文后,你将掌握该插件的安装方式、全部配置项(container、displayWords、displayCharacters、onUpdate)、公开 API(wordCountContainer、update事件、words/characters属性),并能基于源码理解其统计口径与节流机制,写出可直接上线的字数统计与字符上限校验代码。
功能概览
Word Count 是 CKEditor 5 官方出品的一个独立插件包,用于在编辑过程中实时统计编辑器内容中的单词数量和字符数量。它的典型价值在于:
- 帮助写作者控制内容篇幅、跟踪写作进度;
- 在表单场景中校验内容长度(例如社交平台发帖的 120 字符软上限);
- 无需人工数数,即可随时拿到精确的统计数据用于业务逻辑(如草稿字数奖励、按字数计费等)。
该插件属于开源聚合包ckeditor5的一部分,同时被标记为官方插件(isOfficialPlugin)与高级插件(isPremiumPlugin),其许可校验特征码为WC(见 packages/ckeditor5-word-count/src/wordcount.ts 与 packages/ckeditor5-word-count/tests/wordcount.js 中的测试断言)。
快速上手:在页面中显示计数器
官方演示的页面结构非常简单,一个编辑器容器加上一个用于承接统计信息的div即可:
<div id="editor"> <p>Hello world.</p> </div> <div id="word-count"></div>然后通过编辑器实例拿到WordCount插件,并将其自更新的统计容器wordCountContainer挂载到页面上:
ClassicEditor .create( { // Configuration details. } ) .then( editor => { const wordCountPlugin = editor.plugins.get( 'WordCount' ); const wordCountWrapper = document.getElementById( 'word-count' ); wordCountWrapper.appendChild( wordCountPlugin.wordCountContainer ); } );wordCountContainer是一个**自更新(self-updating)**的 HTML 元素:只要编辑器内容发生变化,它内部显示的"Words"与"Characters"数值就会自动刷新,无需手动同步。你可以将这段逻辑与下方"安装"小节组合成一个可运行的页面。
安装
在基于 npm 的项目中,本功能随开源聚合包一起分发,直接安装即可:
npm install ckeditor5安装完成后,将WordCount加入插件列表,并通过wordCount键进行配置:
import { ClassicEditor, WordCount } from 'ckeditor5'; ClassicEditor .create( { licenseKey: '<YOUR_LICENSE_KEY>', // Or 'GPL'. plugins: [ WordCount, /* ... */ ], wordCount: { // Configuration. } } ) .then( /* ... */ ) .catch( /* ... */ );关于授权,WordCount是高级功能,生产环境需要有效的许可证;在本地开发与评估阶段可填写'GPL'使用 GPL 模式。类型层面,插件通过 packages/ckeditor5-word-count/src/augmentation.ts 对EditorConfig进行了模块扩展,因此wordCount配置项与editor.plugins.get( 'WordCount' )的返回值都具备完整的 TypeScript 类型提示。
配置详解
插件提供了四个配置项,全部以config.wordCount为前缀,其类型定义见 packages/ckeditor5-word-count/src/wordcountconfig.ts。
1. container:指定统计容器的挂载位置
将计数器注入页面有两种方式:
- 编程式挂载:通过
WordCount#wordCountContainer属性拿到元素后自行appendChild(见上文"快速上手"); - 声明式配置:通过
config.wordCount.container直接传入一个目标 DOM 元素,插件初始化时会自动把计数器容器追加进去:
ClassicEditor .create( { plugins: [ WordCount, /* ... */ ], wordCount: { container: document.getElementById( 'container-for-word-count' ) } } );对应的实现逻辑位于 packages/ckeditor5-word-count/src/wordcount.ts:init()阶段如果检测到container是一个真实元素(isElement( this._config.container )),就调用appendChild( this.wordCountContainer )完成注入。测试用例 packages/ckeditor5-word-count/tests/wordcount.js 验证了传入container后目标元素会获得一个子节点,且该子节点正是wordCountContainer返回的元素。
无论采用哪种方式,插件渲染出的 DOM 结构固定如下:
<div class="ck ck-word-count"> <div class="ck-word-count__words">Words: %%</div> <div class="ck-word-count__characters">Characters: %%</div> </div>如果希望完全自定义渲染方式,可以忽略容器,改由update事件(见下文)驱动自己的 UI。
2. displayWords / displayCharacters:控制显示哪部分统计
两个布尔配置项用于决定是否展示对应的统计行,未配置时默认同时显示:
ClassicEditor .create( { plugins: [ WordCount, /* ... */ ], wordCount: { displayWords: false // 隐藏单词数 // displayCharacters: false // 隐藏字符数 } } );- 当
displayWords: false时,wordCountContainer只保留字符部分:
<div class="ck ck-word-count"> <div class="ck-word-count__characters">Characters: 28</div> </div>- 当
displayCharacters: false时,只保留单词部分:
<div class="ck ck-word-count"> <div class="ck-word-count__words">Words: 4</div> </div>从源码看,packages/ckeditor5-word-count/src/wordcount.ts 在构建输出视图时逐项判断displayWords、displayCharacters(undefined视为显示),并按需创建ck-word-count__words/ck-word-count__characters子节点;测试 packages/ckeditor5-word-count/tests/wordcount.js 分别断言了两种配置下的容器文本内容。
3. onUpdate:内容统计变化时执行回调
如果要在每次统计值变化时执行自定义逻辑(例如发送字数到后端、驱动进度条),可通过onUpdate注册回调:
ClassicEditor .create( { // ... Other configuration options ... wordCount: { onUpdate: stats => { // Prints the current content statistics. console.log( `Characters: ${ stats.characters }\nWords: ${ stats.words }` ); } } } ) .then( /* ... */ ) .catch( /* ... */ );回调收到一个形如{ words, characters }的对象。其实现位置在 packages/ckeditor5-word-count/src/wordcount.ts:插件在init()中监听自身的update事件,并转发给onUpdate配置。
重要说明(性能相关):出于性能考虑,统计刷新与
onUpdate回调是**节流(throttled)**的,因此回调拿到的数值可能不是"最新时刻"的值。如果你需要精确、即时的数字(例如提交前的严格校验),请直接读取插件的characters与words属性(见下文"Common API")。
实战案例:带 120 字符上限的"发帖编辑器"
官方提供的一个经典场景是:编辑器下方附带环形进度图,字符数接近上限时变橙色,超过上限时编辑器背景变红并禁用"发送"按钮。完整 HTML/CSS 结构如下:
<style> .demo-update { border: 1px solid var(--ck-color-base-border); border-radius: var(--ck-border-radius); box-shadow: 2px 2px 0px hsla( 0, 0%, 0%, 0.1 ); margin: 1.5em 0; padding: 1em; } .demo-update h3 { font-size: 18px; font-weight: bold; margin: 0 0 .5em; padding: 0; } .demo-update .ck.ck-editor__editable_inline { border: 1px solid hsla( 0, 0%, 0%, 0.15 ); transition: background .5s ease-out; min-height: 6em; margin-bottom: 1em; } .demo-update__controls { display: flex; flex-direction: row; align-items: center; } .demo-update__chart { margin-right: 1em; } .demo-update__chart__circle { transform: rotate(-90deg); transform-origin: center; } .demo-update__chart__characters { font-size: 13px; font-weight: bold; } .demo-update__words { flex-grow: 1; opacity: .5; } .demo-update__limit-close .demo-update__chart__circle { stroke: hsl( 30, 100%, 52% ); } .demo-update__limit-exceeded .ck.ck-editor__editable_inline { background: hsl( 0, 100%, 97% ); } .demo-update__limit-exceeded .demo-update__chart__circle { stroke: hsl( 0, 100%, 52% ); } .demo-update__limit-exceeded .demo-update__chart__characters { fill: hsl( 0, 100%, 52% ); } </style> <div class="demo-update"> <h3>Post editor with word count</h3> <div id="demo-update__editor"> <p>Tourists frequently admit that <a href="https://en.wikipedia.org/wiki/Taj_Mahal">Taj Mahal</a> “simply cannot be described with words”.</p> </div> <div class="demo-update__controls"> <span class="demo-update__words"></span> <svg class="demo-update__chart" viewbox="0 0 40 40" width="40" height="40" xmlns="http://www.w3.org/2000/svg"> <circle stroke="hsl(0, 0%, 93%)" stroke-width="3" fill="none" cx="20" cy="20" r="17" /> <circle class="demo-update__chart__circle" stroke="hsl(202, 92%, 59%)" stroke-width="3" stroke-dasharray="134,534" stroke-linecap="round" fill="none" cx="20" cy="20" r="17" /> <text class="demo-update__chart__characters" x="50%" y="50%" dominant-baseline="central" text-anchor="middle"></text> </svg> <button type="button" class="demo-update__send">Send post</button> </div> </div>配合的编辑器初始化代码(以 BalloonEditor 为例)利用onUpdate回调完成全部 UI 联动:
const maxCharacters = 120; const container = document.querySelector( '.demo-update' ); const progressCircle = document.querySelector( '.demo-update__chart__circle' ); const charactersBox = document.querySelector( '.demo-update__chart__characters' ); const wordsBox = document.querySelector( '.demo-update__words' ); const circleCircumference = Math.floor( 2 * Math.PI * progressCircle.getAttribute( 'r' ) ); const sendButton = document.querySelector( '.demo-update__send' ); BalloonEditor .create( { root: { element: document.querySelector( '#demo-update__editor' ) }, // Editor configuration. wordCount: { onUpdate: stats => { const charactersProgress = stats.characters / maxCharacters * circleCircumference; const isLimitExceeded = stats.characters > maxCharacters; const isCloseToLimit = !isLimitExceeded && stats.characters > maxCharacters * .8; const circleDashArray = Math.min( charactersProgress, circleCircumference ); // Set the stroke of the circle to show how many characters were typed. progressCircle.setAttribute( 'stroke-dasharray', `${ circleDashArray },${ circleCircumference }` ); // Display the number of characters in the progress chart. When the limit is exceeded, // display how many characters should be removed. if ( isLimitExceeded ) { charactersBox.textContent = `-${ stats.characters - maxCharacters }`; } else { charactersBox.textContent = stats.characters; } wordsBox.textContent = `Words in the post: ${ stats.words }`; // If the content length is close to the character limit, add a CSS class to warn the user. container.classList.toggle( 'demo-update__limit-close', isCloseToLimit ); // If the character limit is exceeded, add a CSS class that makes the content's background red. container.classList.toggle( 'demo-update__limit-exceeded', isLimitExceeded ); // If the character limit is exceeded, disable the send button. sendButton.toggleAttribute( 'disabled', isLimitExceeded ); } } } );这段代码演示了onUpdate最典型的用法:输入法联动 UI、渐进式警告、超限硬校验三合一。需要再次强调的是,onUpdate是节流的,这里展示的 UI 反馈适合"接近/超过上限"这类对实时性要求不苛刻的场景;若要在提交按钮点击时做最终校验,请读取editor.plugins.get( 'WordCount' ).characters获取精确值。
Common API:插件对外提供的三件套
WordCount插件对外暴露了三个核心能力(见 packages/ckeditor5-word-count/src/wordcount.ts 与官方文档):
wordCountContainer属性:返回一个自更新 HTML 元素,内容随编辑器统计值自动刷新。可通过displayWords/displayCharacters配置隐藏其中任意一行。该容器只在首次访问时创建,重复调用返回同一个元素实例(测试 packages/ckeditor5-word-count/tests/wordcount.js 验证了这一行为)。编辑器销毁时,容器元素会被自动从 DOM 中移除(见destroy()实现 packages/ckeditor5-word-count/src/wordcount.ts)。update事件:每当插件更新统计值时触发,携带{ words, characters }参数,可用于注册自定义回调:
editor.plugins.get( 'WordCount' ).on( 'update', ( evt, stats ) => { // Prints the current content statistics. console.log( `Characters: ${ stats.characters }\nWords: ${ stats.words }` ); } );它等价于config.wordCount.onUpdate的底层机制——事实上onUpdate正是通过监听该事件实现的(packages/ckeditor5-word-count/src/wordcount.ts)。事件同样受节流影响,统计值可能不是最新的。
characters与words属性:可直接读取的精确统计数字,不受节流影响。源码中这两个属性被定义为 getter,每次访问都会基于当前模型内容即时重算(packages/ckeditor5-word-count/src/wordcount.ts),因此非常适合用于"保存前校验"等需要精确值的逻辑。测试 packages/ckeditor5-word-count/tests/wordcount.js 验证了设置模型数据后立即读取words即可拿到正确结果。此外二者都是可观察(observable)属性,可用change:words/change:characters监听变化。
源码深潜:统计口径与底层原理
理解统计口径有助于预判"为什么我的字数跟别的工具数出来不一样"。整个计数流程为:模型(Model)→ 纯文本 → 正则分词/计字符。
第一步:模型转纯文本
packages/ckeditor5-word-count/src/utils.ts 中的modelElementToPlainText()递归遍历编辑器模型的所有子节点:
$text/$textProxy节点直接返回其文本数据(不含任何样式标记);- 对于块级元素(如段落、表格单元格、列表项、图片说明等),每遇到一个子元素就在文本前插入一个
\n换行符作为分隔。
也就是说,每个块(段落/单元格/说明文字)之间会被换行符隔开,而内联样式(加粗、下划线、链接等)不会产生任何额外字符。例如Foo(加粗)+Bar两个段落会被转换为"Foo\nBar"。测试文件 packages/ckeditor5-word-count/tests/utils.js 用引用块、表格、软换行(<softBreak>)、混合结构等场景验证了这一转换逻辑。
插件在_getText()(packages/ckeditor5-word-count/src/wordcount.ts)中遍历文档的所有根节点(root),将各根节点的纯文本用\n连接——这也是多根编辑器(MultiRootEditor)统计的实现基础。
第二步:分词与计数字符
拿到纯文本后:
- 字符数:
txt.replace( /\n/g, '' ).length—— 即去掉换行符后的字符长度,换行/回车不计入字符数(packages/ckeditor5-word-count/src/wordcount.ts)。 - 单词数:通过正则匹配。在支持 Unicode 属性转义的环境中(现代浏览器),使用
([\p{L}\p{N}]+\S?)+(gu标志),其中\p{L}匹配任意语言的字母、\p{N}匹配任意文字系统中的数字;在不支持的环境下降级为([a-zA-Z0-9À-ž]+\S?)+(packages/ckeditor5-word-count/src/wordcount.ts)。
官方在类注释中给出了几个直观的例子(packages/ckeditor5-word-count/src/wordcount.ts):
<paragraph>foo</paragraph> <paragraph>bar</paragraph> // Words: 2, Characters: 7(两个段落,含 1 个换行;字符数不含换行) <paragraph><$text bold="true">foo</$text>bar</paragraph> // Words: 1, Characters: 6(内联样式不产生额外字符) <paragraph>*&^%)</paragraph> // Words: 0, Characters: 5(纯符号不算词) <paragraph>foo(bar)</paragraph> // Words: 1, Characters: 8 <paragraph>12345</paragraph> // Words: 1, Characters: 5(数字串算一个词)第三步:节流刷新
插件在init()中监听模型文档的change:data事件(即内容数据变更,选区变化不触发),并用 250ms 的throttle包裹统计刷新逻辑(packages/ckeditor5-word-count/src/wordcount.ts)。这意味着连续输入时统计不会每击键都重算,而是以 250ms 为周期合并计算,从而保证大文档下的性能。测试 packages/ckeditor5-word-count/tests/wordcount.js 精确验证了:
- 首次内容变更立即触发
update; - 随后 250ms 内的连续多次变更只触发一次
update,且携带的是最终值; - 仅改变选区(不改内容)不会触发
update。
边界行为汇总(均有测试佐证)
以下行为全部来自 packages/ckeditor5-word-count/tests/wordcount.js:
| 输入场景 | 结果 |
|---|---|
1 12 3,5 3/4 1.2 0 | 6 个词(数字均算词) |
j.doe@cksource.com | 1 个词(邮箱算一个词) |
Foo'bar、Foo.bar | 各 1 个词(撇号、点不拆分单词) |
(@#$%^*()) . ??? @ --- ... | 0 个词(纯符号不算词) |
| 列表项编号/项目符号 | 不参与计数(仅计列表文字) |
| 图片说明(caption) | 参与计数 |
| 表格全部单元格 | 参与计数 |
| 段落结束、软换行(Shift+Enter) | 分隔两个词 |
| 希伯来/中文/日文/阿拉伯文等多语种 | 每个词各计 1(Unicode 属性模式下) |
| 编辑器销毁后容器元素 | 自动从 DOM 移除 |
此外,插件对多根编辑器(MultiRootEditor)会累加所有根的字符与单词数(packages/ckeditor5-word-count/tests/wordcount.js);"Words: %0" 与 "Characters: %0" 标签支持翻译(测试中验证了波兰语翻译生效,packages/ckeditor5-word-count/tests/wordcount.js),语言由编辑器的language配置决定。
相关功能与延伸阅读
CKEditor 5 中与字数统计场景常搭配的功能还包括:拼写与语法检查(spelling-and-grammar-checking)、自动保存(autosave)、Markdown 风格的自动格式化(autoformat)与自动文本转换(text-transformation)。例如"写作倒计时 + 定时自动保存 + 字数超限提醒"的组合即可覆盖绝大多数内容创作工具的需求。
若想进一步阅读与验证,仓库内可直接参考的资源有:
- 功能官方指南:packages/ckeditor5-word-count/docs/features/word-count.md
- 插件源码:packages/ckeditor5-word-count/src/wordcount.ts
- 配置类型定义:packages/ckeditor5-word-count/src/wordcountconfig.ts
- 模型转纯文本工具:packages/ckeditor5-word-count/src/utils.ts
- 单元测试:packages/ckeditor5-word-count/tests/wordcount.js、packages/ckeditor5-word-count/tests/utils.js
- 官方演示片段:packages/ckeditor5-word-count/docs/_snippets/features/word-count.js
开发与调试阶段,建议配合官方 CKEditor 5 Inspector 使用,它可以直观展示编辑器的内部数据模型、选区与命令状态,帮助你理解统计值与所见内容之间的对应关系。
【免费下载链接】ckeditor5Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.项目地址: https://gitcode.com/GitHub_Trending/ck/ckeditor5
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考