跳到主要内容

Hocuspocus:基于 Y.js 的实时协作后端详解

💡 **一句话理解:**Hocuspocus 是构建在 Y.js 之上的实时协作 WebSocket 后端。Y.js 负责无冲突地合并数据,Hocuspocus 负责连接、房间、同步、鉴权、在线状态、持久化和扩容。

1. 它能解决什么问题

传统编辑器通常把整篇 JSON 或 HTML 当作一个值保存。多人同时编辑时,后保存的人很容易覆盖先保存的人;仅靠 WebSocket 广播最新内容,也无法正确处理并发、断线和乱序消息。

Hocuspocus 主要解决以下问题:

  • 多人实时编辑冲突:两个用户同时输入、删除或格式化内容时,由 Y.js 的 CRDT 更新自动合并,不依赖“最后写入者获胜”。
  • 跨设备同步:同一账号在电脑、手机或多个标签页打开同一文档时,保持内容一致。
  • 断线重连与离线编辑:客户端可先在本地修改,恢复连接后交换缺失更新并收敛到相同状态;配合 IndexedDB Provider 可实现更完整的离线优先体验。
  • 协作者状态:同步在线用户、名称、颜色、光标与选区,即 Awareness / Presence。
  • 协作文档鉴权:连接房间前校验 token、用户身份、文档权限,并可将连接设为只读。
  • 文档持久化:从数据库加载 Y.js 状态,并在变更后以防抖方式保存。
  • 多实例扩容:多个 Hocuspocus 实例之间通过 Redis 转发文档更新和 Awareness 状态。
  • 复用现有业务系统:通过生命周期 Hooks 接入用户系统、权限服务、数据库、Webhook 和审计逻辑。

典型场景

  • 类 Notion、Google Docs 的协作文档
  • Tiptap / ProseMirror 富文本编辑器
  • Monaco 代码编辑器、表格、白板、表单设计器
  • 评论草稿、项目状态、应用配置等共享状态
  • 需要“先离线工作、后自动同步”的应用

它不直接负责什么

Hocuspocus 不是富文本编辑器,也不是业务数据库。它不会替你提供工具栏、文档列表、成员管理、版本发布流程或业务权限模型。通常需要组合:

  • Tiptap / ProseMirror:编辑器和文档 Schema
  • Y.js:共享数据结构与冲突合并
  • Hocuspocus Provider:浏览器端连接和协议
  • Hocuspocus Server:WebSocket 协作后端
  • PostgreSQL / S3 / SQLite 等:长期持久化
  • Redis:多实例实时消息同步,不负责长期存储

2. 工作原理

  1. 每个客户端维护一个本地 Y.Doc
  2. Tiptap 的 Collaboration 扩展把 ProseMirror 文档绑定到 Y.Doc 中的共享 Fragment。
  3. 用户操作产生增量 Y.js Update,而不是反复发送整篇文档。
  4. Provider 通过 WebSocket 把 Update 发给 Hocuspocus Server。
  5. Server 将更新应用到房间文档并广播给其他连接。
  6. 其他客户端应用 Update;即使更新重复、延迟或乱序,最终也会收敛到一致状态。
  7. Server 在适当时机将 Y.js 二进制状态持久化。

🧠 **关键区别:**WebSocket 只解决“怎么传”;Y.js 解决“并发修改怎么合并”;Hocuspocus 把连接管理、协议、鉴权、生命周期和生产化能力封装起来。

3. 核心对象

对象职责
Y.Doc一份协作文档的 CRDT 容器,可以包含 XML Fragment、Map、Array、Text 等共享类型。
documentName / name房间和文档标识,例如 tenant:42:document:1001;同名客户端会进入同一协作空间。
HocuspocusProvider客户端 Provider,负责连接、认证、同步、重连、Awareness 和多文档复用连接。
Hocuspocus Server服务端,管理 WebSocket、房间内存文档、Hooks、扩展及广播。
Awareness短暂的在线状态,如用户信息和光标;不应当作为业务数据持久化。

4. 最小可运行示例

4.1 安装服务端

Hocuspocus v4 需要 Node.js 22+。服务端与 Provider 应使用相互匹配的主版本。

npm install @hocuspocus/server
// server.ts
import { Server } from '@hocuspocus/server'

const server = new Server({
port: 1234,
})

server.listen()

启动后得到 ws://localhost:1234。这只适合验证同步,尚未加入鉴权和持久化。

4.2 安装客户端与 Tiptap 扩展

npm install \
yjs y-prosemirror \
@hocuspocus/provider \
@tiptap/core @tiptap/pm @tiptap/starter-kit \
@tiptap/extension-collaboration \
@tiptap/extension-collaboration-caret
import * as Y from 'yjs'
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCaret from '@tiptap/extension-collaboration-caret'
import { HocuspocusProvider } from '@hocuspocus/provider'

const ydoc = new Y.Doc()

const provider = new HocuspocusProvider({
url: 'ws://localhost:1234',
name: 'document-1001',
document: ydoc,
token: 'your-access-token',
})

const editor = new Editor({
extensions: [
StarterKit.configure({
// Collaboration 自带基于 Y.js 的历史管理,避免与本地历史冲突
undoRedo: false,
}),
Collaboration.configure({
document: ydoc,
}),
CollaborationCaret.configure({
provider,
user: {
name: 'powerfulyang',
color: '#7c3aed',
},
}),
],
})

用两个浏览器窗口打开相同 name 的文档,即可观察实时同步;使用不同 name 则进入不同房间。

4.3 生命周期清理

在 React/Vue 组件卸载或切换文档时,应销毁旧实例,避免重复连接和监听器泄漏:

editor.destroy()
provider.destroy()
ydoc.destroy()

切换文档时,更稳妥的做法是为新文档建立新的 Y.Doc、Provider 和 Editor 实例,而不是把旧实例强行复用到另一个房间。

5. 生产环境鉴权

客户端发送 token:

const provider = new HocuspocusProvider({
url: 'wss://collab.example.com',
name: `tenant:${tenantId}:document:${documentId}`,
document: ydoc,
token: async () => getFreshAccessToken(),
})

服务端在 onAuthenticate 中校验“用户能否访问当前文档”,而不只是判断 token 是否有效:

import { Server } from '@hocuspocus/server'

const server = new Server({
port: 1234,

async onAuthenticate({ token, documentName, connection }) {
const user = await verifyAccessToken(token)
const permission = await getDocumentPermission(user.id, documentName)

if (!permission) {
throw new Error('Not authorized')
}

connection.readOnly = permission === 'read'

// 返回值会成为后续 hooks 可使用的 context
return {
userId: user.id,
tenantId: user.tenantId,
}
},
})

鉴权注意事项

  • 不要仅信任客户端传来的 documentName、用户 ID 或只读标记。
  • 在服务端同时验证租户、文档和用户权限,防止通过猜测房间名越权访问。
  • 生产环境使用 wss://,并限制允许的 Origin。
  • 短期 token 过期后需要刷新;v4 可通过 token 同步机制重新校验并更新只读权限。
  • 不要把敏感业务信息放进 Awareness,因为它会广播给房间中的其他客户端。

6. 文档持久化

6.1 使用 Hooks

import * as Y from 'yjs'
import { Server } from '@hocuspocus/server'

const server = new Server({
port: 1234,

async onLoadDocument({ documentName }) {
const binary = await storage.get(documentName)
const document = new Y.Doc()

if (binary) {
Y.applyUpdate(document, binary)
}

return document
},

async onStoreDocument({ documentName, document }) {
const binary = Y.encodeStateAsUpdate(document)
await storage.set(documentName, Buffer.from(binary))
},
})

onStoreDocument 是适合保存文档的防抖 Hook;不要在每个字符变化时同步写数据库。也可以使用官方 Database、SQLite 或 S3 扩展。

6.2 应该存什么

优先保存 Y.encodeStateAsUpdate(document) 得到的 Uint8Array / 二进制数据。加载时取回同一份 Y.js 状态并应用到 Y.Doc

不要把协作数据只当成普通 JSON 循环覆盖,否则可能:

  • 丢失 CRDT 历史与状态向量信息
  • 在初始化和同步时重复插入内容
  • 无法正确合并离线期间产生的更新

如果业务需要全文检索、预览、导出或服务端渲染,可以额外生成 Tiptap JSON / HTML 作为派生快照,但应明确哪一份数据是协作真相源。

6.3 初始化旧数据

已有 JSON / HTML 内容迁移到协作模型时,只应在文档尚不存在时转换一次,例如使用 Tiptap / ProseMirror Transformer 生成初始 Y.Doc。不要让每个新连接都重复把旧 JSON 写入 Y.Doc。

7. Awareness:用户与光标

Awareness 用于易失状态:

provider.setAwarenessField('user', {
id: currentUser.id,
name: currentUser.name,
color: '#0ea5e9',
})

适合放入:

  • 用户显示名、头像和颜色
  • 当前光标或选区
  • 当前查看的子页面、编辑模式
  • “正在输入”等瞬时状态

不适合放入:权限真相、文档内容、审批状态等必须持久化和可信的数据。用户断开后,对应 Awareness 状态会消失。

8. 离线优先

Hocuspocus 负责重新联网后的服务端同步;如果希望刷新页面甚至关闭浏览器后仍保留本地文档,可增加 y-indexeddb

npm install y-indexeddb
import { IndexeddbPersistence } from 'y-indexeddb'

const localPersistence = new IndexeddbPersistence(
'document-1001',
ydoc,
)

await localPersistence.whenSynced

同一个 Y.Doc 可以同时连接 IndexedDB Provider 与 Hocuspocus Provider:本地缓存负责离线恢复,WebSocket Provider 负责远程同步。

首次渲染时要区分:

  • 本地 IndexedDB 是否已恢复
  • WebSocket 是否已连接
  • 服务端文档是否已完成同步

否则可能在同步完成前错误地展示“空文档”或执行初始化逻辑。

9. 多实例与 Redis

当单实例无法承载连接数、需要高可用或滚动发布时,可在负载均衡器后运行多个实例,并加入 Redis 扩展:

npm install @hocuspocus/extension-redis

Redis 负责让连接在不同实例上的用户仍能收到彼此的更新和 Awareness 消息。

⚠️ **Redis 扩展不负责持久化文档。**仍需 Database / S3 / SQLite 等持久化方案。并且消息会被所有相连实例处理;若主要瓶颈是 CPU 或内存,可考虑按 documentName 分片到彼此独立的实例,而不是让所有实例共享全部消息。

10. React 使用建议

Hocuspocus v4 提供 @hocuspocus/provider-react,可管理 WebSocket 连接、房间生命周期,并订阅连接、同步和 Awareness 状态。对于 React 18/19 和 Strict Mode,它通常比在组件渲染过程中手动 new HocuspocusProvider() 更安全。

如果手动管理实例:

  • useMemo 或专门的生命周期层创建,而不是每次 render 都创建。
  • Effect cleanup 中销毁 Provider。
  • documentId 改变时彻底重建当前房间实例。
  • 避免多个 Editor 误用同一个可变 Provider 状态。
  • UI 应区分 connectingconnectedsynceddisconnectederror

11. 常见坑

11.1 把初始 content 和 Collaboration 同时当作真相源

协作文档应由 Y.Doc 提供内容。若每个客户端启动时又向编辑器传入相同初始 content,可能出现重复内容或覆盖时序问题。初始化只做一次,之后从持久化的 Y.Doc 加载。

11.2 同时启用两套 Undo / Redo

Tiptap Collaboration 使用 Y.js 的协作历史。StarterKit 自带的本地 UndoRedo 应关闭,否则撤销栈可能互相冲突。

11.3 保存 JSON 而不是 Y.js 二进制状态

JSON 适合展示和业务读取,但不能完整替代 CRDT 状态。把二进制 Y.js 状态作为协作真相源,JSON/HTML 作为派生数据。

11.4 documentName 设计不安全

不要只用可枚举的数字 ID。推荐包含租户与资源类型,并始终在服务端做授权:

tenant:{tenantId}:document:{documentId}

房间名是路由标识,不是权限机制。

11.5 在 Serverless Function 中直接启动长连接服务

Hocuspocus 需要稳定的 WebSocket 长连接和进程生命周期。普通按请求启动、随时冻结的函数环境通常不合适。应使用支持 WebSocket 的常驻服务、容器或受支持的边缘运行环境,并确认平台限制。

11.6 把 Redis 当数据库

Redis 扩展只转发多实例消息。没有长期存储时,所有实例重启后文档仍可能丢失。

11.7 客户端过早执行初始化

connected 只代表 WebSocket 已连接,不一定代表文档已同步。依赖完整内容的逻辑应等待同步状态。

11.8 Schema 不一致

协作用户必须使用兼容的 Tiptap / ProseMirror Schema。若不同版本客户端对节点和属性理解不同,可能丢失或无法渲染内容。发布新 Schema 时应制定向后兼容与迁移策略。

11.9 未限制大文档和恶意连接

生产环境应考虑 token 校验、Origin、连接数、消息大小、文档大小、速率限制、超时、日志和异常隔离。

12. 什么时候选 Hocuspocus

适合选择:

  • 已使用或计划使用 Tiptap、ProseMirror、Y.js
  • 需要自托管和掌控数据
  • 需要鉴权、持久化 Hook、Awareness 与横向扩展
  • 不想自行实现 Y.js WebSocket 协议和房间生命周期

可能不必选择:

  • 只有单用户编辑和普通自动保存
  • 只需要服务器向客户端单向推送通知
  • 团队希望完全托管,不愿运维 WebSocket 与数据库
  • 业务要求严格的“锁定后才能编辑”,而非并发合并;此时应优先实现锁和审批流程

与其他方案的区别

方案适合场景特点
Hocuspocus生产级、自托管 Y.js 协作后端Hooks、鉴权、持久化扩展、Awareness、Redis 扩容
y-websocket 基础服务原型、学习或自行二次开发更轻量,但生产能力通常需要自己补齐
WebRTC Provider小规模 P2P 尝试客户端之间传输,服务端可见性与集中控制较弱
Tiptap Collab希望使用托管服务减少自建与运维成本

13. 推荐落地路线

  1. 验证同步:单实例 + 内存文档,两个浏览器测试并发编辑。
  2. 确定文档标识:设计带租户边界的 documentName
  3. 接入鉴权:实现 onAuthenticate,覆盖无权限、只读、token 过期。
  4. 接入持久化:保存并恢复 Y.js 二进制状态,测试进程重启。
  5. 完善编辑器:Collaboration、CollaborationCaret、协作 Undo/Redo。
  6. 测试异常网络:断网编辑、乱序、刷新、重复连接、长时间离线。
  7. 加入可观测性:连接数、房间数、同步耗时、保存失败、文档大小。
  8. 压力与故障测试:大文档、多协作者、滚动发布和实例故障。
  9. 按瓶颈扩容:需要跨实例消息时用 Redis;CPU/内存问题可按文档分片。

14. 官方资料


**版本备注:**本文按照 2026 年 7 月可用的 Hocuspocus v4 文档整理;升级时应检查 v4 Release Notes、Node.js 版本要求,以及 Server、Provider 与扩展包的版本兼容性。