1. 基本使用#
在 Yjs 中,Y.Doc 是一切的起点。
可以把它理解成一份“协同文档”的载体,就像是前端里的一个全局 store,每个 Yjs 的数据结构都挂载在它之上。你可以创建多个 Y.Doc,Y.Doc 可以实例化多个,每实例化一个就开了一个副本,类似于新加入了一个客户端。只要你通过网络把它们“连起来”,这些副本就能自动合并。
在 Yjs 中,我们如果要协同编辑字符串,应该用 Y.Text,这个结构在底层会自动记录每一次插入和删除,并转化为一种可合并的 CRDT 操作。
const doc = new Y.Doc() // 创建一个 Yjs 的副本,相当于一个新的客户端const text = doc.getText('myText') // 创建一个共享文本// 参数是对共享文本的一个标识(key),如果是第一次使用,会创建一个全新的共享文本// 后续调用的时候,会取出对应 key 的共享文本// 因为支持传入 key,因此在同一个文档中可以标记不同的共享数据片段// doc.getText("title"); // 共享文档标题// doc.getText("body"); // 共享正文text.insert(0, 'Hello Yjs') // 插入文本,第一个参数是插入文本的位置,第二个参数是要插入的文本内容console.log(text.toString()) // 获取当前的共享文本的内容下面是一个基本的示例:
import * as Y from 'yjs'
const doc1 = new Y.Doc() // 创建第一个副本,相当于一个客户端Aconst doc2 = new Y.Doc() // 创建第二个副本,相当于一个客户端B
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()}`) // 输出:Helloconsole.log(`doc2当前的数据:${text2.toString()}`) // 输出:Hello// 思考:如果共享给 doc2
// 该方法用于创建一个更新const update = Y.encodeStateAsUpdate(doc1)Y.applyUpdate(doc2, update) // 应用更新课堂练习
- 在文本中插入一段文字,编码后应用到另一个文档上。
- 修改为删除操作,观察同步结果。
import * as Y from 'yjs'
const doc1 = new Y.Doc() // 创建第一个副本,相当于一个客户端Aconst doc2 = new Y.Doc() // 创建第二个副本,相当于一个客户端B
const text1 = doc1.getText('text') // 创建一个文本类型的共享数据const text2 = doc2.getText('text') // 这里就会和第一个副本的文本类型共享数据进行同步
// 监听两个副本的变化text1.observe(() => { console.log(`doc1收到更新:${text1.toString()}`)})text2.observe(() => { console.log(`doc2收到更新:${text2.toString()}`)})
text1.insert(0, 'Hello ') // 在第一个副本插入数据console.log(`doc1插入数据:${text1.toString()}`) // 输出:Helloconsole.log(`doc2当前的数据:${text2.toString()}`) // 输出:Hello// 思考:如果共享给 doc2
// 该方法用于创建一个更新const update = Y.encodeStateAsUpdate(doc1)Y.applyUpdate(doc2, update) // 应用更新
// 接下来进行删除操作text2.delete(0, 2) // 第一个参数代表删除的起始位置,第二个参数代表删除的长度console.log(`doc1当前的数据:${text1.toString()}`) // Helloconsole.log(`doc2当前的数据:${text2.toString()}`) // 输出:llo
// 接下来我们需要将这个变化同步给副本1const update2 = Y.encodeStateAsUpdate(doc2)Y.applyUpdate(doc1, update2) // 应用更新2. 结构化数据#
在实际应用中,我们面对的数据远不止文本,更多的是结构化内容:
- 一个 JSON 数据对象(如表单数据、配置项)
- 一个动态数组(如评论列表、任务列表)
- 一个嵌套结构(如树状文件、用户配置)
如果说 Y.Text 是“字符串的协同容器”,那接下来的 Y.Map 和 Y.Array,就是协同世界里的“对象”和“数组”。
1. Y.Map
Yjs 中的 Y.Map 是动态对象协同结构,就像 JavaScript 中的对象 {},它支持动态增删键、嵌套结构、事件监听,而且具备协同特性:
- 多人同时 set 键值,不会冲突
- 可以监听某个 key 的变化
- 支持嵌套结构(嵌套 Y.Map、Y.Text、Y.Array)
我们来看一个最基本的例子:
import * as Y from 'yjs'
const doc = new Y.Doc() // 创建第一个副本,相当于一个客户端Aconst profile = doc.getMap('profile') // 这里的profile就是一个共享Map类型的数据
profile.set('name', 'Alice') // 设置一个属性profile.set('age', 30) // 设置另一个属性profile.set('address', '123 Main St') // 设置第三个属性
console.log('profile:', profile.toJSON()) // 输出当前的profile对象
const doc2 = new Y.Doc() // 创建第二个副本,相当于一个客户端Bconst profile2 = doc2.getMap('profile') // 这里的profile就是一个共享Map类型的数据console.log('同步之前的profile2:', profile2.toJSON()) // 输出当前的profile对象
// 接下来我们要把第一个副本的profile数据同步到第二个副本const update = Y.encodeStateAsUpdate(doc)Y.applyUpdate(doc2, update) // 将第一个副本的更新应用到第二个副本console.log('同步之后的profile2:', profile2.toJSON()) // 输出当前的profile对象从语法上看,几乎和普通对象没差,但它是 可共享、可监听、可同步 的。
2. Y.Array
Y.Array 是一个动态协同数组结构,我们可以把它理解成带有版本合并机制的 JavaScript 数组。
- 可以
push、insert、delete - 每一个元素都是可追踪的,修改具有可合并性
- 元素可以是任意结构(如字符串、对象、Y.Text、Y.Map)
import * as Y from 'yjs'
const doc = new Y.Doc() // 创建第一个副本,相当于一个客户端Aconst list = doc.getArray('list')
console.log(list.toArray()) // []list.push(['hello', 'world']) // 向列表中添加元素console.log(list.toArray()) // ["hello", "world"]总结一下:不同的数据类型,在获取内容的时候方式不一样
- getText:通过 toString 方法获取共享文本
- getMap:通过 toJSON 方法获取共享对象
- getArray:通过 toArray 方法获取共享数组
你可以用它做评论区、任务列表、协同表格,甚至 JSON 树结构。Y.Map、Y.Array 可以嵌套使用,例如:
import * as Y from 'yjs'
const doc = new Y.Doc() // 创建第一个副本,相当于一个客户端Aconst users = doc.getMap('users') // 创建了一个Map类型的Yjs对象
const user1 = new Y.Map()const tag = new Y.Array()tag.push(['tag1', 'tag2'])user1.set('tags', tag)
// 接下来再将这个user1添加到users中users.set('user1', user1) // 将user1添加到users中
users.set('name', 'Lucy')
console.log(users.toJSON()) // { name: 'Lucy', user1: { tags: [ 'tag1', 'tag2' ] } }这就像是把一个 JSON 对象“协同化”了,任何一个内部字段的修改都会被正确同步和合并。
-EOF-