在 Google Docs、Figma 这类应用中,多人同时编辑、实时同步、离线可用已经成为“标配”。这些能力的背后,离不开一个关键技术:协同数据同步模型。
今天要介绍的 Yjs,正是当前前端领域中性能最强、生态最成熟的协同编辑底层库之一。
一、什么是 Yjs?
Yjs 是一个高性能的 CRDT(无冲突复制数据类型)实现,用于构建可自动同步的协作应用程序。
简单来说,Yjs 解决的是:
多个客户端在不同时间、不同顺序、甚至离线状态下修改同一份数据,最终还能 自动合并且永远不冲突。
CRDT 是什么?
CRDT(Conflict-free Replicated Data Type)是一类数据结构,具备以下特性:
- 支持多端并发修改
- 不依赖中心服务器进行冲突裁决
- 修改顺序无关,最终一定能收敛到一致状态
- 非常适合分布式、实时协作、离线优先场景
Yjs 正是 CRDT 思想在工程实践中的一次“极致实现”。
二、Yjs 的核心设计理念
1️⃣ 文档(Y.Doc)
Yjs 的一切,都围绕 Y.Doc 展开。
import * as Y from 'yjs'
// 创建一个 Yjs 文档
const ydoc = new Y.Doc()
Y.Doc 是一个共享数据容器,内部可以包含多个共享类型(Shared Types),它们会自动同步、合并和持久化。
2️⃣ 共享类型(Shared Types)
Yjs 将 CRDT 模型暴露为类似原生数据结构的共享类型:
| 类型 | 说明 |
|---|---|
Y.Text | 文本(支持富文本) |
Y.Array | 数组 |
Y.Map | 键值映射 |
Y.Xml | 类 DOM 结构 |
示例:Y.Map
const ymap = ydoc.getMap()
ymap.set('keyA', 'valueA')
模拟一个“远端用户”并产生冲突修改:
const ydocRemote = new Y.Doc()
const ymapRemote = ydocRemote.getMap()
ymapRemote.set('keyB', 'valueB')
// 合并远端更新
const update = Y.encodeStateAsUpdate(ydocRemote)
Y.applyUpdate(ydoc, update)
console.log(ymap.toJSON())
// => { keyA: 'valueA', keyB: 'valueB' }
👉 没有冲突、无需锁、无需手写合并逻辑。
三、五分钟上手:打造一个协作式编辑器
Yjs 本身并不提供编辑器,它采用的是 “编辑器绑定”机制。
1️⃣ 选择编辑器:Quill
npm i quill quill-cursors
npm i yjs y-quill
2️⃣ 绑定编辑器与 Yjs 文档
import * as Y from 'yjs'
import { QuillBinding } from 'y-quill'
// 创建文档
const ydoc = new Y.Doc()
// 定义共享文本
const ytext = ydoc.getText('quill')
// 绑定 Quill 编辑器
const binding = new QuillBinding(ytext, quill)
此时:
ytext是 CRDT 文本结构- Quill 的编辑状态 ⇄ Y.Text 自动双向同步
- 并发编辑自动合并
切换编辑器(如 Monaco、CodeMirror)时,只需替换绑定层即可。
四、连接其他客户端:Provider(提供商)
编辑器现在只能“本地协作”,还无法和其他用户同步。
这一步由 Provider(提供商) 负责。
常见网络提供商
| Provider | 特点 |
|---|---|
y-webrtc | 点对点,无需服务器,适合 Demo |
y-websocket | 常规生产方案 |
y-webxdc / y-dat | 去中心化 |
| Liveblocks / Tiptap / Y-Sweet | 商业托管 |
使用 y-webrtc 示例
import { WebrtcProvider } from 'y-webrtc'
const provider = new WebrtcProvider(
'quill-demo-room',
ydoc
)
- 相同 room-name 的客户端会自动同步
- Provider 可并行使用,形成冗余链路
- 切换 Provider 无需改动文档结构
五、感知(Awareness):光标、在线状态、存在感
协同编辑不仅是“内容同步”,还需要协作感知:
- 谁在线
- 光标在哪里
- 当前正在编辑什么
Yjs 提供了一个 轻量级 Awareness CRDT:
- 不写入文档
- 不做持久化
- 用户离线后自动清理
- 所有官方 Provider 均支持
常见用途:
- 光标位置
- 用户名、头像、颜色
- 选区范围
六、离线支持:y-indexeddb
Yjs 是天然离线优先的。
只需引入 y-indexeddb:
import { IndexeddbPersistence } from 'y-indexeddb'
const ydoc = new Y.Doc()
const persistence = new IndexeddbPersistence('my-room', ydoc)
persistence.once('synced', () => {
console.log('本地内容已加载')
})
效果:
- 修改自动写入 IndexedDB
- 离线可编辑
- 下次上线仅同步增量
- 节点之间可“反向修复”丢失数据
配合 Service Worker,甚至可以实现 完全离线可用的协作应用。
七、事务(Transaction):性能与一致性的关键
Yjs 的所有修改都发生在 事务 中。
ydoc.transact(() => {
ymap.set('food', 'pancake')
ymap.set('number', 31)
})
好处:
- 合并多次修改
- 减少更新广播
- 减少 observer 触发次数
- 显著提升性能
事务事件顺序
beforeTransaction- 数据变更
beforeObserverCallsobserve / observeDeepafterTransactionupdate(Provider 广播)
👉 大量修改时,一定要手动包事务。
八、数据建模最佳实践
1️⃣ 一个 YDoc 还是多个?
考虑维度:
- 数据是否经常一起使用
- 权限是否不同
- 是否需要独立历史/撤销
- 是否需要按需加载
2️⃣ 推荐模式
- 使用一个顶级
Y.Map('data') - 子实体通过 ID 管理
- 或结构与内容分离(列表 Doc + 内容 Doc)
ydoc.getMap('data').get('page-1')
九、生态系统与语言支持
编辑器支持
- ProseMirror
- Tiptap
- Monaco
- Quill
- CodeMirror
- Remirror
数据库 Provider
- y-indexeddb
- y-leveldb
- y-redis
多语言实现
- Y-CRDT(Rust) → WASM / Python / Swift / Java
- Y-Octo(Rust,线程安全)
- Ycs(C#)
十、为什么选择 Yjs?
✅ 无中心、无限扩展
✅ 极致性能(目前最快的 CRDT 实现)
✅ 网络无关、Provider 可插拔
✅ 离线优先、本地优先
✅ 丰富成熟的生态
✅ 工程实践充分验证
结语
Yjs 并不是一个“编辑器库”,而是一个 协同数据模型的基础设施。
一旦你理解了:
“编辑器只是视图,Yjs 才是状态源”
你就可以用它构建的不只是协同编辑器,而是:
- 协作白板
- 多人建模工具
- 实时表单
- 去中心化应用
- 本地优先系统
找个地方开始用 Yjs 吧。你会发现,协同并没有那么复杂。 🚀
文章评论