Tiptap 协作光标扩展演进实录:@tiptap/extension-collaboration-caret 变更历史与源码实现解读
【免费下载链接】tiptapThe headless rich text editor framework for web artisans.项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap
导读
@tiptap/extension-collaboration-caret是 Tiptap 官方提供的协同编辑光标扩展,用于在多人同时编辑同一文档时,实时渲染其他协作者的光标位置与选中区域(selection)。本文以该扩展的 CHANGELOG.md 为主线,梳理它从 v2 到 v3 的命名变更、底层依赖替换与关键缺陷修复,并结合 collaboration-caret.ts 源码、单元测试与 Demo,讲清它的配置项、Storage、Commands 与插件实现原理。读完你可以掌握:如何正确接入并配置协作光标、如何读取在线用户列表、如何动态更新本人用户信息,以及升级到 v3 时的破坏性变更清单。
这个扩展解决什么问题
在基于 Yjs 的协同编辑场景里,Collaboration扩展负责文档数据的同步,但文档内容的"人在哪"并不属于文档数据,而是通过 Yjs Awareness(在线状态协议)在客户端之间广播的。CollaborationCaret扩展负责把这部分感知状态翻译成可视化的光标与选区:
- 每个远端协作者拥有一个随其文档位置移动的光标 DOM 元素(可带姓名标签);
- 远端协作者的文本选区会被一段半透明高亮覆盖;
- 本地用户信息会写入 awareness,并可在运行时通过命令动态更新。
从扩展声明看(collaboration-caret.ts),它直接Extension.create,名称为collaborationCaret,并设置priority: 999,在所有扩展中处于高优先级,确保光标相关插件优先注册。创建时若未提供provider选项会直接抛错(第 L147-L149 行),因此它是一个强依赖协同 Provider 的扩展。
从变更记录看包的身份变迁
CHANGELOG 不仅是版本流水账,更记录了该扩展数次关键的"身份重定义"。
从 cursor 更名为 caret
这是该扩展最重要的一次破坏性变更,出现在 3.0.1 的 Major Changes 中(CHANGELOG.md):
Renamed
@tiptap/extension-collaboration-cursorto@tiptap/extension-collaboration-caretto clarify what kind of cursor this extension is implementing.
即:npm 包名从@tiptap/extension-collaboration-cursor改为@tiptap/extension-collaboration-caret,包描述也相应改为 "collaboration caret extension for tiptap"(见 package.json)。设计意图是明确它渲染的是"插入光标 + 选区高亮"的协作感知标记,而不是浏览器原生指针光标。
对应的升级动作:v2 项目中所有import CollaborationCursor from '@tiptap/extension-collaboration-cursor'需替换为CollaborationCaret。CHANGELOG 中 v2.x 时代的条目(如 2.4.0、2.1.0 等)均仍标注 "Version bump only for package @tiptap/extension-collaboration-cursor",佐证了 v2 阶段它叫 cursor。
底层绑定库替换:y-prosemirror → @tiptap/y-tiptap
3.0.1 与 3.0.0-next.6 的 Major Changes 记录了两件事(CHANGELOG.md):
Replaced y-prosemirror with @tiptap/y-tiptap:Tiptap 团队将 ProseMirror 与 Yjs 的桥接实现整合为自维护的@tiptap/y-tiptap包;- 构建系统切换为 tsup,不再产出 UMD 构建。
这解释了为什么本包现在以@tiptap/y-tiptap作为 peer 依赖(package.json),要求版本^3.0.7;源码中用于渲染光标的yCursorPlugin与选区默认构建器defaultSelectionBuilder正是从@tiptap/y-tiptap导入的(collaboration-caret.ts)。同时@tiptap/core与@tiptap/pm也以workspace:*级别的 peer 依赖形式同步版本。
关键缺陷修复背后的实现原理
CHANGELOG 中几次 Patch Changes 并非普通依赖升级,而是把扩展中容易被忽视的健壮性问题摆上了台面,它们都对应源码或测试中的具体处理。
3.25.0:awareness 状态为 null/undefined 时崩溃
Fix crash when awareness state value is null or undefined (e.g. after a client disconnects)
当某客户端断开后,其 awareness state 条目可能残留为null或undefined。对应处理位于 collaboration-caret.ts 的awarenessStatesToArray:
const awarenessStatesToArray = (states) => { return Array.from(states.entries()).map(([key, value]) => { if (value && value.user) { return { clientId: key, ...value.user } } return { clientId: key } }) }value && value.user的守卫保证了状态为 null/undefined 时仍能产出至少含clientId的条目,而不是抛错。该场景被 collaboration-caret.spec.ts 显式覆盖:测试构造了同时含正常用户、null、undefined的 states Map,断言editor.storage.collaborationCaret.users中这些异常条目被规约为{ clientId }形式。
3.24.0:销毁编辑器时的内存泄漏
Fix memory leak when destroying an editor while the collaboration provider stays alive (e.g. multiple editors sharing one provider). The extension's awareness
updatelistener is now removed on destroy, so the editor can be garbage collected.
多个编辑器共享同一个 provider 是常见架构(如同一个页面开两个并排编辑区)。若每个编辑器都在 provider 的 awareness 上挂update监听,而销毁时不移除,闭包会持续引用已销毁的编辑器,导致无法被 GC。
源码在addProseMirrorPlugins()中用一个带view()的 ProseMirror 插件collaborationCaretAwarenessListener来持有这个订阅(collaboration-caret.ts):编辑器创建时awareness.on('update', ...)并同步一次storage.users;编辑器销毁时view.destroy()会执行awareness.off('update', ...)并清空storage.users。测试 collaboration-caret.spec.ts 用一个可统计监听器数量的 mock awareness 验证:destroy 前监听数为 1,destroy 后归 0。
3.17.1:以 HTML 内容初始化时读取不到 doc 崩溃
Fixed CollaborationCaret crash with "Cannot read properties of undefined (reading 'doc')"... This issue affected editors initialized with HTML content, particularly when using tables.
该问题复现自 GitHub #6979:编辑器以含表格的 HTML 初始化并叠加 Collaboration 与 CollaborationCaret 时崩溃。修复方式是升级@tiptap/y-tiptap@3.0.2,在其中加入编辑器初始化阶段 state 未就绪时的守卫。仓库中的测试(collaboration-caret.spec.ts)用包含<table><tr><th>/<td>结构的 HTML 初始化编辑器,断言不抛异常。
3.10.5:部分 emoji 组合导致协同错误
Fixed collaborative editing errors with certain emoji combinations (like 🔴🟢, 😎🐈, 🟣🔵)
该问题根因在底层文档同步层而非光标渲染本身,通过将@tiptap/y-tiptap升级到稳定版 v3.0.0 解决。它对使用者的启示是:当光标/协同出现诡异行为时,优先检查@tiptap/y-tiptap是否满足 peer 依赖要求的^3.0.7(package.json)。
3.12.0:updateUser 命令不再改写 this.options
Avoid mutating
this.optionsin theupdateUsercommand.this.optionscan be a getter and is not writable; the command now updates the provider awareness directly so user updates are applied correctly.
早期实现试图把新用户信息写回this.options.user,但 options 可能是 getter、不可写。v3.12.0 后updateUser直接操作 awareness(collaboration-caret.ts):
updateUser: attributes => () => { this.options.provider.awareness.setLocalStateField('user', attributes) return true }用户属性是运行时状态,理应只存在于 awareness 而非扩展配置中。
早期 v2 阶段的修复
更早的 2.0.0-alpha.4 修复了 "retrieve awareness states after reconnect"——重连后重新拉取 awareness 状态;2.0.0-beta.22 修复了协作插件的顺序问题(fix plugin order for collab)。这提醒我们:把CollaborationCaret与Collaboration、History等插件的顺序保持文档默认即可,人为调序可能破坏协作一致性。
配置项与实战接入
综合 collaboration-caret.ts 的类型声明与默认值,该扩展的选项如下:
| 选项 | 类型 | 必填 | 说明 |
|---|---|---|---|
provider | any | 是 | Hocuspocus / TiptapCloud 的 Provider 实例,需暴露awareness;未提供时onCreate抛错 |
user | Record<string, any> | 推荐 | 当前用户初始信息,默认{ name: null, color: null },会写入本地 awareness |
render | (user) => HTMLElement | 否 | 自定义光标 DOM 构建函数,默认生成collaboration-carets__caret容器 + 姓名标签 |
selectionRender | (user) => DecorationAttrs | 否 | 自定义选区装饰属性,默认使用 y-tiptap 的defaultSelectionBuilder |
onUpdate | (users) => null | 否 | 已废弃,应改读editor.storage.collaborationCaret.users |
一个典型的接入示例(同时安装协同数据扩展与本扩展,模式与 单测 及 React Demo / Vue Demo 一致):
import { Editor } from '@tiptap/core' import Document from '@tiptap/extension-document' import Paragraph from '@tiptap/extension-paragraph' import Text from '@tiptap/extension-text' import Collaboration from '@tiptap/extension-collaboration' import CollaborationCaret from '@tiptap/extension-collaboration-caret' const editor = new Editor({ element: document.querySelector('#editor'), extensions: [ Document, Paragraph, Text, Collaboration.configure({ document: ydoc }), // 来自 Yjs CollaborationCaret.configure({ provider, // HocuspocusProvider / TiptapCloudProvider user: { name: 'John Doe', color: '#305500' }, }), ], })如果只想展示其他协作者的名字而不要整段 CSS 默认样式,可自行实现render:
render: user => { const cursor = document.createElement('span') cursor.classList.add('collaboration-carets__caret') cursor.setAttribute('style', `border-color: ${user.color}`) const label = document.createElement('div') label.classList.add('collaboration-carets__label') label.setAttribute('style', `background-color: ${user.color}`) label.insertBefore(document.createTextNode(user.name), null) cursor.insertBefore(label, null) return cursor }这就是源码中的默认实现(collaboration-caret.ts),展示了一个"彩色边框光标 + 背景色姓名标签"的最小可复制写法。
通过 Storage 读取在线用户
自 v3 起,扩展在类型声明中注册了editor.storage.collaborationCaret.users(collaboration-caret.ts),每次 awarenessupdate事件都会把全员状态同步进该数组:
const users = editor.storage.collaborationCaret.users // users: [{ clientId: 123, name: 'Alice', color: '#ff0000' }, ...] // 断线残留的异常条目会被规约为 { clientId }clientId对应当前 provider 连接;name、color 等为用户自定属性。
通过命令更新用户信息
运行时修改本人显示名或颜色,应使用updateUser命令:
editor.commands.updateUser({ name: 'Jane', color: '#0af' })旧命令user已被废弃:执行时会向控制台输出 DEPRECATED 警告并转发给updateUser(collaboration-caret.ts);onUpdate选项同理,一旦被自定义函数替换会在onCreate中收到弃用警告(第 L141-L146 行)。升级到 v3 时若在日志中看到这类[tiptap warn],说明还在使用旧 API,需迁移到新写法。
插件层实现:两个插件各司其职
addProseMirrorPlugins()最终返回两个插件(collaboration-caret.ts):
collaborationCaretAwarenessListener:负责把 awareness states 镜像到storage.users,并管理订阅生命周期以规避内存泄漏;yCursorPlugin(provider.awareness, { cursorBuilder, selectionBuilder }):来自@tiptap/y-tiptap,负责在编辑视图上绘制远端光标 DOM 与选区装饰,本扩展通过render/selectionRender注入自定义构建器。
两条插件的分工决定了该扩展的自定义能力边界:数据同步靠 Yjs awareness,视觉呈现靠 PM decoration + 自定义 DOM。
升级到 v3 的破坏性变更清单
汇总 3.0.1 Major Changes(CHANGELOG.md)及后续修复,从 v2 迁移时需要注意:
| 变更 | 说明 |
|---|---|
| 包重命名 | 安装与导入@tiptap/extension-collaboration-caret,替换旧的collaboration-cursor |
| y-prosemirror → @tiptap/y-tiptap | 需安装并保持@tiptap/y-tiptap@^3.0.7(peer 依赖),不要手动混入 y-prosemirror |
| 无 UMD 构建 | 包由 tsup 构建,仅提供 ESM/CJS;依赖 UMD 的环境需自行打包 |
| 版本锁定 | 3.28.0 起会持续 Bump y-tiptap 至最新版,3.22.4 修复了 peer 依赖解析冲突导致的安装失败,建议安装时清理锁文件后重装 |
其余多个版本主要随@tiptap/core与@tiptap/pm同步依赖版本,属于常规 Patch 升级;遇到光标相关怪异行为时,优先确认 y-tiptap 与 core/pm 三者的版本是否对齐。
相关仓库资源
- 扩展源码:collaboration-caret.ts
- 包入口:index.ts(默认导出 + 具名导出)
- 单测:collaboration-caret.spec.ts(覆盖 HTML/表格初始化、监听器清理、null 状态、无内容初始化四类边界)
- 配套数据同步扩展:extension-collaboration
- 可运行 Demo:React 版见 index.jsx,Vue 版见 index.vue,CSS 示例位于对应目录的
style.scss
【免费下载链接】tiptapThe headless rich text editor framework for web artisans.项目地址: https://gitcode.com/GitHub_Trending/ti/tiptap
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考