Yjs + quill:快速实现支持协同编辑的富文本编辑器

news2025/1/11 5:55:50

大家好,我是前端西瓜哥,这次来看看 Yjs 如何帮助我们实现协同编辑能力的。

Y.js 是一个支持 协同编辑 的开源库。只要我们将自己的数据转换为 Y.js 提供的 Y.ArrayY.Map 类型,Y.js 就会自动帮我们做数据的一致性处理和同步。

一致性问题

协同编辑一个很棘手的问题是:多个用户同时编辑产生的冲突要怎么处理,如何保证一致性?

比如两个用户同时往一个文本的末尾加上不同的字符,最终谁的字符在前,谁的字符在后?

目前业界有两种方案,一个是 OT (Operational transformation)算法,是比较主流的一种解法。流行的开源解决方案是 ShareDB。

它的核心在于 Transform(转换):服务端接收两个客户端的对同一版本数据的原子操作行为,转换出它们各自要做的不同操作,然后传递给各个客户端并应用,最终让它们的内容是一致的。

我之前写过一篇介绍 OT 算法的文章,讲的会更详细一些,可以去看看:

《协同编辑中使用的 OT 算法是什么?》

另一种是 CRDT(Conflict-free Replicated Data Type),中文就是 “无冲突复制数据类型”,主要被应用在分布式系统中,即可以不需要中心化服务器。流行的开源方案是 Yjs。

但 CRDT 需要传输更多的数据,有不小的内存和性能开销,且相比 OT 被提出地更晚,学术研究相对较少,所以一开始算不上是主流。

然而随着 Yjs 的出现并做了不少性能优化,CRDT 方案也逐渐流行了起来,越来越多新的协同工具选择使用 Yjs 来作为数据一致性的解决方案。

Yjs 是基于操作的 CRDT,其原理简单来说,就是记录所有用户的操作,这些操作会拼接到一个双向链表中,并通过通用的算法保证确定的顺序,最后所有客户端都能得到相同的一条链表,最后得到的数据自然也是一致的。

Yjs + Quill:打造协同工具

我们来写个 demo 感受一下 Yjs 的强大之处。

先用 vite 搭个普通的不带框架的脚手架,这里我用的 pnpm,其他包管理工具也行。

pnpm create vite

项目名为 yjs-quill-demo,选择 Vanilla(不用框架的意思),然后选择 JavaScript(如果你熟悉 TS,也可以选 TS)

接着是进入文件夹,安装依赖,并运行。

cd yjs-quill-demo
pnpm install
pnpm run dev

打开浏览器输入控制台输出的链接,可以看到:

下面我们来安装依赖。

首先是开源编辑器 quill 和它的插件 quill-cursors。这个插件可以展示一些其他用户的光标的状态

pnpm add quill quill-cursors

将 mian.js 文件原来的内容删除,加上下面内容:

import Quill from 'quill';
import QuillCursors from 'quill-cursors';
import 'quill/dist/quill.snow.css'; // 使用了 snow 主题色

// 使用 cursors 插件
Quill.register('modules/cursors', QuillCursors);

const quill = new Quill(document.querySelector('#app'), {
  modules: {
    cursors: true,
    toolbar: [
      [{ header: [1, 2, false] }],
      ['bold', 'italic', 'underline'],
      ['image', 'code-block'],
    ],
    history: {
      userOnly: true, // 用户自己实现历史记录
    },
  },
  placeholder: '前端西瓜哥...',
  theme: 'snow',
});

效果:

下面我们就要引入 Yjs,给 quill 加上协同编辑功能。

Yjs 官方提供了 y-quill 库,通过它可以将 quill 数据模型和 Yjs 数据模型进行绑定

pnpm add yjs y-quill

追加 Yjs 相关逻辑:

import * as Y from 'yjs';
import { QuillBinding } from 'y-quill';
// ...

const ydoc = new Y.Doc(); // y 文档对象,保存需要共享的数据
const ytext = ydoc.getText('quill'); // 创建名为 quill 的 Text 对象
const binding = new QuillBinding(ytext, quill); // 数据模型绑定

ok,接下来就是要接上服务端,实现数据传输了。服务的提供者,Yjs 称为 provider,大概可以翻译为 “供应者” 的意思。

Yjs 官方提供了几种 Provider:WebRTC、WebSocket、Dat。

这里我们用比较常见的 WebSocket。

pnpm add y-websocket

代码:

import { WebsocketProvider } from 'y-websocket';
// ...

// 连接到 websocket 服务端
const provider = new WebsocketProvider('wss://demos.yjs.dev', 'quill-demo-room', ydoc);
// 数据模型绑定,再额外绑上了光标对象
const binding = new QuillBinding(ytext, quill, provider.awareness); 

这里的服务器用的是 Yjs 提供的 demo 体验用的服务器,因为一些喜闻乐见的原因,可能会连不上这个服务器。

然后你会发现,如果在同一浏览器打开两个 tab,没连上服务也能做协同编辑。这是因为 Yjs 会优先通过浏览器的同 host 共享状态的方式进行通信,然后才是网络通信。所以最好是打开两个不同的浏览器做调试。

我们验证一下。

左边两个 tab 页来自同一个浏览器,右边则是另一个浏览器。

当修改被我限速为 1 KB/s 的 tab 的编辑器内容时,来自同一浏览器的另一个 tab 页立刻发生了变更(证明通信走的是本地),而另一个浏览器的 tab 则慢得多(说明走的网络通讯)。

我们也可以自己在本地起一个服务器,做法是:

HOST=localhost PORT=1234 npx y-websocket

对应着要改一下客户端代码中 ws 服务的地址:

const provider = new WebsocketProvider('ws://localhost:1234', 'quill-demo-room', ydoc);

完整代码

import Quill from 'quill';
import QuillCursors from 'quill-cursors';
import 'quill/dist/quill.snow.css'; // 使用了 snow 主题色
import * as Y from 'yjs';
import { QuillBinding } from 'y-quill';
import { WebsocketProvider } from 'y-websocket';

// 使用 cursors 插件
Quill.register('modules/cursors', QuillCursors);

const quill = new Quill(document.querySelector('#app'), {
  modules: {
    cursors: true,
    toolbar: [
      [{ header: [1, 2, false] }],
      ['bold', 'italic', 'underline'],
      ['image', 'code-block'],
    ],
    history: {
      userOnly: true, // 用户自己实现历史记录
    },
  },
  placeholder: '前端西瓜哥...',
  theme: 'snow',
});

const ydoc = new Y.Doc(); // y 文档对象,保存需要共享的数据
const ytext = ydoc.getText('quill'); // 创建名为 quill 的 Text 对象
// 连接到 websocket 服务端
const provider = new WebsocketProvider('wss://demos.yjs.dev', 'quill-demo-room', ydoc); 
// 数据模型绑定,再绑上光标对象
const binding = new QuillBinding(ytext, quill, provider.awareness); 

结尾

因为用了很多 Yjs 提供的模块化的包,其实我们并没有接触到太多的实现细节,尤其是将数据绑定到 Yjs 提供的类型数据的实现。只能说是简单体验了 Yjs 配合 quill 实现协同编辑的效果。

我是前端西瓜哥,欢迎关注我,学习更多前端知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/495505.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Cookie和Session的API、登录页面

目录 一、Cookie 和 Session 1、HttpServletRequest 类中的相关方法 2、HttpServletResponse 类中的相关方法 3、HttpSession 类中的相关方法 4、Cookie 类中的相关方法 二、网页登录 1、约定前后端交互接口 2、编写一个简单的登录页面 3、编写一个Servlet 来处理这个…

Springboot +Flowable,任务认领和回退(二)

一.简介 有的时候,一个任务节点会存在多个候选人,例如:张三提交一个任务,这个任务即可以由李四处理,又可以由王五处理,那么针对这种多个任务候选人的情况,该如何处理? 二.绘制流程…

SuperMap GIS基础产品组件GIS FAQ集锦(2)

SuperMap GIS基础产品组件GIS FAQ集锦(2) 【iObjects for Spark】读取GDB参数该如何填写? 【解决办法】可参考以下示例: val GDB_params new util.HashMapString, java.io.Serializable GDB_params.put(FeatureRDDProviderParam…

spi 应用层读值为0问题

昨天调SPI遇到读值为0x00&#xff0c;经排查是读写方向的问题。 #include <stdint.h> #include <stdio.h> #include <stdlib.h…

Consensus见闻:雷声大 却不下雨的奧斯汀

前言 由Coindesk举办的Consensus历时3天&#xff0c;于4月28日完美落幕&#xff0c;欧科云链研究院前往美国得克萨斯州奧斯汀参加&#xff0c;本文将分享我们在奧斯汀和Consensus会议中的所见所闻&#xff0c;带你们看一个不一样的奧斯汀。 出品&#xff5c;欧科云链研究院 作…

datagrip连接elasticsearch且进行查询20230506

背景&#xff1a;公司要做一个es的数据存储&#xff0c;然后通过接口进行查询&#xff0c;我在docker下完成了ELK的安装&#xff0c;但是对es还不是很了解&#xff0c;就想着用logstash加载完数据到es中后&#xff0c;在数据库中对es进行查询&#xff0c;发现datagrip是支持连接…

Boosting之Adaboost与GBDT

同质与异质 1.异质模型&#xff1a;把不同类型的算法集成在一起&#xff0c;基础模型要有足够大差异性&#xff08;可以找出最适合当前数据的模型&#xff09; 同质模型&#xff1a;通过一个基础算法生成的同类型学习器。 Boosting概念介绍 Boosting本意就是提升&#xff0…

腾讯云服务器怎么开通端口?以80端口为例轻量和CVM教程合集

腾讯云服务器怎么放通80端口&#xff1f;腾讯云服务器分为云服务器CVM和轻量应用服务器&#xff0c;CVM云服务器在安全组中配置规则开启80端口&#xff0c;轻量应用服务器在防火墙中开通80端口&#xff0c;阿腾云来详细详细说下腾讯云服务器开通80端口教程&#xff1a; 目录 …

【c语言】字符串拼接 | API仿真

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 给大家跳段街舞感谢支持&#xff01;ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ …

linux usb gadget driver代码

本文基于linux-5.4.124 aspeed 2600(BMC)的代码实现来描述arm结构下的gadget driver. 在读之前&#xff0c;我们需要了解什么是usb gadget driver&#xff0c;以及它的作用。 从英文字面上翻译看&#xff0c;usb gadget driver是一个usb小工具驱动。这说了等于没说。实际上&a…

如何通过代码接入手机在网状态 API

引言 在许多场景下&#xff0c;手机号码是一种常用的身份验证信息。而使用手机在网状态 API 可以判断出手机号码是否有效&#xff0c;在一定程度上提高了身份验证的准确性和安全性&#xff0c;它的出现和广泛应用&#xff0c;为各行各业提供了更为便利和高效的解决方案。 本文…

城市夜景照明对于安科瑞智能照明系统的运用

安科瑞 徐浩竣 江苏安科瑞电器制造有限公司 zx acrelxhj 摘要&#xff1a;文章以智能照明控制系统为切入点&#xff0c;介绍了智能照明控制系统在城市夜景照明工程中的应用价值&#xff0c;并结合具体案例分析了城市夜景照明控制管理平台的设计和具体应用。智能照明控制系统…

微软Bing突然爆炸级更新!无需等待人人可用,答案图文并茂

所有人都能上手微软Bing了&#xff01; 今天&#xff0c;微软突然官宣全面开放BingChat&#xff1a; 无需任何等待。只需注册一个账户&#xff0c;首页即可体验。 更关键的是&#xff0c;还有一大堆堪称“家底”的新功能来袭&#xff01; 支持100种语言多模态输出、持续聊天…

控制您的 AWS VPC 终端节点

随着 Amazon Web Services &#xff08;AWS&#xff09; 越来越受欢迎&#xff0c;现在控制您自己的 AWS 虚拟私有云 &#xff08;VPC&#xff09; 终端节点比以往任何时候都更加重要。这可以通过配置和管理 VPC 终端节点中的不同设置来完成。在本文中&#xff0c;我们将讨论为…

yolo目标检测2:yolov1整体思想和网络架构

上一节&#xff1a;https://blog.csdn.net/weixin_39107270/article/details/130408407 概念 You only look once 把检测问题转化成回归问题&#xff0c;一个CNN就搞定了。 2. 核心思想 每个点处有2种候选框&#xff0c;如果候选框内有目标&#xff0c;对候选框进行微调&am…

初学Verilog语言基础笔记整理(实例点灯代码分析)持续更新~

实例&#xff1a;点灯学习 一、Verilog语法学习 1. 参考文章 刚接触Verilog&#xff0c;作为一个硬件小白&#xff0c;只能尝试着去理解&#xff0c;文章未完…持续更新。 参考博客文章&#xff1a; Verilog语言入门学习&#xff08;1&#xff09;Verilog语法【Verilog】一文…

NoSQL自述---衍生过程

NoSQL概述 一.数据存储的演化史 1.单机MySQL的美好年代 在90年代&#xff0c;一个网站的访问量一般都不大&#xff0c;用单个数据库完全可以轻松应付。在那个时候&#xff0c;更多的都是静态网页&#xff0c;动态交互类型的网站不多。 上述架构下&#xff0c;我们来看看数据…

计算机毕业论文内容参考|软件工程|基于java开发汽车销售系统资料

文章目录 导文资料1简述模块与功能总结资料二摘要前言绪论课题背景国内外现状与趋势相关技术与方法介绍导文 如下是一个 Java 实现的汽车销售系统的简单描述,供参考。 计算机毕业论文内容参考|软件工程|基于java开发汽车销售系统资料 资料1 简述 汽车销售系统是专门为汽车销…

jetcache:阿里这款多级缓存框架一定要掌握

0. 引言 之前我们讲解了本地缓存ehcache组件&#xff0c;在实际应用中&#xff0c;并不是单一的使用本地缓存或者redis&#xff0c;更多是组合使用来满足不同的业务场景&#xff0c;于是如何优雅的组合本地缓存和远程缓存就成了我们要研究的问题&#xff0c;而这一点&#xff…

前端002_初始化项目

1、命名和启动项目 将目录名 vue-admin-template-master 重命名为 db-manager-system 将 db-manager-system/package.json 中的 name 值改为 db-manager-system {"name": "db-manager-system","version": "1.0.1","descriptio…