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

news/2024/11/29 23:36:40/

大家好,我是前端西瓜哥,这次来看看 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.ppmy.cn/news/62719.html

相关文章

基于Java的两个通用安全模块的设计与实现

摘 要 本文详细介绍了基于口令的身份认证与文件安全传输两个通用安全模块的设计原理和实现过程,分析了当前口令保存的安全性,提出了运用MD5算法等对口令进行处理,并将处理结果保存在数据库中的方法。同时为了进一步增强认证系统的灵活度,设计了用户注册时的口令模式选择、…

shell函数

shell函数 功能 使用函数可以避免代码重复使用函数可以将大的工程分割为若干小的功能模块,代码的可读性更强 一:函数的定义方法 方法一 function 函数名 { 命令序列 }举例: function user {useradd $nameecho 123456 | passwd --stdin …

ASN.1-PKCS10-x509

在国际标准ITU-T X.690 《Information technology – ASN.1 encoding rules: Specification of Basic Encoding Rules (BER), Canonical Encoding Rules (CER) and Distinguished Encoding Rules (DER)》中定义了ASN.1编码规则。对于一般数据类型(比如Integer、octe…

惊现数据库误操作后,看这家银行如何打造“零盲区”运维安全

第三方运维人员数据库误操作致业务中断 堡垒机凸显短板 这家银行如何破局? 走在数字化转型前沿的银行业,不断增加的IT资产、业务系统,给IT运维提出了更高的挑战。银行运维的核心之一在于数据安全、系统稳定,面临着庞杂的运维场景…

Java中的基本容器知识你真的了解过吗?

前言:尽量使用简单易懂的通俗语言让大家初步了解各个重要的知识点。博学之,审问之,慎思之,明辨之,笃行之。 一、容器(Collection) Collection容器其实是用来存储独立元素的各种数据结构&#xf…

iphone死机屏幕没反应?可以用这2种办法解决!

iPhone用的时间长了,难免不会遇到卡屏、死机的情况,如果出现这种状况我们应该怎么办呢,下面小编整理出来了几招解决方法,教大家解决iPhone卡屏、死机的问题。 一、强制重启 如果自己的iPhone一直处于卡屏无法操作,或死…

L4公司进军辅助驾驶,放话无图也能跑遍中国

作者 | Amy 编辑 | 德新 高阶智能驾驶走向规模量产,高精地图成为关键的门槛之一。今年,多家车企和智驾公司都喊出「不依赖高精地图,快速大规模落地」的口号。 华为、小鹏、元戎以及毫末等,可能是最快在国内量产 无高精图智…

TensorRT入门实战,TensorRT Plugin介绍以及TensorRT INT8加速

文章目录 一、TensorRT介绍,工作流程和优化策略TensorRT是什么TensorRT的工作流程TRT优化策略介绍 二、TensorRT的组成和基本使用流程三、TensorRT的基本使用流程四、TensorRT Demo代码 : SampleMNISTCaffe Parser方式构建 五. TensorRT Plugin基本概念工作流程API介绍Dynamic …