找回密码
 立即注册

QQ登录

只需一步,快速开始

xcymoo
超级版主   /  发表于:2023-7-14 17:57  /   查看:4113  /  回复:4
本帖最后由 xcymoo 于 2024-6-21 10:12 编辑

协同编辑是目前成熟的在线文档编辑软件必备的功能,比如腾讯文档就支持多人协同编辑,论坛中已经有一些文章介绍了如何做协同,基本都是采用监听command,然后同步此command给其他客户端来实现的,例如以下系列:
这种做法可以快速实现大部分功能的协同操作,但是也有一些不足,我大致将这些不足分为两种类型:

第一种,command传递之后信息丢失,需要重写或修改command的,比如复制粘贴功能。
这种类型对应的是希望command生效,但实际上没有生效。

第二种,多人协同所必须的特殊功能,情况比较多:
1. 比如编辑一个单元格时,其他人不允许编辑此单元格,并有样式提醒;
2. A用户正在编辑时,B用户在上方插入了一行,此时A编辑的单元格也要下移,而不是保留在原位;
3. 缩放时不对其他页面有影响;
这种类型对应的是不希望command生效,或者希望改变command生效的效果。

如果你也在做协同,并且遇到了上述问题,那么这篇文章或许可以解答你心中的疑问。

先看下最终的实现效果吧:

在开始前,先对demo的架构做一个说明,我此次写的demo是html做前端,并用nodejs做服务端,前后端通信采用websocket的方式,目录结构如下:
image.png994199218.png
大家在测试demo前,请务必认真阅读readme文件。

下面我就讲一下如何针对上面提到的几种情况做优化,以更好得满足协同的需求,整体的思路其实比较简单,无非就是对那些不满足需求的command做拦截,单独处理。以上提到的情况并不包含实现协同所需的全部功能,只是抛砖引玉,如果有其他没有考虑到的情况,可以用同样的方法处理。

一、向所有客户端同步command
这里用commandManager新增监听的方式来监听所有的操作,并用websocket发送到服务端。马赛克部分为后续其他代码逻辑,暂时不用看。
image.png826641235.png
服务端仅做一个转发:
image.png744886474.png
其他客户端接受到此消息,执行command即可:
image.png535182189.png
到这里,开头提到的快速实现大部分操作的协同就已经完成了,后续的操作都是为了弥补当前方案的不足。

二、处理粘贴
粘贴的command同步到其他客户端时,会执行失败,仔细对比发出的command和接收的command,会发现其中两个字段发生了变化:
image.png976373901.png
这两个数组内部本应该是Range对象,但是却被转换成了不同的Object,这是由于我们使用了JSON.stringify方法,而用此方法序列化时并不支持Range对象,所以我们在客户端接受到此信息时,需要重新将其还原为Range:
image.png285567321.png
其实你可能会发现当存在fromRanges的时候,我直接用了copyTo方法实现了粘贴,并没有重新执行command,效果其实是一样的。
这里还隐含这另一种情况:从外部复制内容,粘贴到spread,这时fromRanges对象是不存在的,那么我们就需要执行command了,当然执行之前要把pastedRanges数组的值变为Range类型。

三、编辑状态唯一
即同一个单元格同一时间只能有一个用户编辑。这是协同编辑几乎必备的一个需求,看起来很简单,但事实上是比较复杂的。
当客户端有用户开始编辑时,向服务端发送消息,
image.png21329649.png
而服务端需要维护一个数组,记录所有当前正在被编辑的单元格信息,并向所有客户端同步
image.png358923034.png
其他客户端收到消息后,用户如果要编辑此单元格,则禁止用户进入编辑状态
这里有一个点需要注意,如果此单元格是一个日期类的下拉框,则不会进入editStarting事件,此时需要用其他方式同步此信息并阻止用户打开日期框。
image.png98552550.png
当然,用户可能希望看到有哪些人正在编辑哪些单元格,类似于这种效果:
image.png519109191.png
这里是用自定义单元格的方案实现的:
image.png646333617.png

这个功能算是初步实现了,但是考虑一下这种情况:如果你正在编辑时,其他用户在上方插入了一行呢?
Lily本来正在编辑A2,Alen在上方插入一行后,Lily应该编辑的是A3,但是以我们目前的实现方式,Lily编辑的仍然是A2。对应的,在上方删除行、在左侧插入删除列都会有同样的问题。
这里Lily和Alen两个人都会受到影响,Lily编辑的单元格应该移动,Alen被锁定的单元格也应该移动,而Alen这边比较简单,服务端根据插入行列更新锁定单元格信息就好,Lily这边则麻烦一些,需要记录下Lily已经输入的功能,并且在新的单元格打开,并开启输入框,其中callback函数就是选择新的输入框的逻辑,根据不同的状态有所不同,所以用回调函数的形式实现。
image.png825100402.png

四、行列变动同步
相信你也注意到,在上述处理中,行列的变化信息是很重要的,在原生command的基础上还要有编辑框的处理逻辑,所以行列的变化也需要我们单独来处理,在客户端收到行列变化的消息时, 做出拦截:
image.png739841003.png
并对编辑的框框做出正确的移动
image.png816550292.png

结语
到这里,这篇文章也接近尾声了,整体实现的思路其实比较简单,无非就是拦截那些不符合协同需求或者同步时有问题的command,并重新实现它们。这种方式能够快速实现简单的协同,并且做出定制化的修改。
但是这种方式也存在着一些问题,比如无法支持undo堆栈,你可以在代码中看到我会随时清空undo堆栈,阻止用户进行undoredo操作,这是因为用commandManager.execute的方式执行command时,一定会进入到堆栈,这就导致A用户的操作会出现在B用户的undo堆栈中,B用户撤销时,就有可能撤销A用户的操作。
除了上面这个问题以外,一定还有其他更深、更棘手的问题存在,所以要在实际项目中实现协同,我的想法是根据业务限制用户的操作类型,并对这些有限的操作针对性地开发协同功能,这样虽然效率比较低,但是由于涉及面小,更便于控制。
OK,以上就是这篇文章的全部内容了,欢迎读者在评论区留下你们的想法~


简单协同.zip

11.9 KB, 下载次数: 969

评分

参与人数 2满意度 +10 收起 理由
卿词。 + 5
清泉自涌 + 5

查看全部评分

4 个回复

倒序浏览
拖拉机
注册会员   /  发表于:2023-7-19 21:36:37
沙发
太厉害了,协同表格非常有用
回复 使用道具 举报
Joestar.XuSpreadJS 开发认证
超级版主   /  发表于:2023-7-20 08:56:20
板凳
拖拉机 发表于 2023-7-19 21:36
太厉害了,协同表格非常有用

回复 使用道具 举报
Jun2005
注册会员   /  发表于:2023-11-20 11:20:35
地板
不太明白 gcexcel怎么做到实时去更新json,不可能每次操作都做一次open json吧?
回复 使用道具 举报
Joestar.XuSpreadJS 开发认证
超级版主   /  发表于:2023-11-20 15:58:35
5#
Jun2005 发表于 2023-11-20 11:20
不太明白 gcexcel怎么做到实时去更新json,不可能每次操作都做一次open json吧?

GcExcel在此处的作用是根据Command执行相关的操作,具体可以参考这个链接中的文章:https://gcdn.grapecity.com.cn/showtopic-82519-1-1.html
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 立即注册
返回顶部