协同编辑是目前成熟的在线文档编辑软件必备的功能,比如腾讯文档就支持多人协同编辑,基本都是采用监听command,然后同步此command给其他客户端来实现的,例如以下系列:
https://gcdn.grapecity.com.cn/showtopic-82517-1-1.html https://gcdn.grapecity.com.cn/showtopic-82518-1-1.html https://gcdn.grapecity.com.cn/showtopic-82519-1-1.html
这种做法可以快速实现大部分功能的协同操作,但是也有一些不足,我大致将这些不足分为两种类型:
第一种,command传递之后信息丢失,需要重写或修改command的,比如复制粘贴功能。
这种类型对应的是希望command生效,但实际上没有生效。
第二种,多人协同所必须的特殊功能,情况比较多:
1. 比如编辑一个单元格时,其他人不允许编辑此单元格,并有样式提醒;
2. A用户正在编辑时,B用户在上方插入了一行,此时A编辑的单元格也要下移,而不是保留在原位;
3. 缩放时不对其他页面有影响;
这种类型对应的是不希望command生效,或者希望改变command生效的效果。
如果你也在做协同,并且遇到了上述问题,那么这篇文章或许可以解答你心中的疑问。
先看下最终的实现效果吧:
在开始前,先对demo的架构做一个说明,我此次写的demo是html做前端,并用nodejs做服务端,前后端通信采用websocket的方式,目录结构如下:
大家在测试demo前,请务必认真阅读readme文件。
下面我就讲一下如何针对上面提到的几种情况做优化,以更好得满足协同的需求,整体的思路其实比较简单,无非就是对那些不满足需求的command做拦截,单独处理。以上提到的情况并不包含实现协同所需的全部功能,只是抛砖引玉,如果有其他没有考虑到的情况,可以用同样的方法处理。
这里用commandManager新增监听的方式来监听所有的操作,并用websocket发送到服务端。马赛克部分为后续其他代码逻辑,暂时不用看。
服务端仅做一个转发:
其他客户端接受到此消息,执行command即可:
到这里,开头提到的快速实现大部分操作的协同就已经完成了,后续的操作都是为了弥补当前方案的不足。
粘贴的command同步到其他客户端时,会执行失败,仔细对比发出的command和接收的command,会发现其中两个字段发生了变化:
这两个数组内部本应该是Range对象,但是却被转换成了不同的Object,这是由于我们使用了JSON.stringify方法,而用此方法序列化时并不支持Range对象,所以我们在客户端接受到此信息时,需要重新将其还原为Range:
其实你可能会发现当存在fromRanges的时候,我直接用了copyTo方法实现了粘贴,并没有重新执行command,效果其实是一样的。
这里还隐含这另一种情况:从外部复制内容,粘贴到spread,这时fromRanges对象是不存在的,那么我们就需要执行command了,当然执行之前要把pastedRanges数组的值变为Range类型。
即同一个单元格同一时间只能有一个用户编辑。这是协同编辑几乎必备的一个需求,看起来很简单,但事实上是比较复杂的。当客户端有用户开始编辑时,向服务端发送消息,
而服务端需要维护一个数组,记录所有当前正在被编辑的单元格信息,并向所有客户端同步
其他客户端收到消息后,用户如果要编辑此单元格,则禁止用户进入编辑状态
当然,用户可能希望看到有哪些人正在编辑哪些单元格,类似于这种效果:
这里是用自定义单元格的方案实现的:
这个功能算是初步实现了,但是考虑一下这种情况:如果你正在编辑时,其他用户在上方插入了一行呢?
Lily本来正在编辑A2,Alen在上方插入一行后,Lily应该编辑的是A3,但是以我们目前的实现方式,Lily编辑的仍然是A2。对应的,在上方删除行、在左侧插入删除列都会有同样的问题。
这里Lily和Alen两个人都会受到影响,Lily编辑的单元格应该移动,Alen被锁定的单元格也应该移动,而Alen这边比较简单,服务端根据插入行列更新锁定单元格信息就好,Lily这边则麻烦一些,需要记录下Lily已经输入的功能,并且在新的单元格打开,并开启输入框,其中callback函数就是选择新的输入框的逻辑,根据不同的状态有所不同,所以用回调函数的形式实现。
相信你也注意到,在上述处理中,行列的变化信息是很重要的,在原生command的基础上还要有编辑框的处理逻辑,所以行列的变化也需要我们单独来处理,在客户端收到行列变化的消息时, 做出拦截:
并对编辑的框做出正确的移动
到这里,这篇文章也接近尾声了,整体实现的思路其实比较简单,无非就是拦截那些不符合协同需求或者同步时有问题的command,并重新实现它们。这种方式能够快速实现简单的协同,并且做出定制化的修改。
但是这种方式也存在着一些问题,比如无法支持undo堆栈,你可以在代码中看到我会随时清空undo堆栈,阻止用户进行undoredo操作,这是因为用commandManager.execute的方式执行command时,一定会进入到堆栈,这就导致A用户的操作会出现在B用户的undo堆栈中,B用户撤销时,就有可能撤销A用户的操作。
除了上面这个问题以外,一定还有其他更深、更棘手的问题存在,所以要在实际项目中实现协同,我的想法是根据业务限制用户的操作类型,并对这些有限的操作针对性地开发协同功能,这样虽然效率比较低,但是由于涉及面小,更便于控制。
OK,以上就是这篇文章的全部内容了,欢迎读者在评论区留下你们的想法~