跳到内容

年/年

存储库文件导航

Yjs公司

CRDT框架,具有强大的共享数据抽象

Yjs是一个CRDT实施暴露其内部数据结构为共享的类型。共享类型是常见的数据类型,如地图阵列拥有超级大国:变化会自动分配给其他对等点和合并时没有合并冲突。

Yjs是网络不可知论者(p2p!),支持许多现有的富文本编辑,离线编辑,版本快照,撤销/重做共享光标。它可以很好地扩展到无限数量的用户,并且很好甚至适用于大型文档。

👷‍♀️ 如果您正在寻求专业支持,请考虑通过“支持合同”支持该项目GitHub赞助商.我会处理你的问题更快,我们可以在定期视频会议上讨论问题。否则你可以在我们的社区找到帮助讨论板.

赞助

请在财务上为项目做出贡献,尤其是如果您的公司依赖在Yjs上。成为赞助商

专业支持

  • 与维护人员签订的支持合同-通过向开源Yjs项目捐款,您可以获得直接来自作者的专业支持。这包括以下机会每周视频通话,讨论您的具体挑战。
  • Synergy代码-专门从事咨询和开发可视化实时协同编辑解决方案应用程序、Synergy Codes专注于交互式图表、复杂图形、图表和各种数据可视化类型。他们的专业知识使开发人员能够构建利用Yjs的强大功能,提供吸引人的交互式视觉体验。请参见他们在可视化协作展柜.

谁在使用Yjs

目录

概述

此存储库包含可以观察到的共享类型的集合同时进行更改和操作。网络功能和双向绑定在单独的模块中实现。

装订

姓名 游标 结合 演示
散文镜                                                  y-prosemirror公司 演示
被子 y型套筒 演示
代码镜像 y代码镜像 演示
摩纳哥 y-monaco公司 演示
板岩 板岩-yjs 演示
块套件 (本地) 演示
词汇 (本地) 演示
瓦尔提奥 价值yjs 演示
伊梅尔 伊梅尔·尤斯 演示
反应/Vue/Svelte/MobX 同步存储 演示
mobx-keystone公司 mobx-keystone-yjs公司 演示

提供者

建立客户之间的沟通,管理意识信息,并且存储用于离线使用的共享数据是相当麻烦的。提供者为您管理所有这些,是您的完美起点协作应用程序。

此提供程序列表不完整。请打开PR以将您的提供商添加到这个列表!

连接提供程序

y-腹板套筒
一个模块,包含一个简单的websocket后端和一个连接到该后端。y-redis(y-redis),y型糖,ypy-网络套接字 霍库斯波库斯(见下文)是备选方案后端到y-websocket。
y-webrtc公司
使用WebRTC对等传播文档更新。同行交流通过信令服务器发送信令数据。公共可用的信令服务器可用。信令服务器上的通信可以通过以下方式加密提供共享机密,保留连接信息和共享文档私有。
@活动块/yjs
Liveblocks Yjs公司提供了一个完整的托管WebSocket基础结构和Yjs的持久化数据存储文件。无需配置或维护。它还具有Yjs webhook事件,用于读取和更新Yjs文档的REST API,以及浏览器DevTools扩展。
y型糖
具有S3或文件系统持久性的独立yjs服务器。他们提供了云服务也。
霍库斯波库斯
具有sqlite持久性、webhooks、auth等功能的独立可扩展yjs服务器。
party工具包
用于构建多人应用程序的云服务。
y-libp2p
使用libp2p通过传播更新八卦订阅.还包括一个对等同步机制,以赶上错过的更新。
y-日期
[WIP]使用多点供油的。每个客户都有CRDT本地更新的附加日志(超核心)。Multifeed管理和同步hypercores和y-dat侦听更改并将其应用于Yjs文档。
矩阵-CRDT
使用矩阵作为的离线后端Yjs通过使用矩阵提供者.使用Matrix作为Yjs更新的传输和存储,这样您可以专注于构建您的客户端应用程序和Matrix可以提供强大的功能,如身份验证、,授权、联合、托管(自托管或SaaS)甚至端到端加密(E2EE)。
yrb-可操作
Yjs客户端的ActionCable伴侣。有一个配件redis扩展也。
ypy-网络套接字
Websocket后端,用Python编写。
微型底座
本地首批应用程序的反应式数据存储。它们支持多个CRDT不同的网络技术。
y-webxdc
用于在中共享数据的提供程序webxdc聊天应用程序.

持久性提供程序

y索引b
高效地将文档更新持久化到浏览器indexeddb数据库。文档立即可用,只需要通过网络提供商。
y-mongodb-提供者
使用MongoDB向服务器添加永久存储。可与一起使用y-websocket提供程序。
@toeverything/y-indexeddb
与y-indexeddb类似,但具有子文档支持和完全TypeScript。
五火
基于Firestore的Yjs数据库和连接提供程序。
y-op-硅藻土
使用在React Native应用程序中持久化YJS更新硅藻土是React Native最快的SQLite库。
y-postgresql格式
使用PostgreSQL和很容易与y-websocket兼容。

端口

有几个与其他编程语言兼容的Yjs端口。

入门

使用您最喜欢的包管理器安装Yjs和提供程序:

npm i yjs y网络套接字

启动y-websocket服务器:

端口=1234节点/节点模块/y-websocket/bin/server.cjs

示例:观察类型

进口 * 作为   “是的”;

常数 文件 = 新的 .文件();
常数 亚雷 = 文件.获取数组('我的阵列')
亚雷.观察(事件 => {
  慰问.日志(“yarray已修改”)
})
//每次本地或远程客户端修改yarray时,都会调用观察者
亚雷.插入(0, [“val”]) //=>“yarray被修改”

示例:嵌套类型

记住,共享类型只是普通的旧数据类型。唯一的限制是共享类型在共享文档中只能存在一次。

常数 我的妈妈 = 文件.获取地图(“地图”)
常数 食物数组 = 新的 .阵列()
食物数组.插入(0, [“苹果”, “香蕉”])
我的妈妈.设置(“食物”, 食物数组)
我的妈妈.得到(“食物”) === 食物数组 //=>真
我的妈妈.设置(“水果”, 食物数组) //=>错误!已定义foodArray

现在您了解了如何在共享文档上定义类型。接下来你可以跳了演示存储库或继续阅读API文档。

示例:使用和组合提供程序

任何Yjs提供者都可以相互组合。这样您就可以同步数据通过不同的网络技术。

在大多数情况下,您希望使用网络提供商(如y-websocket或y-webrtc)与持久性提供程序(浏览器中的y-indexeddb)结合使用。持久性允许您更快地加载文档并持久化脱机时创建。

为了演示,我们将两个不同的网络提供商与持久性提供程序。

进口 * 作为   “是的”
进口 { Webrtc提供商 }  “y-webrtc”
进口 { Websocket提供者 }  'y-websocket'
进口 { 索引ddb持久性 }  'y-indexeddb'

常数 ydoc公司 = 新的 .文件()

//这允许您立即获取(缓存的)文档数据
常数 索引bProvider = 新的 索引ddb持久性('count-demo', ydoc公司)
索引bProvider.同步时.然后(() => {
  慰问.日志('已从索引数据库加载数据')
})

//将客户端与y-webrtc提供程序同步。
常数 webrtc提供商 = 新的 Webrtc提供商('count-demo', ydoc公司)

//将客户端与y-websocket提供程序同步
常数 网络套接字提供商 = 新的 Websocket提供程序(
  'wss://demos.yjs.dev版', 'count-demo', ydoc公司
)

//产生和的数字数组
常数 亚雷 = ydoc公司.获取数组(“计数”)

//观察总和的变化
亚雷.观察(事件 => {
  //数据更改时打印更新
  慰问.日志('新总和£º' + 亚雷.到阵列().减少((,b条) =>  + b条))
})

//总和加1
亚雷.([1]) //=>“新总和:1”

美国石油学会

进口 * 作为   “是的”

共享的类型

Y.阵列

支持高效插入/删除元素的可共享类数组类型在任何位置。在内部,它使用数组的链接列表,当必要的。

const-yarray=新Y.Array()
父级:Y.AbstractType | null
插入(索引:数字,内容:数组<object|boolean|Array |string|null|Uint8Array |Y.Type>)
在以下位置插入内容指数。请注意,内容是一个元素数组。即。数组.insert(0,[1])拼接列表并在处插入1位置0。
push(数组<Object|boolean|Array |string|number|null|Uint8Array |Y.Type>)
unshift(Array<Object | boolean | Array | string | number | null | Uint8Array | Y.Type>)
删除(索引:数字,长度:数字)
get(索引:数字)
slice(开始:number,结束:number):Array<Object | boolean | Array | string | number | null | Uint8Array | Y.Type>
检索一系列内容
长度:数字
forEach(函数(值:object | boolean | Array | string | number | null | Uint8Array | Y.Type,索引:数字,数组:Y.array))
map(函数(T,number,YArray):M):数组<M>
toArray():数组<object|boolean|Array | string | number | null | Uint8Array | Y.Type>
将此YArray的内容复制到新数组。
toJSON():数组<Object|boolean|Array|string|number|null>
将此YArray的内容复制到一个新的Array中。它转换所有子类型使用它们的到JSON方法。
[符号.迭代器]
返回一个YArray迭代器,其中包含数组中每个索引的值。
for(让yarray的值){..}
observe(函数(YArrayEvent,事务):void)
将事件侦听器添加到此类型,每次都会同步调用该类型此类型已修改。如果在事件侦听器中修改了此类型,当前事件侦听器返回后,将再次调用事件侦听器。
unobserve(函数(YArrayEvent,事务):void)
删除观察此类型的事件侦听器。
observeDeep(函数(数组<YEvent>,事务):void)
将事件侦听器添加到此类型,每次都会同步调用该类型此类型或其任何子级都已修改。如果修改了此类型在事件侦听器中,事件侦听器将在当前事件侦听器返回。事件侦听器接收自己创建的所有事件或其任何子公司。
unobserveDeep(函数(数组<YEvent>,事务):void)
删除观察深度此类型的事件侦听器。
Y.地图

可共享的贴图类型。

const ymap=新Y.Map()
父级:Y.AbstractType | null
大小:数字
键/值对的总数。
get(key:string):object | boolean | string | number | null | Uint8Array | Y.Type
set(键:string,值:object | boolean | string | number | null | Uint8Array | Y.Type)
删除(键:字符串)
has(key:string):boolean
get(索引:数字)
清除()
删除此YMap中的所有元素。
克隆():Y.Map
将此类型克隆到一个新的Yjs类型中。
toJSON():Object<string,Object|boolean|Array|string|number|null|Uint8Array>
复制[键,值]这对Y映射到一个新对象。使用它们的到JSON方法。
forEach(函数(值:object | boolean | Array | string | number | null | Uint8Array | Y.Type,键:字符串,映射:Y.map))
对每个键值对执行一次提供的函数。
[符号.迭代器]
返回的迭代器[键,值]对。
for(让ymap的[key,value]){..}
条目()
返回的迭代器[键,值]对。
值()
返回所有值的迭代器。
按键()
返回所有键的迭代器。
observe(函数(YMapEvent,Transaction):void)
将事件侦听器添加到此类型,每次都会同步调用该类型此类型已修改。在该类型在事件监听器中被修改的情况下,当前事件侦听器返回后,将再次调用事件侦听器。
unobserve(函数(YMapEvent,事务):void)
删除观察此类型的事件侦听器。
observeDeep(函数(数组<YEvent>,事务):void)
将事件侦听器添加到此类型,每次都会同步调用该类型此类型或其任何子级都已修改。如果修改了此类型在事件监听器中,将在当前事件监听器之后再次调用事件监听器事件侦听器返回。事件侦听器接收自己创建的所有事件或其任何子公司。
unobserveDeep(函数(数组<YEvent>,事务):void)
删除观察深度此类型的事件侦听器。
Y.文本

针对文本共享编辑进行优化的可共享类型。它允许为文本中的范围指定属性。这使得实现此类型的rich-text绑定。

此类型也可以转换为增量格式类似地YTextEvents将更改计算为增量。

const ytext=新Y.Text()
父级:Y.AbstractType | null
插入(索引:数字,内容:字符串,[formattingAttributes:Object<string,string>])
在处插入字符串指数并为其指定格式属性。
ytext.insert(0,'粗体文本',{粗体:true})
删除(索引:数字,长度:数字)
格式(索引:数字,长度:数字,格式属性:对象<string,string>)
将格式属性指定给文本中的范围
applyDelta(delta:增量,opts:对象<string,any>)
请参见套筒三角洲可以设置选项以防止删除结束newLines,默认值为true。
ytext.applyDelta(delta,{sanitize:false})
长度:数字
toString():字符串
将此类型(无格式选项)转换为字符串。
toJSON():字符串
请参见toString(字符串)
toDelta():增量
将此类型转换为套筒三角洲
observe(函数(YTextEvent,事务):void)
将事件侦听器添加到此类型,每次都会同步调用此类型已修改。如果在事件侦听器中修改了此类型,当前事件侦听器返回后,将再次调用事件侦听器。
unobserve(函数(YTextEvent,事务):void)
删除观察此类型的事件侦听器。
observeDeep(函数(数组<YEvent>,事务):void)
将事件侦听器添加到此类型,每次都会同步调用此类型或其任何子级都已修改。如果修改了此类型在事件侦听器中,事件侦听器将在当前事件侦听器返回。事件侦听器接收自己创建的所有事件或其任何子公司。
unobserveDeep(函数(数组<YEvent>,事务):void)
删除观察深度此类型的事件侦听器。
Y.Xml片段

包含Y.XmlElements数组的容器。

constyxml=新Y.XmlFragment()
父级:Y.AbstractType | null
firstChild:Y.XmlElement | Y.Xml文本|空
插入(索引:数字,内容:数组<Y.XmlElement | Y.Xml文本>)
删除(索引:数字,长度:数字)
get(索引:数字)
slice(start:number,end:number):数组<Y.XmlElement | Y.Xml文本>
检索一系列内容
长度:数字
克隆():Y.XmlFragment
将此类型克隆为新的Yjs类型。
toArray():数组<Y.XmlElement | Y.XmlText>
将子项复制到新数组。
toDOM():文档片段
将此类型和所有子项转换为新的DOM元素。
toString():字符串
获取所有子体的XML序列化。
toJSON():字符串
请参见toString(字符串).
createTreeWalker(filter:function(AbstractType<any>):boolean):可迭代
创建一个遍历子对象的Iterable。
observe(函数(YXmlEvent,事务):void)
将事件侦听器添加到此类型,每次都会同步调用该类型此类型已修改。如果在事件侦听器中修改了此类型,当前事件侦听器返回后,将再次调用事件侦听器。
unobserve(函数(YXmlEvent,事务):void)
删除观察此类型的事件侦听器。
observeDeep(函数(数组<YEvent>,事务):void)
将事件侦听器添加到此类型,每次都会同步调用该类型此类型或其任何子级都已修改。如果修改了此类型在事件侦听器中,事件侦听器将在当前事件侦听器返回。事件侦听器接收自己创建的所有事件或其任何子公司。
unobserveDeep(函数(数组<YEvent>,事务):void)
删除观察深度此类型的事件侦听器。
Y.Xml元素

表示XML元素的可共享类型。它有一个节点名称,属性和子项列表。但它没有努力验证内容,并且实际上与XML兼容。

constyxml=新Y.XmlElement()
父项:Y.AbstractType | null
firstChild:Y.XmlElement | Y.Xml文本|空
nextSibling:Y.XmlElement | Y.Xml文本|空
prevSibling:Y.XmlElement | Y.Xml文本| null
insert(索引:数字,内容:数组<Y.XmlElement|Y.XmlText>)
删除(索引:数字,长度:数字)
get(索引:数字)
长度:数字
setAttribute(属性名称:string,属性值:string)
removeAttribute(属性名称:字符串)
getAttribute(attributeName:string):string
getAttributes():对象<string,string>
get(i:number):Y.XmlElement | Y.XmlText
检索第i个元素。
slice(start:number,end:number):数组<Y.XmlElement | Y.Xml文本>
检索一系列内容
克隆():Y.XmlElement
将此类型克隆为新的Yjs类型。
toArray():数组<Y.XmlElement|Y.Xml文本>
将子项复制到一个新的数组中。
toDOM():元素
将此类型和所有子项转换为新的DOM元素。
toString():字符串
获取所有子体的XML序列化。
toJSON():字符串
请参见toString(字符串).
observe(函数(YXmlEvent,事务):void)
将事件侦听器添加到此类型,该类型将每隔修改此类型的时间。如果在事件中修改了此类型监听器,事件监听器将在当前事件之后再次调用侦听器返回。
unobserve(函数(YXmlEvent,事务):void)
删除观察此类型的事件侦听器。
observeDeep(函数(数组<YEvent>,事务):void)
将事件侦听器添加到此类型,每次都会同步调用该类型此类型或其任何子级都已修改。如果修改了此类型在事件侦听器中,事件侦听器将在当前事件侦听器返回。事件侦听器接收自己创建的所有事件或其任何子公司。
unobserveDeep(函数(数组<YEvent>,事务):void)
删除观察深度此类型的事件侦听器。

Y.文件

常数 文件 = 新的 .文件()
客户端ID
标识此客户端的唯一id。(只读)
气相色谱法
此文档实例上是否启用了垃圾收集。设置`doc.gc=false`以便禁用gc并能够恢复旧内容。请参见https://github.com/yjs/yjs#yjs-crdt算法有关Yjs中gc的更多信息。
transact(函数(Transaction):void[,origin:any])
共享文档上的每个更改都发生在事务中。观察员呼叫和这个更新在每个事务之后调用事件。你应该更改为单个事务以减少事件量电话。即。doc.transact(()=>{yarray.insert(..);ymap.set(..)})触发单个更改事件。
您可以指定一个可选的起源存储在上的参数交易.来源on('update',(update,origin)=>..).
toJSON():任何
不推荐:建议直接对共享类型调用toJSON。将整个文档转换为js对象,递归遍历每个yjs类型。不记录尚未定义的类型(使用ydoc.getType(..)).
get(字符串,Y.[TypeClass]):[Type]
定义共享类型。
getArray(字符串):Y.Array
定义共享的Y数组类型。等于y.get(字符串,y.Array).
getMap(字符串):Y.Map
定义共享的Y映射类型。相当于y.get(字符串,y.Map).
getText(字符串):Y.Text
定义共享的Y文本类型。等于y.get(字符串,y.Text).
getXmlElement(string,string):Y.XmlEletion
定义共享的Y.XmlElement类型。相当于y.get(字符串,y.XmlElement).
getXmlFragment(字符串):Y.XmlFragrament
定义共享的Y.XmlFragment类型。等于y.get(字符串,y.XmlFragment).
on(字符串,函数)
在共享类型上注册事件侦听器
关闭(字符串,函数)
从共享类型中注销事件侦听器

Y.文档事件

on('update',函数(updateMessage:Uint8Array,origin:any,Y.Doc):void)
收听文档更新。文件更新必须发送给所有其他人同龄人。您可以按任意顺序多次应用文档更新。使用`updateV2`接收V2事件。
on('beforeTransaction',函数(Y.Transaction,Y.Doc):void)
在每个事务之前发出。
on('afterTransaction',函数(Y.Transaction,Y.Doc):void)
每次交易后发出。
on('beforeAllTransactions',函数(Y.Doc):void)
事务可以嵌套(例如,当一个事务中的事件调用另一个交易)。在第一个事务之前发出。
on('afterAllTransactions',函数(Y.Doc,数组<Y.Transaction>):void)
清除最后一个事务后发出。

文档更新

共享文档上的更改被编码为文档更新.文档更新是可交换的幂等的。这意味着它们可以应用以任何顺序多次执行。

示例:侦听更新事件并将其应用于远程客户端

常数 文档1 = 新的 .文件()
常数 文档2 = 新的 .文件()

文档1.('更新', 更新 => {
  .应用更新(文档2, 更新)
})

文档2.('更新', 更新 => {
  .应用程序更新(文档1, 更新)
})

//所有更改也应用于其他文档
文档1.获取数组(“myarray”).插入(0, [“你好,doc2,你明白了吗?”])
文档2.获取数组(“myarray”).得到(0) //=>“你好,doc2,你明白了吗?”

Yjs内部保持状态向量表示下一个每个客户端的预期时钟。在另一种解释中,它持有每个客户端创建的结构数。当两个客户端同步时,您可以通过发送计算差异的状态向量。

示例:通过交换完整的文档结构同步两个客户端

常数 状态1 = .将状态编码为更新(ydoc1型)
常数 状态2 = .将状态编码为更新(ydoc2型)
.应用更新(ydoc1公司, 状态2)
.应用更新(ydoc2型, 状态1)

示例:通过计算差异同步两个客户端

此示例显示如何用最少的交换量同步两个客户端通过仅使用远程的状态向量计算差异来获得数据客户端。使用状态向量同步客户端需要另一次往返,但可以节省大量带宽。

常数 状态矢量1 = .编码状态向量(ydoc1公司)
常数 状态矢量2 = .编码状态向量(ydoc2型)
常数 差异1 = .将状态编码为更新(ydoc1公司, 状态矢量2)
常数 差异2 = .将状态编码为更新(ydoc2型, 状态矢量1)
.应用更新(ydoc1公司, 差异2)
.应用更新(ydoc2型, 差异1)

示例:同步客户端而不加载Y.Doc

可以在不加载Yjs的情况下同步客户端并计算增量更新文档到内存。Yjs公开了一个API来直接计算二进制文档更新。

//将当前状态编码为二进制缓冲区
 当前状态1 = .将状态编码为更新(ydoc1公司)
 当前状态2 = .将状态编码为更新(ydoc2型)
//现在,我们可以继续使用状态向量同步客户端,而无需使用Y.Doc
ydoc1公司.破坏()
ydoc2型.破坏()

常数 状态矢量1 = .编码状态矢量源更新(当前状态1)
常数 状态矢量2 = .编码状态矢量源更新(当前状态2)
常数 差异1 = .差异更新(当前状态1, 状态矢量2)
常数 差异2 = .差异更新(当前状态2, 状态矢量1)

//同步客户端
当前状态1 = .合并更新([当前状态1, 差异2])
当前状态2 = .合并更新([当前状态2, 差异1])

模糊更新

如果您的一个用户遇到一个奇怪的错误(例如,rich-text编辑器抛出错误消息),则无需从您的用户。相反,他们可以混淆文档(即用无意义的生成内容)。请注意,有人可能会仍然通过查看的一般结构来推断内容的类型文档。但这比要求原始文档要好得多。

模糊更新包含所需的所有CRDT相关数据合并。因此,合并模糊更新是安全的。

常数 ydoc公司 = 新的 .文件()
//执行一些更改。。
ydoc公司.获取文本().插入(0, “你好,世界”)
常数 更新 = .将状态编码为更新(ydoc公司)
//以下更新包含加扰数据
常数 模糊更新 = .模糊更新(更新)
常数 ydoc2型 = 新的 .文件()
.应用更新(ydoc2型, 模糊更新)
ydoc2型.获取文本().toString(字符串)() // => "00000000000"

使用V2更新格式

Yjs实现了两种更新格式。默认情况下,您使用的是V1更新格式。您可以选择V2更新格式,这样可以提供更好的压缩。尚未被所有提供商使用。但是,如果您正在构建自己的提供商。以下所有功能都可用于后缀“V2”。例如。Y.应用更新Y.应用更新V2。在收听更新时您需要特别需要收听V2事件,例如。yDoc.on('updateV2',…).我们还支持两种格式之间的转换功能:Y.转换更新格式V1到V2&Y.将更新格式V2转换为V1.

更新API

Y.applyUpdate(Y.Doc,更新:Uint8Array,[transactionOrigin:any])
对共享文档应用文档更新。您可以选择指定交易来源将存储在交易.来源ydoc.on('update',(update,origin)=>..).
Y.encodeStateAsUpdate(Y.Doc,[encodedTargetStateVector:Uint8Array]):Uint8数组
将文档状态编码为可应用于远程文档。(可选)指定目标状态向量以仅写入更新消息的差异。
Y.encodeStateVector(Y.Doc):Uint8Array
计算状态向量并将其编码为Uint8Array。
Y.mergeUpdates(阵列<Uint8Array>)
删除时将多个文档更新合并为单个文档更新重复信息。合并文档更新始终小于由于压缩编码而进行的单独更新。
Y.encodeStateVectorFromUpdate(Uint8Array):Uint8Array
从文档更新计算状态向量并将其编码到Uint8Array。
Y.diffUpdate(更新:Uint8Array,状态向量:Uint9Array):Uint8数组
将缺少的差异编码为另一条更新消息。此功能有效类似于Y.encodeStateAsUpdate(ydoc,stateVector)但奏效了而不是更新。
将更新格式V1转换为V2
将V1更新格式转换为V2更新格式。
将更新格式V2转换为V1
将V2更新格式转换为V1更新格式。

相对位置

在处理协作文档时,我们通常需要处理位置。位置可以表示光标位置、选择范围,甚至可以指定对一系列文本进行注释。正常索引位置(以整数表示)为使用起来不方便,因为索引范围在远程操作时立即失效更改操作文档。相对位置为您提供了强大的API明确的立场。

相对位置固定到共享文档中的元素,而不是受远程更改影响。即给定文件“a | c”,相对位置附加到c(c)。当远程用户通过以下方式修改文档时在光标之前插入一个字符,光标将保持与性格c(c).insert(1,'x')(“a|c”)=“ax|c”。当相对位置为设置为文档的末尾,它将一直附加到文档。

示例:转换为相对位置并返回

常数 relPos(相对位置) = .从类型索引创建相对位置(y文本, 2)
常数 销售时点情报系统 = .从相对位置创建绝对位置(relPos(相对位置), 文件)
销售时点情报系统.类型 === y文本 //=>真
销售时点情报系统.指数 === 2 //=>真

示例:将相对位置发送到远程客户端(json)

常数 relPos(相对位置) = .从类型索引创建相对位置(y文本, 2)
常数 编码RelPos = JSON格式.字符串(relPos(相对位置))
//将encodedRelPos发送到远程客户端。。
常数 解析的RelPos = JSON格式.解析(编码RelPos)
常数 销售时点情报系统 = .从相对位置创建绝对位置(解析的RelPos, 远程文档)
销售时点情报系统.类型 === 远程文本 //=>真
销售时点情报系统.指数 === 2 //=>真

示例:将相对位置发送到远程客户端(Uint8Array)

常数 relPos(相对位置) = .从类型索引创建相对位置(y文本, 2)
常数 编码器返回位置 = .编码相对位置(relPos(相对位置))
//将encodedRelPos发送到远程客户端。。
常数 解析的RelPos = .解码相对位置(编码RelPos)
常数 销售时点情报系统 = .从相对位置创建绝对位置(解析的RelPos, 远程文档)
销售时点情报系统.类型 === 远程文本 //=>真
销售时点情报系统.指数 === 2 //=>真
Y.createRelativePositionFromTypeIndex(类型:Uint8Array | Y.type,索引:数字[,关联=0])
创建固定到任何序列中第i个元素的相对位置共享类型(如果关联>=0). 默认情况下,该职位关联带有指定索引位置后面的字符。如果关联<0,则相对位置与角色关联在指定的索引位置之前。
Y.createAbsolutePositionFromRelativePosition(相对位置,Y.Doc):{type:Y.AbstractType,index:number,assoc:number}|null
从相对位置创建绝对位置。如果相对位置无法引用,或类型被删除,则结果为null。
Y.encodeRelativePosition(相对位置):Uint8Array
对Uint8Array的相对位置进行编码。二进制数据是首选文档更新的编码格式。如果您喜欢JSON编码,可以只需JSON.stringify/JSON.parse相对位置即可。
Y.decodeRelativePosition(Uint8Array):相对位置
将二进制编码的相对位置解码为RelativePosition对象。

Y.Undo经理

Yjs附带了一个撤销/重做管理器,用于在Yjs类型。可以选择将更改范围限定到事务源。

常数 y文本 = 文件.获取文本(“文本”)
常数 撤消管理器 = 新的 .撤消管理器(y文本)

y文本.插入(0, “abc”)
撤消管理器.解开()
y文本.toString(字符串)() // => ''
撤消管理器.重做()
y文本.toString(字符串)() //=>“abc”
构造函数(范围:Y.AbstractType |数组<Y.AbtractType>[,{captureTimeout:number,trackedOrigins:Set<any>,deleteFilter:function(item):boolean}])
接受单个类型作为范围或类型数组。
撤消()
重做()
停止捕获()
on('stack-item-added',{stackItem:{meta:Map<any,any>},type:'undo'|“重做”})
注册当堆叠项已添加到undo或redo-stack。
on('stack-item-updated',{stackItem:{meta:Map<any,any>},type:'undo'|“重做”})
注册当现有堆叠项已更新。当“captureInterval”中发生两个更改时,就会发生这种情况。
on('stack-item-popped',{stackItem:{meta:Map<any,any>},类型:“undo”|“重做”})
注册当堆叠项从弹出undo或redo-stack。
on('stack-cleared',{undoStackCleared:布尔值,redoStackclered:布尔})
注册在撤消和/或重做堆栈被清除时调用的事件。

示例:停止捕获

如果UndoManager在时间间隔内创建Undo-StackItems,则会将其合并小于选项捕获超时.致电um停止捕获()所以下一个StackItem不会被合并。

//无需停止捕获
y文本.插入(0, “a”)
y文本.插入(1, “b”)
撤消管理器.解开()
y文本.toString(字符串)() //=>“”(注意删除了“ab”)
//带stopCapturing
y文本.插入(0, “a”)
撤消管理器.停止捕获()
y文本.插入(0, “b”)
撤消管理器.解开()
y文本.toString(字符串)() //=>“a”(注意,只删除了“b”)

示例:指定跟踪的原点

共享文档上的每个更改都有其来源。如果未指定原点,默认为无效的。通过指定跟踪来源你可以有选择地指定应跟踪哪些更改撤消管理器. TheUndoManager实例始终添加到跟踪来源.

 自定义绑定 {}

常数 y文本 = 文件.获取文本(“文本”)
常数 撤消管理器 = 新的 .撤消管理器(y文本, {
  追踪来源:新的 设置([42, 自定义绑定])
})

y文本.插入(0, “abc”)
撤消管理器.解开()
y文本.toString(字符串)() //=>“abc”(不跟踪,因为原点为“null”且不是零件
                 //(共`trackedTransactionOrigins`)
电子文本.删除(0, ) //恢复更改

文件.办理(() => {
  y文本.插入(0, “abc”)
}, 42)
撤消管理器.解开()
y文本.toString(字符串)() //=>“”(由于origin是“trackedTransactionorigins`”的实例,因此被跟踪)

文件.办理(() => {
  y文本.插入(0, “abc”)
}, 41)
撤消管理器.解开()
y文本.toString(字符串)() //=>“abc”(未跟踪,因为41不是
                 //`trackedTransactionorigins`)
y文本.删除(0, ) //恢复更改

文件.办理(() => {
  y文本.插入(0, “abc”)
}, 新的 自定义绑定())
撤消管理器.解开()
y文本.toString(字符串)() //=>“”(由于原点是“CustomBinding”且
                 //`CustomBinding`位于`trackedTransactionorigins`中)

示例:向StackItems添加附加信息

撤消或重新执行以前的操作时,通常需要恢复其他元信息,如光标位置或文档。您可以将元信息指定给撤消/重做堆栈项。

常数 y文本 = 文件.获取文本(“文本”)
常数 撤消管理器 = 新的 .撤消管理器(y文本, {
  跟踪来源:新的 设置([42, 自定义绑定])
})

撤消管理器.('堆叠项已添加', 事件 => {
  //将当前光标位置保存在堆栈项上
  事件.堆栈项..设置('光标位置', 获取相对光标位置())
})

撤消管理器.('堆叠项已停止', 事件 => {
  //恢复堆栈项上的当前光标位置
  恢复光标位置(事件.堆栈项..得到('光标位置'))
})

Yjs CRDT算法

无冲突的复制数据类型用于协作编辑的(CRDT)是替代方法运营转型(OT)。一个非常简单的这两种方法的区别在于OT试图转换索引头寸以确保趋同(所有客户最终都是相同的内容),而CRDT使用通常不涉及索引的数学模型转换,如链接列表。OT目前是共享文本编辑。支持共享编辑的OT方法中央真相来源(中央服务器)需要太多的簿记在实践中可行。CRDT更适合分布式系统,提供额外保证文档可以与远程客户端同步,以及不需要一个核心的真相来源。

Yjs实现了中描述的算法的修改版本纸张.这个文章解释了CRDT模型的简单优化,以及提供了有关Yjs性能特征的更多信息。有关具体实施的更多信息,请参阅内部.md和中Yjs代码库的本演练.

适用于共享文本编辑的CRDT受到以下事实的影响:只会变大。有些CRDT的大小不会增长,但它们不会具有有益于共享文本编辑的特性(例如意图保留)。Yjs实现了对原始版本的许多改进该算法减少了文档只会增长的代价。我们无法垃圾收集已删除的结构(逻辑删除),同时确保唯一结构的顺序。但我们可以。将前面的结构合并为单个结构以减少元信息量。我们可以从中删除内容结构(如果已删除)和3。如果我们不再关心结构的顺序(例如,如果父级是删除)。

示例:

  1. 如果用户按顺序插入元素,则结构将合并为单个结构。例如。text.insert(0,'a'),text.inset(1,'b');首先表示为两个结构([{id:{客户端,时钟:0},内容:“a”},{id:}客户端,时间:1},content:“b”})然后合并成一个结构:[{id:{客户端,时钟:0},内容:'ab'}].
  2. 当包含内容的结构(例如。项目字符串)删除,则结构将替换为项目已删除不包含内容的不再。
  3. 删除类型时,所有子元素都将转换为GC公司结构。GC公司struct只表示存在一个struct,并且该struct已被删除。GC公司结构始终可以与其他结构合并GC公司如果id是相邻。

尤其是在处理结构化内容时(例如共享编辑ProseMirror),当标杆管理随机文档编辑。实际上,它们显示出更好的结果,因为用户通常在序列,从而生成易于合并的结构。基准显示即使在最坏的情况下,用户从右向左编辑文本,即使对于大型文档,Yjs也能获得良好的性能。

状态向量

Yjs能够在同步两个客户端时仅交换差异。我们使用lamport时间戳来标识结构并跟踪客户创建了它们。每个结构都有一个struct.id={client:number,clock:number}唯一标识结构的。我们定义下一个预期时钟由每个客户作为状态向量。此数据结构类似于版本向量数据结构。但我们只使用状态向量来描述本地文档的状态,所以我们可以计算远程客户端缺少的结构。我们不使用它来跟踪因果关系。

许可证和作者

Yjs和所有相关项目麻省理工学院授权.

Yjs是基于我作为一名学生在RWTH(跑道)i5号机组现在我在业余时间学习Yjs。

通过在GitHub赞助商或雇佣作为您合作的承包商应用程序。