在线协作编辑的处理

先说“在线”,在线其实就是个 B/S 或者 C/S 架构,也就是一个 web/native 用户端加上一个服务器端。这里的技术难点不算很大,主要就是把数据同步的问题解决了,即如何让用户端的输入及时准确地同步到服务器端,当然同时还要考虑网络传输失败、性能等等问题,这里 web 侧比较好的一个开源解决方案就是 PouchDB,它可以帮你快速实现数据在浏览器和服务器端的双向同步,有兴趣的可以了解一下。

然后是“多人协作”,本质上是一个分布式系统上常说的 Multiple Leader Duplication,任何一个用户端都可以视为一个 Data Leader,这些 Leader 之间同步数据必然会遇到冲突的问题。

对于 Multiple Leader Duplication 的冲突问题,解决方法也就那么几种:

直接避免产生冲突。具体方法就是不让多个用户同时编辑同一处地方,这种解决方法最有效也最粗暴,具体要看产品形态适不适合这种方案。把冲突暴露给用户,让用户自己解决。现在大多数专业的版本控制软件就是这么做的,但不适用于在线文档这种大部分用户都是非专业的产品。给写入操作打上一个全局 index,可以是时间戳,可以是序列号,总之是全局的并且是递增的即可,然后任何冲突的地方,都选择 index 较高的那个写入。这样的好处就是冲突的解决是完全自动化的,不需要用户参与。缺点就是如果遇到同步间隔很长的情况,会丢失很多用户的输入。比如你断网写了一小时文章,然后又连网同步,发现被你同事几分钟前的修改给覆盖了。这里推荐一本书叫《Designing Data-Intensive Applications》,里面的第五章 Duplication 就提到了在线协作文档的同步和冲突问题,有兴趣的可以读一下。

最后是“文档编辑”问题,这就是一个非常传统的界面开发了。这方面的技术和框架非常多,目前 Web 比较流行的解决方案就是 React/Redux 或者 Vue/Vuex 这样的类 Flux 架构,也就是单向数据流(当然用别的类似框架也无所谓)。

文档编辑器使用类 Flux 架构之后,整个应用的状态(或者叫 Model 层)可以切分为三大块:

文档的状态,即文档自身的内容,包括样式;编辑器的状态,比如富文本编辑器的光标位置、选中状态、编辑器菜单等等;应用自身的状态,比如登录状态、用户数据、权限管理等等。切分成这三大块之后,要做的事情就非常清晰了:

1、写 UI,并且把这些 Model 状态映射到 UI 上,这里完全不需要考虑输入,就是很纯净地把一堆 Model 映射到 UI 上。比如文档对象的渲染、编辑器各种状态的实现等等,比如你可以写代码把下面这个对象

{
type: 'text',
content: 'Hello World!'
style: {
color: '#aaa',
fontWeight: 'bolder'
}
}

映射为一段颜色是 #aaa 的加粗文本。

2、处理用户的输入,修改相应的状态,这里完全不需要考虑 UI 的渲染,只要用户的输入能正确地改变相应的 Model 即可。

比如用户在界面空白处点击了鼠标右键,我们需要打开右键菜单,具体做法就是把编辑器状态里的右键菜单设置为打开,并且设置好位置即可。具体这个菜单怎么在 UI 上展示,第一步已经做了,我们这里完全不需要关心。

虽然说起来这么简单,但实际开发的时候确实有很多难点:

用户的输入具有复杂性。比如在一个地方按下鼠标(mousedown),那么光标应该移动过去,但如果用户这时继续拖动鼠标(mousemove),那么就应该是一个拖选事件,如何正确区分点击和拖选就是一个比较麻烦的问题。类似的问题还有快捷键的实现、点击穿透问题,如果有移动端的话还会遇到定制光标样式的问题。编辑器本身的坑是非常非常多的。具体可以移步这个问题:为什么都说富文本编辑器是天坑?