DUICUO

Yjs + Quill: 共同編集をサポートするリッチテキストエディタを素早く実装

皆さんこんにちは。フロントエンド開発者のWatermelon Broです。今日は、YJSを使って共同編集機能を実現する方法を見ていきましょう。

Y.jsは共同編集をサポートするオープンソースライブラリです。データをY.jsが提供するY.ArrayまたはY.Map型に変換すれば、Y.jsが自動的にデータの整合性と同期を処理します。

一貫性の問題

共同編集における難しい問題の 1 つは、複数のユーザーが同時に編集することによって生じる競合をどのように処理し、一貫性をどのように確保するかということです。

たとえば、2 人のユーザーが同時にテキストの末尾に異なる文字を追加した場合、どちらの文字が最初に表示され、どちらの文字が最後に表示されるでしょうか。

現在、業界には2つのソリューションがあります。1つは、より主流のアプローチであるOT(オペレーショナル・トランスフォーメーション)アルゴリズムです。人気の高いオープンソースソリューションはShareDBです。

その中核は変換にあります。サーバーは、2 つのクライアントから同じバージョンのデータに対するアトミック操作を受け取り、それを各クライアントが実行する必要のある異なる操作に変換し、それを各クライアントに渡して適用し、最終的にコンテンツの一貫性を保ちます。

もう1つのタイプはCRDT(Conflict-free Replicated Data Type)で、主に分散システムで使用され、中央サーバーを必要としません。人気のオープンソースソリューションはYJSです。

しかし、CRDTはより多くのデータ転送を必要とし、メモリとパフォーマンスに大きなオーバーヘッドをもたらします。さらに、OTよりも後に提案され、学術研究も比較的少なかったため、当初は主流とは考えられていませんでした。

しかし、Yjs の登場と数多くのパフォーマンス最適化により、CRDT ソリューションは徐々に普及し、ますます多くの新しいコラボレーション ツールがデータ一貫性ソリューションとして Yjs の使用を選択するようになりました。

YJSは操作ベースのCRDTです。簡単に言えば、すべてのユーザー操作を記録し、それらの操作を二重連結リストに連結し、汎用アルゴリズムを用いて順序付けを保証します。最終的に、すべてのクライアントは同じ連結リストを取得でき、取得されたデータは自然に一貫性を保ちます。

Yjs + Quill: コラボレーションツールの構築

YJS の威力を体験するためにデモを書いてみましょう。

まず、viteを使ってフレームなしの基本的なscaffoldを構築します。ここではpnpmを使用しましたが、他のパッケージ管理ツールでも問題ありません。

 pnpm create vite

プロジェクト名はyjs-quill-demoです。Vanilla(フレームワークなし)を選択し、JavaScriptを選択します(TSに慣れている場合は選択することもできます)。

次に、フォルダーに移動し、依存関係をインストールして、プログラムを実行します。

 cd yjs-quill-demo pnpm install pnpm run dev

ブラウザを開き、コンソール出力にリンクを入力すると、次のように表示されます。

次に、依存関係をインストールします。

まず、オープンソースエディタ「Quill」とそのプラグイン「quill-cursors」があります。このプラグインは、他のユーザーのカーソル状態を表示できます。

 pnpm add quill quill-cursors

main.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', });

効果:

次に、quill に共同編集機能を追加する Yjs を紹介します。

YJS の公式ドキュメントには、quill データ モデルを YJS データ モデルにバインドできる y-quill ライブラリが用意されています。

 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); // 数据模型绑定

さて、次のステップはサーバーに接続してデータ転送を実装することです。サービスプロバイダーは、Yjsでは「プロバイダー」と呼ばれ、大まかに「サプライヤー」と訳すことができます。

YJS は、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が提供するデモサーバーです。いくつかの既知の理由により、このサーバーに接続できない場合があります。

同じブラウザで2つのタブを開くと、サーバー接続がなくても共同編集が可能であることがわかります。これは、YJSがブラウザホスト型の状態共有を優先し、その後にネットワーク通信を行うためです。そのため、デバッグには2つの異なるブラウザを使用することをお勧めします。

確認してみましょう。

左側の 2 つのタブは同じブラウザのものですが、右側のタブは別のブラウザのものです。

速度を 1 KB/秒に制限したタブのエディター コンテンツを変更すると、同じブラウザーの別のタブはすぐに変更されました (通信がローカルであったことが証明されています)。一方、別のブラウザーのタブははるかに遅くなりました (ネットワーク通信を使用していることを示しています)。

ローカルにサーバーをセットアップすることもできます。手順は次のとおりです。

 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を組み合わせた共同編集機能については、基本的な経験しかありませんでした。