Cumulo Editor 是如何实现实时协同编程的

最近折腾了一下 Cirru Editor 的新版本: Cumulo Editor,
相关代码和演示可以在这些地址找到:
https://github.com/Cirru/cumu...
https://github.com/mvc-works/...
http://weibo.com/1651843872/F...
https://twitter.com/jiyinyiyo...

协同编程是什么

网上能找到关于协同编辑的说明:
https://en.wikipedia.org/wiki...

A collaborative editor is a form of collaborative software application that allows several people to edit a computer file using different computers, a practice called collaborative editing.

There are two types of collaborative editing: real-time and non-real-time. In real-time collaborative editing (RTCE), users can edit the same file simultaneously, whereas in Non-real-time collaborative editing, the users do not edit the same file at the same time (similar to revision control systems). Collaborative real-time editors generally permit both the above modes of editing in any given instance.

一个有名的例子是 Google Docs, 它支持多人同时编辑.
你可以看到多个人的光标同时显示在网页上, 在各自的位置编辑文档.
这是一种很高效的合作写文档的方式, 可以非常快速基于对方的结果迭代.

要做到协同编程, 我们就要对代码提供相似的功能, 允许多个人编辑.
首先我们需要有服务器作为操作顺序的协调者, 用来保存和同步的状态,
接着需要有 Diff 算法, 使得通过网络传输的信息是最精简的,
然后还要有 Patch 算法, 并且在 Patch 前后光标的行为应当一致.
每个环节到要打磨到一定的程度, 否则很容易影响到正常编写代码的过程.
实际上基于文本的代码同步会遇到很多问题, 设置对编辑器原有功能也会造成影响.

Cirru 的语法树的设计

Cirru 里采用了一样的思路, 代码的核心形态并不是文本, 而是语法树,
而且完成了 Stack Editor 这样一个能比较快速地编辑代码的方案,
Stack Editor 基于 Clojure Macros 可以从语法树生成 Clojure 代码,
虽然不能生成全部的 Clojure 语法, 比如特殊符号, 比如 Macro,
但是可以覆盖很大一部分网页开发需要的功能, 从而提供完整方案.
由于 Stack Editor 是基于网页来做代码的编辑, 也就避开了文本编辑器的惯用方案.

Cirru 的语法树看起来是 ["def" "a" ["+" "1" "2"]] 这样一个数组的形态,
这个形态和 S 表达式对应, 也就是 Lisp 社区广泛接受的表达式方案.
对于这样一个语法树, 通过确定操作的类型, 以及操作的位置, 就能进行操作,
而且用户可以有个明确的坐标, 作为编辑时表达和确定编辑位置的方案.
基于这样的结构, 可以得到很精简的语法树, 也便于在网页上渲染.

除了表达式可以被 Cirru 记法替代, 整个源码文件结构都可以被改写,
比如文件抽象为命名空间, 用一个 HashMap 存储,
然后每个文件分成 ns defs proc 三部分, 其中 proc 表示脚本代码,
所以一个项目的源码看上去就像是这样:

{:package "app"
 :files {"app.main" {:ns ["ns" "app.main"]
                     :defs {"main!" ["defn" "main!" []]}
                     :proc []}
         "app.lib" {:ns ["ns" "app.lib"]
                    :defs {}
                    :proc []}}}

经过这样的思维转化, 协同编程的问题就被重新理解了,
不再问怎样同步文本, 而是说, 这棵树怎样进行修改的同步?
树的同步是个相对简单的问题, 因为操作树的一个分支, 通常不会影响到其他分支,
我们只要定义好节点, 定义好操作, 然后这些操作有确定的顺序, 就可以了.
之后通过服务器讲操作同步到多个不同的客户端, 就能形成同步的假想.

针对 Cumulo Diff 的优化

Cumulo 是我基于 React 设计一个通过 tree diff/patch 来跨客户端同步信息的方案,
React 有一份 Virtual DOM, 然后可以通过 Diff 分发然后 Patch 到不同的客户端,
而 Cumulo 则是在服务端生成 Diff, 让客户端精确地同步需要的数据,
这个特性也就适合运用在 Cirru 语法树的同步上面, 服务端维护好树, 客户端同步.
这里不深入讲 Cumulo, 可以看我之前的内容了解 Cumulo:
https://segmentfault.com/t/cu...
https://github.com/Cumulo/cum...

这里有个 tree diff 的效率问题, 因为数组的 Diff 其实比较低效,
因为数组比较难探测子节点之间如何匹配, 需要一个 key 来辅助,
比如 React 当中就借助一个 key 来判断子节点, 从而提高效率.
另一个问题是数组受到位置影响比较明显, 比如前面插入元素, 后面的坐标就改变了.
而 HashMap 相对来说是更稳定的实现, 只在祖先路径更改时坐标会改变.

于是我设计了一个新的数据结构, 原先 Cirru 表达式的格式是:

["def" "f" ["x" "y"]]

在新的格式当中存储成这样,

{:type :expr
 :data {"T" {:type :leaf, :text "def"}
        "j" {:type :leaf, :text "f"}
        "r" {:type :expr
             :data {"T" {:type :leaf, :text "x"}
                    "j" {:type :leaf, :text "y"}}}}}

为了方便理解, 可以认为是这样一个结构:

{"T" "def"
 "j" "f"
 "r" {"T" "x",
      "j" "y"}}

可以看到表达式的子节点变成了用 HashMap 存储, 增加了 key,
这个 key 的字典序是和表达式当中的子节点位置对应的,
在 Cirru 中我们会遇到任意位置可能插入新的节点, 就需要生成新的 key,
而且这个新的 key 要满足正确的字典序, 比如在两个中间插入, 或者头尾插入.
关于这个 key 的算法, 我实现了一个简单的类库来达成, 可以了解:
http://clojure-china.org/t/bi...

总之转化为 HashMap 之后, 树的 Diff 就快多了, 而且冲突也会变少,
于是一下子就绕过了很多问题, 协同编辑也就在一定范围内可行了.

Cumulo Editor 当前和未来

Cumulo Editor 整体基于 Stack Editor 的功能做了重新实现,
主打的是协同编辑, 同时也支持其他一些简单的编码辅助功能,
比如基于语法树跳转到定义, 快速在表达式之间跳转等等.
没有实现 Stack Editor 当中查找使用位置的功能, 以后也许会加上.

关于还是关于协同编辑部分, 目前没有做深度的测试,
比如我把服务器部署在阿里云香港, 这个延时完全是在可接受范围内的.
表达式的操作会等待服务器响应, 符号的修改是本地实时的,
至少目前测试看来, 编辑方面的流畅度没有明显的问题.
可能担心的还是人数和代码里增加之后, 编辑器是够延时增加引发问题.

另一个问题是协同编辑会遇到抢占文件保存的问题,
也就是一个问保存文件, 导致另一个人代码没完成就保存, 从而出错.
我认为这一点对实际开发的影响会不小, 甚至需要增加功能来规范.
当然现在也很难说, 我一个人比较搞不出那么多幺蛾子来,
所以如果有人感兴趣想一起测试, 微信上加我, 并且注明 "Cirru",
我会试着积累一下经验, 看下这方面究竟会有多大的问题.
由于整个方案基于 ClojureScript, 所以需要先有一些了解再开始尝试.

这东西首先很突破常规, 但比较难说是否派的上用场,
毕竟协同编辑带啦冲突的话, 可能个人的开发效率下降也未可知,
但是对于未知呢, 总是不能轻易下定论的, 花时间去探索一下总可以吧 :)

你可能感兴趣的:(cirru,clojurescript)