什么是协同编辑
协同编辑的核心挑战是如何处理多个用户同时编辑同一份文档时产生的冲突。目前主要有以下几种解决方案:
1. 操作转换(Operational Transformation, OT)
- 将用户的编辑操作转换为可以安全应用的操作
- 需要复杂的转换算法来处理并发操作
- Google Docs 早期版本使用的技术
2. 无冲突复制数据类型(CRDT)
- 数据结构本身保证操作的可交换性和结合性
- 无需中心化的冲突解决机制
- Yjs 采用的技术方案
3. Yjs 的 CRDT 实现
Yjs 使用了一种称为 "Y.CRDT" 的特殊 CRDT 实现:
makefile
用户A: 插入字符 'H' 在位置 0
用户B: 插入字符 'i' 在位置 0
传统方式: 需要协调谁先执行,可能产生冲突
CRDT方式: 每个操作都有唯一标识,最终结果确定且一致
Yjs 协同流程:
- 用户进行编辑操作
- Yjs 将操作转换为 CRDT 操作
- 操作通过网络传输给其他用户
- 其他用户接收并应用操作
- 所有用户最终达到一致状态
项目初始化
创建项目
sh
pnpm create vite@latest
# 运行之后选择 react ts
进入项目目录并安装基础依赖:
sh
cd your-project-name
pnpm install
WebSocket协同编辑
安装依赖
安装 tiptap 相关依赖:
sh
pnpm add @tiptap/react @tiptap/extension-document @tiptap/extension-paragraph @tiptap/extension-text @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor
安装 yjs WebSocket 相关依赖:
sh
pnpm add yjs y-websocket
实现步骤
1. 创建随机用户函数
首先创建一个生成随机用户名和颜色的工具函数:
ts
function getRandomUser() {
const names = [
'Alice',
'Bob',
'Charlie',
'Diana',
'Eve',
'Frank',
'Grace',
'Henry',
'Ivy',
'Jack'
]
const colors = [
'#ff6b6b',
'#4ecdc4',
'#45b7d1',
'#96ceb4',
'#feca57',
'#ff9ff3',
'#54a0ff',
'#5f27cd',
'#c44569',
'#f8b500'
]
const randomName = names[Math.floor(Math.random() * names.length)]
const randomColor = colors[Math.floor(Math.random() * colors.length)]
return {
name: randomName,
color: randomColor
}
}
2. 创建 YDoc 和 WebSocket Provider 实例
ts
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
const ydoc = new Y.Doc()
const provider = new WebsocketProvider('ws://localhost:1234', 'room', ydoc)
3. 配置编辑器扩展
ts
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 CollaborationCursor from '@tiptap/extension-collaboration-cursor'
const extensions = [
Document,
Text,
Paragraph,
Collaboration.configure({
document: ydoc
}),
CollaborationCursor.configure({
provider,
user: {
name: getRandomUser().name,
color: getRandomUser().color
},
})
]
4. 创建组件
使用 EditorProvider
包装器来实现协同编辑:
ts
import { EditorProvider } from '@tiptap/react'
export default function TiptapWs() {
const content = '' // 初始内容
return (
<EditorProvider
content={content}
extensions={extensions}
/>
)
}
注意: 在这个实现中,我们使用了 EditorProvider
而不是 useEditor
hook,这是一种更简洁的方式来创建协同编辑器。
5. 启动 WebSocket 服务
安装 WebSocket 服务器依赖:
sh
pnpm add @y/websocket-server -D
启动 WebSocket 服务:
sh
HOST=0.0.0.0 PORT=1234 npx y-websocket
现在多个用户可以通过 WebSocket 连接进行实时协同编辑。
如图:

WebRTC协同编辑
WebRTC 实现的协同编辑不需要中心化的服务器,通过点对点连接实现协同。
安装依赖
安装 tiptap 相关依赖:
sh
pnpm add @tiptap/react @tiptap/extension-document @tiptap/extension-paragraph @tiptap/extension-text @tiptap/extension-placeholder @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor
安装 yjs WebRTC 相关依赖:
sh
pnpm add yjs y-webrtc
实现步骤
1. 创建 YDoc 和 WebRTC Provider 实例
ts
import * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'
const ydoc = new Y.Doc()
const provider = new WebrtcProvider('tiptap-collaboration-cursor-extension', ydoc)
重要说明:
- 第一个参数是房间名称,相同房间名称的用户会连接到同一个协同会话
- WebRTC 使用 P2P 连接,不需要中心化服务器
2. 完整组件实现
ts
import { EditorContent, useEditor } from '@tiptap/react'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Placeholder from '@tiptap/extension-placeholder'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'
function getRandomUser() {
// ... 与上面相同的实现
}
const ydoc = new Y.Doc()
const provider = new WebrtcProvider('tiptap-collaboration-cursor-extension', ydoc)
export default function TiptapWebRTC() {
const editor = useEditor({
extensions: [
Document,
Paragraph,
Text,
Collaboration.configure({
document: ydoc
}),
CollaborationCursor.configure({
provider,
user: {
name: getRandomUser().name,
color: getRandomUser().color
}
}),
Placeholder.configure({
placeholder: 'Write something ... It'll be shared with everyone else looking at this example.'
})
]
})
return <EditorContent editor={editor} />
}
如图:

相关样式文件
css
.tiptap {
:first-child {
margin-top: 0;
}
/* Placeholder (at the top) */
p.is-editor-empty:first-child::before {
color: var(--gray-4);
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
p {
word-break: break-all;
}
/* Give a remote user a caret */
.collaboration-cursor__caret {
border-left: 1px solid #0d0d0d;
border-right: 1px solid #0d0d0d;
margin-left: -1px;
margin-right: -1px;
pointer-events: none;
position: relative;
word-break: normal;
}
/* Render the username above the caret */
.collaboration-cursor__label {
border-radius: 3px 3px 3px 0;
color: #0d0d0d;
font-size: 12px;
font-style: normal;
font-weight: 600;
left: -1px;
line-height: normal;
padding: 0.1rem 0.3rem;
position: absolute;
top: -1.4em;
user-select: none;
white-space: nowrap;
}
}
WebRTC vs WebSocket 对比
特性 | WebRTC | WebSocket |
---|---|---|
连接方式 | 点对点 (P2P) | 客户端-服务器 |
服务器需求 | 无需中心化服务器 | 需要 WebSocket 服务器 |
延迟 | 通常更低 | 取决于服务器性能 |
可扩展性 | 受浏览器连接数限制 | 服务器可处理大量连接 |
网络穿透 | 可能需要 STUN/TURN | 简单的 HTTP 升级 |
适用场景 | 小团队协作 | 大规模协同编辑 |
最佳实践和注意事项
1. 用户状态管理
建议为每个用户分配持久化的身份标识:
ts
// 可以从 localStorage 或用户登录信息获取
const userId = localStorage.getItem('userId') || generateUserId()
const userName = localStorage.getItem('userName') || 'Anonymous'
CollaborationCursor.configure({
provider,
user: {
name: userName,
color: getUserColor(userId) // 基于用户ID生成固定颜色
}
})
2. 错误处理和重连
ts
// WebSocket 重连处理
provider.on('status', (event) => {
console.log('Connection status:', event.status) // 'connecting' | 'connected' | 'disconnected'
})
provider.on('connection-error', (event) => {
console.error('Connection error:', event)
// 实现重连逻辑
})
3. 数据持久化
ts
// 可以将文档内容持久化到数据库
ydoc.on('update', () => {
const content = editor.getHTML()
// 保存到数据库或本地存储
saveDocument(content)
})
4. 性能优化
- 使用
Collaboration
扩展的fragment
选项来指定协同的文档片段 - 合理设置
CollaborationCursor
的更新频率 - 对于大型文档,考虑使用文档分片
5. 安全考虑
- 在生产环境中,WebSocket 服务器应该添加身份验证
- 考虑添加内容过滤和权限控制
- WebRTC 连接可能需要配置 STUN/TURN 服务器来处理 NAT 穿透
总结
本文展示了使用 Tiptap 和 Yjs 实现协同编辑的两种方案:
- WebSocket 方案:适合需要中心化控制、大规模用户协同的场景
- WebRTC 方案:适合小团队、低延迟需求的 P2P 协同场景
两种方案都提供了完整的协同编辑功能,包括实时文本同步、光标位置共享、用户状态显示等。选择哪种方案取决于具体的应用场景和技术要求。