Nuxt3 + Tiptap + Y.js でリアルタイム共同編集が可能なエディターを作る
NotionやGoogle Docsなど、最近のオンラインドキュメントエディターでは、リアルタイムで複数人による共同編集が可能なサービスをよく見かけます。
便利ですよね、リアルタイムでの共同編集。 自分も日々お世話になっています。
最近、お仕事で上記サービスのような共同編集が可能なエディターを実装する機会があったのですが、そちらで採用した Tiptap というエディターライブラリがかなりいい感じだったので、今回その実装を簡単なサンプルとして紹介してみたいと思います。
なお、今回作成したサンプルのソースコードはGitHubに公開しています。
https://github.com/amotz/nuxt-tiptap-sample
完成イメージ
検証環境
- nuxt
3.2.0
- tiptap
2.0.0-beta.217
- y-prosemirror
1.0.20
- y-websocket
1.4.5
- yjs
13.5.45
Nuxt3 プロジェクト作成
今回のサンプルはフロントエンドのフレームワークとしてNuxt 3を利用するので、まずは適当に新しいNuxtプロジェクトを作成しておきます。
$ npx nuxi init nuxt-tiptap-sample
Tiptapを使ったリッチテキストエディター実装
次に、ユーザーで編集可能なリッチテキストエディターを Tiptap を使って実装してみます。
Tiptapとは
Tiptap はリッチテキストエディターのツールキットであるProseMirrorのヘッドレスラッパーで、サクッとWYSIWYGエディターを構築できるオープンソースのライブラリです。
公式ドキュメントが充実しており、デフォルトで多くの拡張機能が用意されているためカスタマイズ性も良好なライブラリになっています。(一部の拡張機能は有償としてプライベートレジストリで提供)
Tiptap はVue.js(Nuxt.js)
/React(Next.js)
/Svelte
など、様々なフレームワークで利用することができます。
また、共同編集機能も提供しており、リアルタイムでのコラボレーションを実現することができます。
Tiptap インストール&コンポーネント実装
それでは、実際に以下を参考にしてTiptapをインストールしてみます。
Tiptap Documentation - Installation - Vue.js 3
$ npm install @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit
インストールできたら、以下のようにエディター用のコンポーネントを実装します。
Tiptapはヘッドレスなので、雑にCSSでスタイルも設定しておきます。
$ npm install sass --save-dev
<template> <editor-content :editor="editor" /> </template> <script> import { Editor, EditorContent } from '@tiptap/vue-3' import StarterKit from '@tiptap/starter-kit' export default { components: { EditorContent, }, data() { return { editor: null, } }, mounted() { this.editor = new Editor({ content: '', extensions: [ StarterKit, ], }) }, beforeDestroy() { this.editor.destroy() }, } </script> <style lang="scss"> /* Basic editor styles */ .ProseMirror { > * + * { margin-top: 0.75em; } border: 1px solid #e0e0e0 !important; box-sizing: border-box; border-radius: 3px; padding: 16px 16px 16px 16px; font-size: 14px; code { background-color: rgba(#616161, 0.1); color: #616161; } } .content { padding: 1rem 0 0; h3 { margin: 1rem 0 0.5rem; } pre { border-radius: 5px; color: #333; } code { display: block; white-space: pre-wrap; font-size: 0.8rem; padding: 0.75rem 1rem; background-color:#e9ecef; color: #495057; } } </style>
アプリ側もコンポーネントを利用するように修正してみます。
<template> <div> <client-only> <tiptap-editor /> </client-only> </div> </template> <script> import TiptapEditor from '~/components/TiptapEditor.vue' export default { components: { TiptapEditor } } </script>
これで最低限エディターが動作する準備ができたので、ローカルでNuxtを起動してみます。
$ npm run dev
シンプルなWYSIWYGエディターが動作するようになりました。
Tiptap Extension + Y.jsによる共同編集の実装
エディターが実装できたので、次はリアルタイムで共同編集が実現できるような実装を追加してみます。
Tiptap では共同編集用の拡張ライブラリが用意されており、Y.jsと組み合わせることでリアルタイムの共同編集を実現できます。
Tiptap Documentation - Collaborative editing
Y.jsとは
今回共同編集の仕組みとして利用するY.js
は、クライアント間でリアルタイムにデータを同期するモジュラーフレームワークです。
WebSocketやWebRTCといったプロトコルを介して、クライアント間でデータを同期させます。
また、Y.jsではCRDTというデータ構造を用いて複数クライアントでの同時編集における衝突を解決しています。
CRDTやY.jsの理解については、以下がとても参考になりました。ありがとうございます!
共同編集用のライブラリインストール
それでは、共同編集に必要なライブラリをインストールしていきます。
今回はY.jsでデータ同期を行うためのプロバイダーとしてy-websocketを利用します。
$ npm install @tiptap/extension-collaboration yjs y-websocket y-prosemirror
共同編集用のWebSocketサーバー起動
y-websocketにはサーバーが実装されているので、ローカル環境でNuxtとは別にWebSocketサーバーを起動しておきます。
$ HOST=localhost PORT=1234 npx y-websocket
コンポーネント側の実装
コンポーネント側は、TiptapのExtensionを設定し共同編集用のプロバイダーとして先ほど起動したWebSocketサーバーのURL ws://localhost:1234
を指定します。
<template> <editor-content :editor="editor" /> </template> <script> import { Editor, EditorContent } from '@tiptap/vue-3' import StarterKit from '@tiptap/starter-kit' import Collaboration from '@tiptap/extension-collaboration' import { WebsocketProvider } from "y-websocket"; import * as Y from "yjs"; export default { components: { EditorContent, }, data() { return { editor: null, } }, mounted() { const ydoc = new Y.Doc(); this.provider = new WebsocketProvider("ws://localhost:1234", "sample-document", ydoc); this.editor = new Editor({ content: '', extensions: [ StarterKit, Collaboration.configure({ document: ydoc, }), ], }) }, beforeDestroy() { this.editor.destroy() this.provider.destroy(); }, } </script> (以下省略)
これで共同編集の実装ができたので、Nuxtのアプリを起動してみます。
$ npm run dev
リアルタイムに共同編集ができるエディターになりました!
共同編集者にカーソルを付ける
無事に共同編集ができるエディターが実装できたのですが、これだと誰がどこを編集しているのか分かりづらいため、編集者にカーソルを付けてみます。
Tiptap Documentation - Collaborative editing - Show other cursors
共同編集用のカーソルを実装するためのExtensionをインストールします。
$ npm install @tiptap/extension-collaboration-cursor
コンポーネント側もカーソルを出すような実装を追加します。
(今回はカーソルに表示させるユーザー名とカーソル色はランダムとしています)
<template> <editor-content :editor="editor" /> </template> <script> import { Editor, EditorContent } from "@tiptap/vue-3"; import StarterKit from "@tiptap/starter-kit"; import Collaboration from "@tiptap/extension-collaboration"; import CollaborationCursor from "@tiptap/extension-collaboration-cursor"; import { WebsocketProvider } from "y-websocket"; import * as Y from "yjs"; export default { components: { EditorContent, }, data() { return { editor: null, provider: null, }; }, mounted() { const ydoc = new Y.Doc(); this.provider = new WebsocketProvider( "ws://localhost:1234", "sample-document", ydoc ); this.editor = new Editor({ content: "", extensions: [ StarterKit, Collaboration.configure({ document: ydoc, }), CollaborationCursor.configure({ provider: this.provider, user: { name: this.getRandomName(), color: this.getRandomColor() }, }), ], }); }, beforeDestroy() { this.editor.destroy(); this.provider.destroy(); }, methods: { getRandomColor() { const list = [ "#958DF1", "#F98181", "#FBBC88", "#FAF594", "#70CFF8", "#94FADB", "#B9F18D", ]; return list[Math.floor(Math.random() * list.length)]; }, getRandomName() { const list = [ "Lea Thompson", "Cyndi Lauper", "Tom Cruise", "Madonna", "Jerry Hall", "Joan Collins", "Winona Ryder", "Christina Applegate", "Alyssa Milano", "Molly Ringwald", "Ally Sheedy", "Debbie Harry", "Olivia Newton-John", "Elton John", "Michael J. Fox", "Axl Rose", "Emilio Estevez", "Ralph Macchio", "Rob Lowe", "Jennifer Grey", "Mickey Rourke", "John Cusack", "Matthew Broderick", "Justine Bateman", "Lisa Bonet", ]; return list[Math.floor(Math.random() * list.length)]; }, }, }; </script> <style lang="scss"> /* Basic editor styles */ .ProseMirror { > * + * { margin-top: 0.75em; } border: 1px solid #e0e0e0 !important; box-sizing: border-box; border-radius: 3px; padding: 16px 16px 16px 16px; ul, ol { padding: 0 1rem; } h1, h2, h3, h4, h5, h6 { line-height: 1.1; } code { background-color: rgba(#616161, 0.1); color: #616161; } pre { background: #0d0d0d; color: #fff; font-family: "JetBrainsMono", monospace; padding: 0.75rem 1rem; border-radius: 0.5rem; code { color: inherit; padding: 0; background: none; font-size: 0.8rem; } } mark { background-color: #faf594; } img { max-width: 100%; height: auto; } hr { margin: 1rem 0; } blockquote { padding-left: 1rem; border-left: 2px solid rgba(#0d0d0d, 0.1); } hr { border: none; border-top: 2px solid rgba(#0d0d0d, 0.1); margin: 2rem 0; } ul[data-type="taskList"] { list-style: none; padding: 0; li { display: flex; align-items: center; > label { flex: 0 0 auto; margin-right: 0.5rem; user-select: none; } > div { flex: 1 1 auto; } } } } /* Give a remote user a caret */ .collaboration-cursor__caret { position: relative; margin-left: -1px; margin-right: -1px; border-left: 1px solid #0d0d0d; border-right: 1px solid #0d0d0d; word-break: normal; pointer-events: none; } /* Render the username above the caret */ .collaboration-cursor__label { position: absolute; top: -1.4em; left: -1px; font-size: 12px; font-style: normal; font-weight: 600; line-height: normal; user-select: none; color: #0d0d0d; padding: 0.1rem 0.3rem; border-radius: 3px 3px 3px 0; white-space: nowrap; } </style>
Nuxtのアプリを起動してみます。
$ npm run dev
編集者のカーソルが表示されるようになりました!分かりやすくていい感じになりましたね。
おわりに
Nuxt3 + Tiptap + Y.js でリアルタイム共同編集が可能なエディターを実装してみました。
今回Tiptapを初めて使いましたが、拡張性が高そう&ドキュメントも充実しており、素晴らしいライブラリだなと感じました。
どなたかの参考になれば幸いです。