一、缘起:一个被忽视的协作场景
先问大家一个问题:当你需要和同事临时记录几个要点时,通常怎么做?
最常见的做法是打开微信拉群发公告,或者开个腾讯文档。但这两个方案都有痛点:
-
微信消息很快被淹没
-
腾讯文档等工具需要登录,很多人没有账号或懒得登录
于是我做了一个小工具——CollabNote。
地址:CollabNote
一句话总结:一个无需登录、打开即用的实时协作备忘录。
使用时只需三步:
-
打开页面,创建备忘录
-
点击「复制」获取分享链接
-
把链接发给任何人,所有人打开后内容实时同步
不需要注册,不需要登录,不需要任何前置操作。


二、技术选型:为什么是CRDT?
实时协作的核心问题是冲突处理。当两个人同时编辑同一份文档,A删除了第2行,B在第3行加了几个字——如果只是简单按时间顺序覆盖,结果一定是错的。
业内主要有两种方案:
| 对比维度 | OT(操作转换) | CRDT(无冲突复制数据类型) |
|---|---|---|
| 代表应用 | Google Docs | Figma、Linear |
| 核心思路 | 服务端转换冲突操作 | 数据结构层面保证一致性 |
| 实现复杂度 | 复杂,不同数据类型需不同逻辑 | 相对模块化 |
| 离线支持 | 较难 | 天然支持 |
| 服务端依赖 | 强依赖中心服务器 | 支持P2P/客户端-服务器 |
OT 的典型代表是 Google Docs,但实现非常复杂。不同数据类型需要不同的转换函数,且强依赖服务端进行冲突合并。
CRDT 的代表是 Yjs。其核心思想是:每个字符或元素都有唯一ID,删除用"墓碑标记"而非真正移除,保证插入位置的相对性始终可追溯。无论操作以什么顺序到达,最终结果一致。
CollabNote 选择了 CRDT(Yjs)方案。
为什么?
-
实现更清晰:Yjs 的 API 设计直观,社区活跃,有丰富的编辑器集成
-
服务端轻量:服务端只需转发和持久化操作,冲突合并完全在客户端完成
-
扩展性好:后续可以轻松支持离线编辑等高级特性
三、技术架构
CollabNote 的技术栈:
text
前端:React + TypeScript + Vite 编辑器:TipTap(基于 ProseMirror) 协同引擎:Yjs(CRDT) 通信协议:WebSocket(y-websocket)
架构图(简化版):
text
┌─────────┐ WebSocket ┌─────────┐
│ 用户 A │ ──── 操作更新 ────→ │ │
└─────────┘ │ 服务端 │
│ (转发/ │
┌─────────┐ │ 持久化) │
│ 用户 B │ ←─── 广播更新 ──── │ │
└─────────┘ └─────────┘
四、核心实现
4.1 Yjs + TipTap 集成
Yjs 官方提供了与 ProseMirror(TipTap 底层)的集成方案,核心代码非常精简:
typescript
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { ySyncPlugin, yCursorPlugin } from 'y-prosemirror'
// 1. 创建 Yjs 文档(CRDT 容器)
const ydoc = new Y.Doc()
// 2. 连接协同服务器
const provider = new WebsocketProvider(
'wss://your-server.com/collab',
'room-id-001',
ydoc
)
// 3. 获取共享数据结构
const yXmlFragment = ydoc.getXmlFragment('prosemirror')
// 4. 注入到 TipTap 编辑器
const editor = new Editor({
extensions: [
// ... 其他扩展
YjsExtension.configure({
yDoc: ydoc,
yXmlFragment: yXmlFragment,
provider: provider,
}),
],
})
4.2 自动保存机制
自动保存采用 500ms 防抖 策略:
typescript
// 500ms 防抖保存
const debouncedSave = useCallback(
debounce((content: string) => {
saveToServer(content)
}, 500),
[]
)
// 监听内容变化
editor.on('update', ({ editor }) => {
const content = editor.getHTML()
debouncedSave(content)
})
500ms 是一个权衡值——太短服务端压力大,太长用户感知延迟。
同时保留 Ctrl+S 手动保存,满足部分用户习惯。
4.3 Awareness:光标与在线状态
Awareness 是协同系统中非持久化的临时状态,包括光标位置、选区、用户在线状态等。
typescript
// 设置用户信息
provider.awareness.setLocalState({
user: {
name: '用户A',
color: '#ff6b6b',
},
cursor: { from: 10, to: 15 },
})
// 监听其他用户状态变化
provider.awareness.on('change', (changes) => {
// 更新界面上的光标显示
updateCursors(provider.awareness.getStates())
})
Awareness 与文档内容走不同的通道:
-
文档内容:需要持久化、强一致性,通过 CRDT 同步
-
Awareness:不需要持久化、最终一致即可,通过独立广播通道传输
五、踩坑与经验
5.1 光标位置保持
当远程用户的更新到来时,如果直接替换编辑器内容,会导致当前用户的光标跳转。
解决方案:在应用远程变更前保存光标位置,更新完成后恢复。
TipTap 的 setContent(content, false) 中 false 参数用于阻止触发新的事件,配合光标位置保存基本解决问题。
5.2 删除确认
「清空」和「删除」两个操作都需要二次确认。原因很简单——协作场景中别人可能在编辑,你直接清空内容会严重影响体验。
5.3 关于"无需登录"的权衡
无需登录是 CollabNote 的核心体验优势,但也带来一些代价:
-
没有用户身份,无法做权限控制和历史追溯
-
数据安全依赖用户自觉(不建议记录敏感信息)
对于轻量级临时协作这个定位,这些权衡是可接受的。
六、后续计划
-
更丰富的文本格式:目前是纯文本,后续支持 Markdown 或更富的格式
-
历史版本回滚:基于操作日志重放实现
-
移动端适配:完善移动端体验
-
导出功能:支持导出为 Markdown、TXT
七、写在最后
CollabNote 的初衷很简单:做一个简单到不用思考的协作工具。
不需要登录,不需要学习,打开就能用。它不会成为下一个 Notion,但它填补了"临时会议记录、头脑风暴、快速分享"这个场景的空缺。
如果你有类似的需求,欢迎试试。如果你对技术实现感兴趣,欢迎交流讨论。
项目地址:CollabNote
欢迎在评论区留言交流,觉得有用的话点个赞支持一下 😊

481

被折叠的 条评论
为什么被折叠?



