JavaScript中的协同编辑:操作转换简介

I've set out to build a robust collaborative code editor for the web. It's called Codr, and it lets developers work together in real time - like Google Docs for code. For web developers, Codr doubles as a shared reactive work surface where every change is instantly rendered for all viewers. Check out Codr's newly launched Kickstarter campaign to learn more.

我已经着手为网络构建一个强大的协作代码编辑器。 它称为Codr,它使开发人员可以实时协同工作-例如用于代码的Google Docs。 对于Web开发人员而言,Codr可以兼作共享的被动工作台,在此工作台,所有更改都会立即呈现给所有查看者。 查看Codr新推出的Kickstarter广告系列以了解更多信息。

A collaborative editor allows multiple people to edit the same document simultaneously and to see each other's edits and selection changes as they occur. Concurrent text editing allows for engaging and efficient collaboration that would otherwise be impossible. Building Codr has enabled me to better understand and (I hope) convey how to build a fast and reliable collaborative application.

协作编辑器允许多个人同时编辑同一文档,并在彼此发生更改时查看彼此的编辑和选择更改。 并发文本编辑允许进行引人入胜且高效的协作,而这在其他情况下是不可能的。 构建Codr使我能够更好地理解并(希望)传达如何构建快速而可靠的协作应用程序。

挑战 (The Challenge)

If you've built a collaborative editor or have talked with someone who has, then you know that gracefully handling concurrent edits in a multi-user environment is challenging. It turns out, however, that a few relatively simple concepts greatly simplify this problem. Below I'll share what I've learned in this regard through building Codr.

如果您已经建立了协作式编辑器,或者与拥有该功能的人进行了交谈,那么您就会知道,在多用户环境中优雅地处理并发编辑是一项挑战。 但是,事实证明,一些相对简单的概念可以大大简化此问题。 下面,我将分享我通过构建Codr在这方面学到的知识。

The primary challenge associated with collaborative editing is concurrency control. Codr uses a concurrency control mechanism based on Operational Transformation (OT). If you'd like to read up about the history and theory of OT, then check out the wikipedia page. I'll introduce some of the theory below, but this post is intended as an implementor's guide and is hands-on rather than abstract.

与协作编辑相关的主要挑战是并发控制 。 Codr使用基于操作转换(OT)的并发控制机制。 如果您想了解OT的历史和理论,请查看Wikipedia页面 。 我将在下面介绍一些理论,但是本文旨在作为实现者的指南,并且是动手而不是抽象的。

Codr is built in JavaScript and code examples are in JavaScript. Significant logic needs to be shared between the server and client to support collaborative editing, so a node/iojs backend is an excellent choice. In the interest of readability, code examples are in ES6.

Codr是用JavaScript内置的,而代码示例是用JavaScript编写的。 服务器和客户端之间需要共享重要的逻辑以支持协作编辑,因此,节点/ iojs后端是一个不错的选择。 为了提高可读性,ES6中提供了代码示例。

天真的协作编辑方法 (A Naive Approach to Collaborative Editing)

In a zero-latency environment, you might write a collaborative editor like this:

在零延迟环境中,您可以编写如下的协作编辑器:

Client

客户

editor.on('edit', (operation) => 
    socket.send('edit', operation));
socket.on('edit', (operation) => 
    editor.applyEdit(operation));


Server

服务器

socket.on('edit', (operation) => {
    document.applyEdit(operation);
    getOtherSockets(socket).forEach((otherSocket) => 
        otherSocket.emit('edit', operation)
    );
});


Every action is conceptualized as either an insert or delete operation. Each operation is:

每个动作在概念上都是插入删除操作。 每个操作是:

  1. Locally applied in the editing component

    本地应用在编辑组件中
  2. Sent to the server

    发送到服务器
  3. Applied to a server-side copy of the document

    应用于文档的服务器端副本
  4. Broadcast to other remote editors

    广播给其他远程编辑
  5. Locally applied to each remote editor's copy of the document

    本地应用于每个远程编辑者的文档副​​本

延迟破坏了事情 (Latency Breaks Things)

When you introduce latency between the client and server, however, you run into problems. As you've likely foreseen, latency in a collaborative editor introduces the possibility of version conflicts. For example:

但是,当您在客户端和服务器之间引入延迟时,就会遇到问题。 您可能已经预见到,协作编辑器中的延迟会引入版本冲突的可能性。 例如:

Starting document state:

起始文件状态:

bcd


User 1 inserts a at document start. The operation looks like this:

用户1在文档开始处插入a 。 该操作如下所示:

{
    type: 'insert',
    lines: ['a'],
    range: {
        start: { row: 0, column: 0}
        end: {row: 0, column: 1}
    }
}


At the same time, User 2 types e at document end:

同时, 用户2在文档末尾键入e

{
    type: 'insert',
    lines: ['e'],
    range: {
        start: { row: 0, column: 3}
        end: {row: 0, column: 4}
    }
}


What should happen is that User 1 and User 2 end up with:

应该发生的情况是, 用户1用户2最终会:

abcde


In reality, User 1 sees:

实际上, 用户1看到:

bcd    <-- Starting Document State
abcd   <-- Apply local "insert 'a'" operation at offset 0
abced  <-- Apply remote "insert 'e'" operation at offset 3


And User 2 sees:

用户2看到:

bcd    <-- Starting Document State
bcde   <-- Apply local "insert 'e'" operation at offset 3
abcde  <-- Apply remote "insert 'a'" operation at offset 0


Oops! 'abced' != 'abcde' - the shared document is now in an inconsistent state.

糟糕! 'abced' != 'abcde'共享文档现在处于不一致状态。

简易修复太慢 (The Easy Fix is Too Slow)

The above conflict occurs because each user is "optimistically" applying edits locally without first ensuring that no one else is making edits. Since User 1 changed the document out from under User 2, a conflict occurred. User 2's edit operation presupposes a document state that no longer exists by the time it's applied to User 1's document.

发生上述冲突的原因是,每个用户都在本地“乐观地”应用编辑,而没有先确保没有其他人在进行编辑。 由于用户1用户2下更改了文档,因此发生了冲突。 用户2的编辑操作预设了一个文档状态,该状态在应用于用户1的文档时不再存在。

A simple fix is to switch to a pessimistic concurrency control model where each client requests an exclusive write lock from the server before applying updates locally. This avoids conflicts altogether. Unfortunately, the lag resulting from such an approach over an average internet connection would render the editor unusable.

一个简单的解决方法是切换到悲观的并发控制模型,在该模型中,每个客户端在本地应用更新之前都向服务器请求独占写入锁定。 这完全避免了冲突。 不幸的是,这种方法导致的平均互联网连接上的滞后将使编辑器无法使用。

营救业务转型 (Operational Transformation to the Rescue)

Operational Transformation (OT) is a technique to support concurrent editing without compromising performance. Using OT, each client optimistically updates its own document locally and the OT implementation figures out how to automatically resolve conflicts.

操作转换(OT)是一种在不影响性能的情况下支持并发编辑的技术。 使用OT,每个客户都可以在本地乐观地更新其自己的文档,并且OT实施可以弄清楚如何自动解决冲突。

OT dictates that when we apply a remote operation we first "transform" the operation to compensate for conflicting edits from other users. The goals are two-fold:

OT指出,当我们应用远程操作时,我们首先要“转换”该操作以补偿来自其他用户的冲突编辑。 目标有两个:

  1. Ensure that all clients end up with consistent document states

    确保所有客户最终拥有一致的文档状态
  2. Ensure that the intent of each edit operation is preserved

    确保保留每个编辑操作的意图

In my original example, we'd want to transform User 2's insert operation to insert at character offset 4 instead of offset 3 when we apply it to User 1's document. This way, we respect User 2's intention to insert e after d and ensure that both users end up with the same document state.

在我的原始示例中,当我们将用户2的插入操作应用于用户1的文档时,我们希望将其转换为在字符偏移量4而不是偏移量3处插入。 这样,我们尊重用户2d之后插入e意图 ,并确保两个用户最终都处于相同的文档状态。

Using OT, User 1 will see:

使用OT, 用户1将看到:

bcd    <-- Starting Document State
abcd   <-- Apply local "insert 'a'" operation at offset 0
abcde  <-- Apply TRANSFORMED "insert 'e'" operation at offset 4


And User 2 will see:

用户2将看到:

bcd    <-- Starting Document State
bcde   <-- Apply local "insert 'e'" operation at offset 3
abcde  <-- Apply remote "insert 'a'" operation at offset 0


行动的生命周期 (The Life-Cycle of An Operation)

A helpful way to visualize how edits are synchronized using OT is to think of a collaborative document as a git repository:

可视化使用OT如何同步编辑的一种有用方法是将协作文档视为git存储库:

  1. Edit operations are commits

    编辑操作是提交
  2. The server is the master branch

    服务器是主分支
  3. Each client is a topic branch off of master

    每个客户都是母版的一个分支

Merging Edits Into Master (Server-Side) When you make an edit in Codr, the following occurs:

将编辑合并到主服务器(服务器端)中在Codr中进行编辑时,将发生以下情况:

  1. The Codr client branches from master and locally applies your edit

    Codr客户端从master分支,并在本地应用您的编辑

  2. The Codr client makes a merge request to the server

    Codr客户端向服务器发出合并请求

Here's git's lovely (slightly adapted) diagram. Letters reference commits (operations):

这是git的可爱(略有改编)图。 信函引用提交(操作):

Before Merge:

合并之前:

      A topic (client)
     /
    D---E---F master (server)


After Merge:

合并后:

      A ------ topic
     /         \
    D---E---F---G master


To do the merge, the server updates (transforms) operation A so that it still make sense in light of preceding operations E and F, then applies the transformed operation (G) to master. The transformed operation is directly analogous to a git merge commit.

为了进行合并,服务器会更新(转换)操作A以便根据先前的操作EF仍然有意义,然后将转换后的操作( G )应用于主服务器。 转换后的操作直接类似于git merge commit。

Rebasing Onto Master (Client-Side) After an operation is transformed and applied server-side, it is broadcasted to the other clients. When a client receives the change, it does the equivalent of a git rebase:

在主服务器上重新建立基础(客户端)转换操作并将其应用到服务器端后,会将其广播到其他客户端。 当客户端收到更改时,它等效于git rebase

  1. Reverts all "pending" (non-merged) local operations

    还原所有“待处理”(非合并)本地操作
  2. Applies remote operation

    应用远程操作
  3. Re-applies pending operations, transforming each operation against the new operation from the server

    重新应用挂起的操作,将每个操作转换为服务器上的新操作

By rebasing the client rather than merging the remote operation as is done server-side, Codr ensure that edits are applied in the same order across all clients.

通过重新定级客户端而不是像服务器端那样合并远程操作,Codr确保在所有客户端上以相同的顺序应用编辑。

建立规范的编辑操作顺序 (Establishing a Canonical Order of Edit Operations)

The order in which edit operations are applied is important. Imagine that two users type the characters a and b simultaneously at the same document offset. The order in which the operations occur will determine if ab or ba is shown. Since latency is variable, we can't know with certainty what order the events actually occurred in, but it's nonetheless important that all clients agree on the same ordering of events. Codr treats the order in which events arrive at the server as the canonical order.

应用编辑操作的顺序很重要。 想象两个用户在相同的文档偏移量处同时键入字符ab 。 操作发生的顺序将确定显示ab还是ba 。 由于延迟是可变的,因此我们无法确定事件实际发生的顺序,但是所有客户端都必须同意相同的事件顺序,这一点很重要。 Codr将事件到达服务器的顺序视为规范顺序。

The server stores a version number for the document which is incremented whenever an operation is applied. When the server receives an operation, it tags the operation with the current version number before broadcasting it to the other clients. The server also sends a message to the client initiating the operation indicating the new version. This way every client knows what its "server version" is.

服务器存储文档的版本号,只要应用操作,该版本号就会递增。 当服务器接收到一个操作时,它将使用当前版本号标记该操作,然后再将其广播给其他客户端。 服务器还向客户端发送一条消息,以启动指示新版本的操作。 这样,每个客户端都知道其“服务器版本”是什么。

Whenever a client sends an operation to the server, it also sends the client's current server version. This tells the server where the client "branched", so the server knows what previous operations the new change needs to be transformed against.

每当客户端向服务器发送操作时,它也会发送客户端的当前服务器版本。 这告诉服务器客户端在哪里“分支”,因此服务器知道需要对新更改进行哪些先前的操作。

转变运营 (Transforming an Operation)

The core of Codr's OT logic is this function:

Codr OT逻辑的核心是以下功能:

function transformOperation(operation1, operation2) {
    // Modify operation2 such that its intent is preserved
    // subsequent to intervening change operation1
}


I won't go into the full logic here, as it gets involved, but here are some examples:

我将不涉及全部逻辑,因为它涉及其中,但以下是一些示例:

  1. If op1 inserted line(s) before op2's line, increase op2's line offset accordingly.

    如果op1 op2的行之前插入了行,则相应地增加op2的行偏移量。

  2. If op1 inserted text before op2 on the same line, increase op2's character offset accordingly.

    如果op1在同一行上的op2 之前插入了文本,则相应地增加op2的字符偏移量。

  3. If op1 occurred entirely after op2, then don't do anything.

    如果op1完全 op2之后发生,则什么也不做。

  4. If op1 inserts text into a range that op2 deletes, then grow op2's deletion range to include the inserted text and add the inserted text. Note: Another approach would be to split op2 into two deletion actions, one on either side of op1's insertion, thus preserving the inserted text.

    如果op1将文本插入op2删除的范围内,则增大op2的删除范围以包括插入的文本并添加插入的文本。 注意 :另一种方法是将op2拆分为两个删除操作,一个在op1插入的任一侧,从而保留插入的文本。

  5. If op1 and op2 are both range deletion operations and the ranges overlap, then shrink op2's deletion range to only include text NOT deleted by op1.

    如果op1op2都是范围删除操作,并且范围重叠,则将op2的删除范围缩小到仅包括op1删除的文本。

同步光标位置和选择 (Syncing Cursor Position and Selection)

A user selection is simply a text range. If the start and end points of the range are equal, then the range is a collapsed cursor. When the user selection changes, the client sends the new selection to the server and the server broadcasts the selection to the other clients. As with editing operations, Codr transforms the selection against conflicting operations from other users. The transform logic for a selection is simply a subset of the logic needed to transform an insert or delete operation.

用户选择只是一个文本范围。 如果范围的startend相等,则范围是折叠的光标。 当用户选择更改时,客户端将新选择发送到服务器,服务器将选择广播到其他客户端。 与编辑操作一样,Codr将选择内容转换为其他用户的冲突操作。 选择的变换逻辑只是变换insertdelete操作所需的逻辑子集。

撤销重做 (Undo/Redo)

Codr gives each user their own undo stack. This is important for a good editing experience: otherwise hitting CMD+Z could undo someone else's edit in a different part of the document.

Codr为每个用户提供自己的撤消堆栈。 这对于获得良好的编辑体验很重要:否则,按CMD+Z可能会撤消文档其他部分中其他人的编辑。

Giving each user their own undo stack also requires OT. In fact, this is one case where OT would be necessary even in a zero-latency environment. Imagine the following scenario:

为每个用户提供自己的撤消堆栈也需要OT。 实际上,这是一种情况,即使在零延迟环境中也需要OT。 想象以下情况:

abc     <-- User 1 types "abc"
abcde   <-- User 2 types "de"
ce      <-- User 1 deletes "bcd"
??      <-- User 2 hits CMD+Z


User2's last action was:

User2的最后一个操作是:

{
    type: 'insert',
    lines: ['de'],
    range: {
        start: { row: 0, column: 3}
        end: {row: 0, column: 5}
    }
}


The inverse (undo) action would be:

反向(撤消)操作为:

{
    type: 'delete',
    lines: ['de'],
    range: {
        start: { row: 0, column: 3}
        end: {row: 0, column: 5}
    }
}


But we obviously can't just apply the inverse action. Thanks to User 1's intervening change, there is no longer a character offset 3 in the document!

但是我们显然不能仅仅应用反作用。 由于用户1的干预更改,文档中不再有字符偏移3

Once again, we can use OT:

再一次,我们可以使用OT:

var undoOperation = getInverseOperation(myLastOperation);
getOperationsAfterMyLastOperation().forEach((operation) => 
    transformOperation(operation, undoOperation);
);
editor.applyEdit(undoOperation);
socket.emit('edit', undoOperation);


By transforming the undo operation against subsequent operations from other clients, Codr will instead apply the following operation on undo, achieving the desired behavior.

通过将撤消操作转换为其他客户端的后续操作,Codr将改为对撤消应用以下操作,以实现所需的行为。

{
    type: 'delete',
    lines: ['e'],
    range: {
        start: { row: 0, column: 1}
        end: {row: 0, column: 2}
    }
}


Implementing undo/redo correctly is one of the more challenging aspects of building a collaborative editor. The full solution is somewhat more involved than what I've described above because you need to undo contiguous inserts and deletes as a unit. Since operations that were contiguous may become non-contiguous due to edits made by other collaborators, this is non-trivial. What's cool though, is that we can reuse the same OT used for syncing edits to achieve per-user undo histories.

正确实现撤消/重做是构建协作编辑器更具挑战性的方面之一。 完整的解决方案比上面描述的要复杂得多,因为您需要撤消连续的插入和删除作为一个单元。 因为这邻接可能变得不连续的操作,由于由其他协作者所做的编辑,这是不平凡的。 不过,很酷的是,我们可以重用用于同步编辑的相同OT,以实现每个用户的撤消历史记录。

结论 (Conclusion)

OT is a powerful tool that allows us to build high-performance collaborative apps with support for non-blocking concurrent editing. I hope that this summary of Codr's collaborative implementation provides a helpful starting point for understanding OT. A huge thanks to David for his invitation to let me share this piece on his blog.

OT是一款功能强大的工具,可让我们构建高性能协作应用程序,并支持非阻塞并发编辑。 我希望这份Codr合作实施的摘要为理解OT提供一个有用的起点。 非常感谢David的邀请,让我在他的博客上分享此文章。

Want to learn more about Codr? Check out the KickStarter campaign or tweet to @CodrEditor to request an invite.

想更多地了解Codr? 检出KickStarter广告活动或发推文到@CodrEditor以请求邀请。

翻译自: https://davidwalsh.name/collaborative-editing-javascript-intro-operational-transformation

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值