基础概念
基础概念
感知信息(Awareness)
感知信息(用户状态)指的是多人在线协作编辑下其他编辑者的在线状态和信息,例如他人的光标信息,用户标识颜色。这些统称为感知信息, yjs专门提供了一个对象用来存储这些信息。
question: 为什么用单独的字段存储这些信息?
官方文档的解释是这些信息并不属于文档信息的一部分,更像是一种辅助多人编辑的时效性信息,未来也不会持久化到数据,所以作为一个单独的对象信息是合理的, 在线协作编辑下,这些信息往往在用户离线或者离开编辑页面后就失去意义了。
以下是官方提供的一段实例代码:
// 所有的连接都提供了感知信息
const awareness = provider.awareness
// 你可以添加事件监听函数来监听用户信息的变化
awareness.on('change', changes => {
//当用户信息变化时打印出所有的用户状态信息
console.log(Array.from(awareness.getStates().values()))
})
//通过修改user字段可以自定义自己的状态信息
awareness.setLocalStateField('user', {
// 定义用户名
name: 'Align',
// 定义自己在其他人页面上的展示颜色
color: '#ffb61e' // 注意为十六进制的颜色
})
目前感知信息(Awareness)字段还没有规范化,可以自定义一些字段,不过需要注意现有的支持yjs协作的编辑器通常会使用cursor和user字段来记录光标位置和用户信息,这些编辑器都接收Awareness实例来获取光标信息,例如quill:
const binding = new QuillBinding(ytext, quill, provider.awareness)
离线支持(Offline Support)
yjs提供了专门的库用来支持离线下操作。y-indexeddb用于将文档数据同步到浏览器的indexDB数据库中,官网使用示例如下:
import { IndexeddbPersistence } from 'y-indexeddb'
const ydoc = new Y.Doc()
const roomName = 'my-room-name'
const persistence = new IndexeddbPersistence(roomName, ydoc)
// The persistence provider works similarly to the network providers:
// const network = new WebrtcProvider(roomName)
通过监听实例的async
事件可以在本地数据加载时执行相关代码:
persistence.once('synced', () => { console.log('initial content loaded') })
另外官方提到使用y-indexeddb的优点是当服务器或其他分布式方有数据丢失时yjs也能依靠本地数据库中的存储在下次更新中自动修复。其实就是相当于本地缓存 的意思,看样子官方还是推荐使用y-indexeddb作为本地缓存的。
使用y-indexeddb构建离线应用程序
通过将应用程序的数据托管到y-indexeddb可以轻易的构建一个离线的网页应用程序或网站。
不过y-indexeddb只充当数据库的作用,模拟服务器端的controller和server层需要使用浏览器提供的Server Worker API来实现。 不过我没有使用过这个API,这里只附上其在MDN中的文档地址
共享类型(Shared Types)
文档是一堆序列化的数据结构,你可以轻易的设想出一篇文档的数据对象可能具有的结构和属性,例如一篇简单的word文档,最顶层document Object下可能具有页眉(header field),内容(content field),页脚(footer field)等等。现在我们有了一个文档对象可以对这个文档对象进行编辑(增删改查),并通过服务器分发同步到其他客户端实现同步,但是当冲突和并发发生的时候文档数据可不会自己处理这些情况,如何保证在复杂和高频的增删修改下文档数据的最终一致性,这正是yjs做的事情。
但并不是把文件对象光秃秃的扔给yjs实例他就能处理了,yjs的分布式冲突处理能力是基于自身提供的共享数据类型来实现的,我们需要将普通的文档对象转换为yjs提供的共享数据对象,这样yjs才能正确处理并解决数据冲突。
以下是官网提供的文档示例:
import * as Y from 'yjs'
//新建一个文档对象
const ydoc = new Y.Doc()
// 在该文档下创建一个数组
const yarray = ydoc.getArray('my array')
// 对这个数组添加监听
yarray.observe(event => {
// 数据变化时打印出数据
console.log('delta:', event.changes.delta)
})
// 共享类型的使用也不是完全没有限制的,有一些规则需要遵守,我们通过下面的一些实例来展示:
// 向数组添加或删除元素
yarray.insert(0, ['some content']) // => delta: [{ insert: ['some content'] }]
// 注意上述方法中插入的内容应该被数组包裹
yarray.toArray() // => ['some content']
// 我们可以插入任何json格式的数据,当然Uint8Arrays也是可以的.
yarray.insert(0, [1, { bool: true }, new Uint8Array([1,2,3])]) // => delta: [{ insert: [1, { bool: true }, Uint8Array([1,2,3])] }]
yarray.toArray() // => [1, { bool: true }, Uint8Array([1,2,3]), 'some content']
// 你甚至可以插入共享类型以实现嵌套
const subArray = new Y.Array()
yarray.insert(0, [subArray]) // => delta: [{ insert: [subArray] }]
// 请注意,当你向被插入的subArray共享对象中插入数据时不会触发yarray的监听(没有类似冒泡的机制)
subArray.insert(0, ['nope']) // [observer not called]
// 要实现监听需要在subArray中绑定监听事件即可
subArray.observe(event => { })
// 或者直接在yarray上绑定深层监听
yarray.observeDeep(events => { console.log('All deep events: ', events) })
subArray.insert(0, ['this works']) // => All deep events: [..]
// 注意不要把一个共享类型插入到不同的地方,共享类型应该只存在于一个地方
yarray.insert(0, [subArray]) // Throws exception!
除了yarray,yjs还提供了其他共享数据类型
注意事项
- 最重要的,也是刚刚提到的,不要移动已经插入的共享数据类型。
- 不要直接修改插入到共享对象中的对象,yjs考虑到性能方面不会克隆插入的数据,直接修改数据会产生无法同步的数据变化。
事务(Transactions)
在yjs中,所有的数据修改都应该通过发起事务(Transactions)来实现。当你调用方法修改数组对象时(e.g. yarray.insert(..)),实际上yjs内部隐式的创建了事务来执行修改。 以下示例展示了如何显示的创建事务并修改数据:
const ydoc = new Y.Doc()
const ymap = ydoc.getMap('favorites')
// 设置初始值
ymap.set('food', 'pizza')
// 每次事务完成后都会触发监听
ymap.observe(event => {
console.log('changes', event.changes.keys)
})
ydoc.transact(() => {
ymap.set('food', 'pencake')
ymap.set('number', 31)
}) // => changes: Map({ number: { action: 'added' }, food: { action: 'updated', oldValue: 'pizza' } })
每次事务的修改都会触发事件的监听,所以最好尽可能的合并修改到一个事务中以减少不需要的监听触发。
yjs事件的触发遵循以下顺序:
ydoc.on('beforeTransaction', event => { .. })
事务执行前调用- 事务已触发
ydoc.on('beforeObserverCalls', event => {})
事务执行后但是还没开始触发监听ytype.observe(event => { .. })
- 监听器被调用ytype.observeDeep(event => { .. })
- 深层监听器被调用ydoc.on('afterTransaction', event => {})
- - 事务执行完成后ydoc.on('update', update => { .. })
- - 其他客户端修改触发的更新事件
可以看到其他客户端的修改会触发update事件,所以尽量合并事件减少事务次数是必要的。