如何快速实现多人协同编辑?
引言
协同编辑是目前成熟的在线文档编辑软件必备的功能,比如腾讯文档就支持多人协同编辑,基本都是采用监听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做拦截,单独处理。以上提到的情况并不包含实现协同所需的全部功能,只是抛砖引玉,如果有其他没有考虑到的情况,可以用同样的方法处理。
一、向所有客户端同步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,以上就是这篇文章的全部内容了,欢迎读者在评论区留下你们的想法~
简单协同代码下载链接:
https://gcdn.grapecity.com.cn/forum.php?mod=attachment&aid=MjgzMDk0fGVlNTFkNGQ1fDE2OTA3NjM4Mjl8MHwxNzY0MDU%3D
扩展链接:
Spring Boot框架下实现Excel服务端导入导出
项目实战:在线报价采购系统(React +SpreadJS+Echarts)
Svelte 框架结合 SpreadJS 实现纯前端类 Excel 在线报表设计
相关文章:
如何快速实现多人协同编辑?
引言 协同编辑是目前成熟的在线文档编辑软件必备的功能,比如腾讯文档就支持多人协同编辑,基本都是采用监听command,然后同步此command给其他客户端来实现的,例如以下系列: https://gcdn.grapecity.com.cn/showtopic-…...
ThinkPHP 一对多关联
用一对多关联的前提 多的一方的数据库表有一的一方数据库表的外键。 举例,用户获取自己的所有文章 数据表结构如下 // 用户表 useruser_id - integer // 用户主键name - varchar // 用户名称// 文章表 articlearticle_id - integer // 文章主键title - varchar …...
C++基础篇(二)基本数组及示例
目录 一、一维数组1、定义和初始化2、访问和修改3、元素逆置和冒泡排序 二、二维数组(用指针进行访问与修改)1、定义和初始化2、访问与修改 三、更高维度的数组1、三维数组2、高维数组 一、一维数组 1、定义和初始化 在 C 中,可以使用下面的…...
C++多态练习题
目录 一.习题1: 解决下列测试代码所出现的问题 测试用例1: 测试用例2: 代码改进: 习题1总结: 二.习题2. 求类对象的大小 三.习题3: 代码解析 : 解析图: 四.习题4ÿ…...
ELD透明屏在智能家居中有哪些优点展示?
ELD透明屏是一种新型的显示技术,它能够在不需要背光的情况下显示图像和文字。 ELD透明屏的原理是利用电致发光效应,通过在透明基板上涂覆一层特殊的发光材料,当电流通过时,发光材料会发出光线,从而实现显示效果。 ELD…...
第十三章 利用PCA简化数据
文章目录 第十三章 利用PCA简化数据13.1降维技术13.2PCA13.2.1移动坐标轴 13.2.2在NumPy中实现PCA13.3利用PCA对半导体制造数据降维 第十三章 利用PCA简化数据 PCA(Principal Component Analysis,主成分分析)是一种常用的降维技术࿰…...
开源中文分词Ansj的简单使用
ANSJ是由孙健(ansjsun)开源的一个中文分词器,为ICTLAS的Java版本,也采用了Bigram HMM分词模型:在Bigram分词的基础上,识别未登录词,以提高分词准确度。 虽然基本分词原理与ICTLAS的一样&#…...
251_多线程_创建一个多线程的图像处理应用,其中每个线程负责对一部分图像进行处理,然后将处理后的结果合并为最终图像
举一个更丰富的例子来说明多线程的用法。 我们将创建一个多线程的图像处理应用,其中每个线程负责对一部分图像进行处理,然后将处理后的结果合并为最终图像。 这个例子可以更好地展示多线程并发处理的优势。 假设有一个函数 processImageSection,它会对图像的一个特定区域进…...
[吐槽Edge浏览器]关于Edge浏览器的闪退问题
这个浏览器嘛,在谷歌浏览器不能页面翻译后,一直是用的高高兴兴的,可突然有一天,Edge浏览器页面加载不出来了。 很慌,大概就是页面崩溃、加载失败什么的都出现过。 修了整整一天,不知道原因在哪,…...
数据包在网络中传输的过程
ref: 【先把这个视频看完了】:数据包的传输过程【网络常识10】_哔哩哔哩_bilibili 常识都看看 》Ref: 1. 这个写的嘎嘎好,解释了为啥4层7层5层,还有数据包封装的问题:数据包在网络中的传输过程详解_数据包传输_张孟浩_jay的博客…...
Acwing.875 快速幂
题目 给定n组ai , bi, pi,对于每组数据,求出akimod pi的值。 输入格式 第一行包含整数n。 接下来n行,每行包含三个整数ai , bi,pi。输出格式 对于每组数据,输出一个结果,表示aibimod pi的值。 每个结果占一行。 数…...
【决策树-鸢尾花分类】
决策树算法简介 决策树是一种基于树状结构的分类与回归算法。它通过对数据集进行递归分割,将样本划分为多个类别或者回归值。决策树算法的核心思想是通过构建树来对数据进行划分,从而实现对未知样本的预测。 决策树的构建过程 决策树的构建过程包括以…...
类与对象(中--构造函数)
类与对象(中--构造函数) 1、构造函数的特性2、默认构造函数3、编译器自动生成的默认构造函数(无参的)(当我们不写构造函数时)3.1 编译器自动生成的默认构造函数只对 自定义类型的成员变量 起作用࿰…...
Makefile学习1
文章目录 Makefile学习1Makefile简介Makefile重要性Makefile内容1) 显式规则2) 隐晦规则3) 变量的定义4) 文件指示5) 注释 Makefile规则规则默认目标多目标多规则目标伪目标 Makefile目标依赖头文件依赖自动生成头文件依赖关系 Makefile命令Makefile变量变量定义和使用赋值立即…...
城市内涝监测预警系统,科学“智治”应对灾害
近日,台风“杜苏芮”以摧枯拉朽之势给我国东南沿海地区带来狂风骤雨,福建的三个国家气象观测站日降水量突破历史极值。之后,“杜苏芮”一路北上。中央气象台预报称,7月29日至8月1日,北京、天津、河北、山东西部、河南北…...
切片[::-1]解析列表list表示的“非负整数加1”
列表数位表示非负整数,熟练操作“满十进位”。 (本笔记适合熟练操作Python列表list的 coder 翻阅) 【学习的细节是欢悦的历程】 Python 官网:https://www.python.org/ Free:大咖免费“圣经”教程《 python 完全自学教程》,不仅仅…...
Mac下certificate verify failed: unable to get local issuer certificate
出现这个问题,可以安装证书 在finder中查找 Install Certificates.command找到后双击,或者使用其他终端打开 安装完即可...
Django项目启动错误
uwsgi项目启动错误信息如下Did you install mysqlclient?Command pkg-config --exists mysqlclient returned non-zero exit status 1Command pkg-config --exists mariadb returned non-zero exit status 1.Traceback (most recent call last):File "/home/dream21th/co…...
Vue2 第十二节 Vue组件化编程 (二)
1. VueComponent 2. 单文件组件 一. VueComponent 组件本质上是一个名为VueComponent的构造函数,不是程序员定义的,是Vue.extend生成的只需要写<school/>或者<school><school/>,Vue解析时,会帮我们创建schoo…...
pycharm 远程连接服务器并且debug, 支持torch.distributed.launch debug
未经允许,本文不得转载,vx:837007389 文章目录 step1:下载专业版本的pycharmstep2 配置自动同步文件夹,即远程的工程文件和本地同步2.1 Tools -> Deployment -> configuration2.2 设置同步文件夹2.3 同步服务器…...
第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
在rocky linux 9.5上在线安装 docker
前面是指南,后面是日志 sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo dnf install docker-ce docker-ce-cli containerd.io -y docker version sudo systemctl start docker sudo systemctl status docker …...
MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
GitFlow 工作模式(详解)
今天再学项目的过程中遇到使用gitflow模式管理代码,因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存,无论是github还是gittee,都是一种基于git去保存代码的形式,这样保存代码…...
Linux nano命令的基本使用
参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...
C++实现分布式网络通信框架RPC(2)——rpc发布端
有了上篇文章的项目的基本知识的了解,现在我们就开始构建项目。 目录 一、构建工程目录 二、本地服务发布成RPC服务 2.1理解RPC发布 2.2实现 三、Mprpc框架的基础类设计 3.1框架的初始化类 MprpcApplication 代码实现 3.2读取配置文件类 MprpcConfig 代码实现…...
