Yjs
Yjs 是一个基于 CRDT 的高性能协作编辑框架,它将用户对文档的修改转化为增量更新操作,并通过内部的数据结构进行冲突处理与合并。
Yjs 可以通过 WebSocket、WebRTC 等通信机制实现多方数据同步。开发者只需使用其提供的 API 即可轻松实现文本、数组等结构的实时协作,复杂的合并逻辑与光标状态同步等均由 Yjs 自动处理。
架构设计

从上到下分别是:编辑器集成层、Yjs 核心层、通信与存储层。
编辑器适配层
这一层是用户直接交互的前端编辑器,例如:
- ProseMirror、Tiptap(富文本编辑器)
- Slate(React 编辑器)
- Monaco(VS Code 内核的代码编辑器)
Yjs 为这些编辑器提供了专用的绑定模块(类似于驱动程序),用于监听用户在编辑器中的操作(如文本插入、删除),并将其同步到 Yjs 的共享文档中。这样,用户的本地编辑行为会实时转换为可传播的 CRDT 更新操作,进而同步给其他协作者。
Yjs 支持大多数 主流编辑器的接入。
因此,为了实现协同能力,每种编辑器都需要开发一个绑定层,用于在编辑器的数据结构与 Yjs 的文档结构之间进行双向转换(同步变更与应用变更)。这个绑定层正是实现协作功能的桥梁(起到一个连接编辑器和 Yjs 的桥梁作用)。
- y-prosemirror:用于 ProseMirror 编辑器。
- y-monaco:用于 Monaco 编辑器。
- y-quill:用于 Quill 编辑器。
- y-codemirror:用于 CodeMirror 编辑器。
Yjs 核心
这一层是整个系统的核心,Yjs 负责以下任务:
- 管理文档的状态副本(即 CRDT 文档)。
- 将用户操作转化为 CRDT 操作。
- 自动合并并发编辑,解决冲突。
- 管理数据结构,如
Y.Text,Y.Map,Y.Array,Y.Xml。
所有编辑器的变更都会同步到这里,然后统一进行处理和广播,是整个协作系统的中枢神经。
通信与存储模块
Yjs 本身不包含网络通信逻辑,而是通过插件方式提供以下通信与持久化能力:
- y-protocols:定义同步协议与协作状态(如光标位置、用户状态)的传输格式。
- y-websocket:使用 WebSocket 实现基于服务器的实时通信。
- y-webrtc:使用 WebRTC 实现点对点通信,无需中间服务器。
- y-indexeddb:在浏览器中进行本地持久化,支持离线编辑与重连恢复。
- y-redis:在多个 WebSocket 服务器之间同步更新(适用于负载均衡场景)。
开发者可以根据应用需求灵活选择这些模块进行组合,构建出具备离线能力、实时同步能力、跨设备同步能力的协作系统。
数据流
Yjs 的整个数据流向如下所示:
Yjs优点
冲突处理可靠
Yjs 基于 CRDT 数据结构 实现分布式冲突自动合并机制,具备以下特性:
- 强一致性:即使并发编辑也能最终达成一致状态。
- 无需中央服务器协调:天然支持去中心化。
- 天然处理网络延迟/离线重连:变更按因果顺序合并,无需额外冲突解析。
与传统的 OT(Operational Transformation)算法相比,CRDT 在复杂网络环境下更稳健,适用于异步协作和离线编辑。
协作者状态同步
Yjs 内置 Awareness 模块,用于同步协作者的界面状态信息:
- 光标位置、文本选区。
- 用户名、用户颜色标识。
- 在线/离线状态。
这一机制让协作者能够感知他人的存在,提升协作体验,有效避免操作冲突。
离线编辑支持
Yjs 是天然支持离线编辑的框架:
- 所有变更都通过本地 CRDT 数据结构记录。
- 在恢复联网后可无缝同步给其他协作者。
- 通常配合
y-indexeddb实现 浏览器端持久化。
离线编辑 + 自动同步 是构建 Local-First 应用的基础能力。
版本快照与历史恢复
Yjs 提供轻量级的文档快照机制:
- 快照是结构性的,不需要保存全量内容。
- 可用于实现版本回溯、撤销恢复等功能。
- 可将快照序列化保存到数据库,便于持久化。
高并发协作能力
Yjs 针对性能优化良好,适用于高并发场景:
- 文档数据结构增量压缩。
- 支持垃圾回收(减少内存占用)。
- 实测可支持 几十到上百人 实时编辑(具体上限与文档规模、GC 策略有关)。
在配合合适的通信层(如 WebSocket 广播服务)时,Yjs 表现出极高的扩展性。
插件化生态
Yjs 拥有丰富的绑定与适配模块:
| 编辑器 | 对应绑定模块 |
|---|---|
| ProseMirror / Tiptap | y-prosemirror |
| Quill | y-quill |
| Monaco(代码编辑器) | y-monaco |
| Slate | y-slate |
| CodeMirror | y-codemirror.next |
同时也有配套的通信模块(y-websocket、y-webrtc)和存储模块(y-indexeddb、y-leveldb 等),可以灵活组合使用。
Yjs 用工程化方式实现了 CRDT 协作能力,是构建现代多人协作应用(富文本、代码、图形)的强大基石。借助 Yjs,开发者可以快速实现支持离线、实时同步和冲突自动合并的多人协作功能。
对比
Automerge 和 Yjs 两者简单对比:
| 对比点 | Automerge | Yjs |
|---|---|---|
| 性能 | 较慢(v2 有改进) | 非常快(结构优化+GC) |
| 内存使用 | 较大(因保留完整历史) | 较小(支持垃圾回收) |
| 社区活跃度 | 中等 | 高 |
| 插件生态 | 较少 | 非常丰富(尤其是编辑器方向) |
| 合并冲突处理 | 清晰但占空间 | 高效但实现复杂 |
Yjs官网
最后是关于 Yjs 的官网:
第一个是 Yjs 的正式官网,由作者 Kevin Jahns 维护,特点如下:
- 权威文档:API、使用方式、通信模块等都有说明
- 代码示例:含 TypeScript 支持、协作代码演示
- 插件介绍:如 y-websocket、y-indexeddb、y-prosemirror 等
- 持续更新:是社区开发者首选参考站点
- 适合:技术人员、框架集成、深入研究者
第二个是社区作品,这个页面是一个设计美观的 介绍性落地页(Landing Page),特点如下:
- 主要用于视觉展示,像一个营销型首页。
- 页面简洁,有动画效果,展示 Yjs 优点。
- 由社区用户或爱好者构建(非官方维护)。
- 并不包含完整的文档或深入的技术细节。
核心API
基本使用
安装相关依赖:
pnpm add quill quill-cursors yjs y-quill y-websocket
quill:Quill 是一个开源、模块化的 WYSIWYG 富文本编辑器,支持格式化文字、插入图片、列表、代码块等。quill-cursors:这个库扩展了 Quill,使其支持在协同编辑中显示多个用户的“光标位置”和“选中区域”,通常会以彩色的虚线框或用户名标签表示。yjs:Yjs 是一个高性能的 CRDT 库,可以让多个用户在没有冲突的情况下协同编辑共享数据。y-quill:这个库是 Yjs 提供的绑定器(binding),可以把 Quill 的编辑器内容和 Yjs 的共享文档(Y.Text)同步起来。从而实现实时协同富文本编辑。y-websocket:y-websocket 提供了一个 WebSocket 客户端和服务器端实现,用于在多用户之间同步 Yjs 文档数据,从而保持文档在所有客户端之间的实时一致性。
- client
- server
import Quill from 'quill'
import QuillCursors from 'quill-cursors'
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { QuillBinding } from 'y-quill'
import 'quill/dist/quill.snow.css'
Quill.register('modules/cursors', QuillCursors)
const quill = new Quill(document.querySelector('#app') as HTMLDivElement, {
modules: {
cursors: true,
toolbar: [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ list: 'ordered' }, { list: 'bullet' }],
['link', 'image', 'video']
],
history: {
userOnly: true
}
},
placeholder: 'Please input content here.',
theme: 'snow'
})
const ydoc = new Y.Doc()
const ytext = ydoc.getText('quill')
const provider = new WebsocketProvider('ws://localhost:1234', 'quill-demo-room', ydoc)
new QuillBinding(ytext, quill, provider.awareness)
{
"scripts": {
"server":"PORT=1234 npx y-websocket"
},
"dependencies": {
"y-websocket": "^1.5.1"
}
}
Y.doc
在 Yjs 中,Y.Doc 是一切的起点。
可以把它理解成一份协同文档的载体,就像是前端里的一个全局 store,每个 Yjs 的数据结构都挂载在它之上。你可以创建多个 Y.Doc,Y.Doc 可以实例化多个,每实例化一个就开了一个副本,类似于新加入了一个客户端。只要你通过网络把它们连起来,这些副本就能自动合并。
在 Yjs 中,如果要协同编辑字符串,应该用 Y.Text,这个结构在底层会自动记录每一次插入和删除,并转化为一种可合并的 CRDT 操作。
import * as Y from "yjs";
// 创建第一个副本,相当于一个客户端A
const doc1 = new Y.Doc();
// 创建第二个副本,相当于一个客户端B
const doc2 = new Y.Doc();
// 创建一个文本类型的共享数据
const text1 = doc1.getText("text");
// 会和第一个副本的文本类型共享数据进行同步
const text2 = doc2.getText("text");
// 监听第二个副本的变化
text2.observe(() => {
console.log(`doc2收到更新:${text2.toString()}`);
});
// 在第一个副本插入数据
text1.insert(0, "Hello ");
console.log(`doc1插入数据:${text1.toString()}`);
console.log(`doc2当前的数据:${text2.toString()}`);
// 该方法用于创建一个更新
const update = Y.encodeStateAsUpdate(doc1);
// 应用更新
Y.applyUpdate(doc2, update);
结构化数据
在实际应用中,面对的数据远不止文本,更多的是结构化内容:
- 一个 JSON 数据对象(如表单数据、配置项)。
- 一个动态数组(如评论列表、任务列表)。
- 一个嵌套结构(如树状文件、用户配置)。
如果说 Y.Text 是“字符串的协同容器”,那接下来的 Y.Map 和 Y.Array,就是协同世界里的对象和数组。
Y.Map
Yjs 中的 Y.Map 是动态对象协同结构,就像 JavaScript 中的对象 {},它支持动态增删键、嵌套结构、事件监听,而且具备协同特性:
- 多人同时
set键值,不会冲突。 - 可以监听某个
key的变化。 - 支持嵌套结构(嵌套
Y.Map、Y.Text、Y.Array)。
import * as Y from "yjs";
const doc = new Y.Doc();
// 共享Map类型的数据
const profile = doc.getMap("profile");
// 设置一个属性
profile.set("name", "Alice");
// 设置另一个属性
profile.set("age", 30);
// 设置第三个属性
profile.set("address", "123 Main St");
// 输出当前的profile对象
console.log("profile:", profile.toJSON());
const doc2 = new Y.Doc();
// 共享Map类型的数据
const profile2 = doc2.getMap("profile");
// 输出当前的profile对象
console.log("同步之前的profile2:", profile2.toJSON());
// 数据同步
const update = Y.encodeStateAsUpdate(doc);
Y.applyUpdate(doc2, update);
console.log("同步之后的profile2:", profile2.toJSON());
从语法上看,几乎和普通对象没差,但它是 可共享、可监听、可同步的。
Y.Array
Y.Array 是一个动态协同数组结构,可以把它理解成带有版本合并机制的 JavaScript 数组。
- 可以
push、insert、delete。 - 每一个元素都是可追踪的,修改具有可合并性。
- 元素可以是任意结构(如字符串、对象、
Y.Text、Y.Map)。
import * as Y from "yjs";
const doc = new Y.Doc();
const list = doc.getArray("list");
// []
console.log(list.toArray());
// 向列表中添加元素
list.push(["hello", "world"]);
// ["hello", "world"]
console.log(list.toArray());
可以使用 Y.Array 做评论区、任务列表、协同表格,甚至 JSON 树结构。Y.Map、Y.Array 可以嵌套使用。
import * as Y from "yjs";
const doc = new Y.Doc();
const users = doc.getMap("users");
const user1 = new Y.Map();
const tag = new Y.Array();
tag.push(["tag1", "tag2"]);
user1.set("tags", tag);
users.set("user1", user1);
users.set("name", "Lucy");
console.log(users.toJSON());
事件监听
在 Yjs 中,可以使用 observe() 和 observeDeep() 来监听协同文档中数据结构的更新。
observe() 和 observeDeep() 的区别如下:
| 方法 | 监听范围 | 典型用途 |
|---|---|---|
observe(cb) | 只监听当前结构(Map / Array) | 精准字段监听 |
observeDeep(cb) | 监听当前结构及其所有子结构 | 嵌套结构复杂时统一处理 |
import * as Y from "yjs";
const doc = new Y.Doc(); // 创建一个副本
const text = doc.getText("text"); // 创建一个文本类型的共享数据
text.observe((event) => {
console.log("文本内容发生变化");
event.changes.delta.forEach((change) => {
if (change.insert) {
console.log("插入的文本内容:", change.insert.toString());
} else if (change.delete) {
console.log("删除了", change.delete, "个字符");
} else if (change.retain) {
console.log("保留了", change.retain, "个字符");
}
});
});
// 插入文本,共享的文本内容发生了变化,因此会触发事件
text.insert(0, "Hello Yjs!");
// 删除文本,共享的文本内容发生了变化,因此会触发事件
text.delete(0, 6);
observeDeep 可以监听一整个嵌套的子结构的变化
root.observeDeep((event) => {
// ...
})
event.path 用于描述发生变化的共享数据相对于被监听的根对象的路径,会从监听的根对象一直到触发事件的具体的数据。
rootMap (Y.Map)
├── title (Y.Text)
└── content (Y.Map)
├── section1 (Y.Text)
└── metadata (Y.Map)
└── author (Y.Text)
假设修改一个深层次的数据 author,那么 event.path 则为:
path: content,metadata,author