🐸

Next.js+yjs+BlockNoteでGoogle Docs+Notionのような共同編集エディタを作ろう

2023/10/23に公開

サークルで制作してるアプリに、共同編集できるエディタを実装したいという話が上がり調査したところ

  • Notionライクなテキストエディタ「BlockNote」
  • yjsという良さげなデータ構造のライブラリ

を組み合わせたらいい感じに動作したので記事にしました。

yjsとは

Yjs is a modular framework for syncing things in real-time - like editors!

yjsはCRDT(a conflict-free replicated data type)を実装した競合しないデータ構造を扱えるフレームワークです。
分散コンピューティングに特化したデータ構造で、リアルタイムな共同編集などに用いることができ、マージの競合なしにデータを伝えることができます。

https://github.com/yjs/yjs
https://docs.yjs.dev/

BlockNoteとは

おしゃれなテキストエディターです。
Notionのようにカスタムブロックがあり、ドラッグ&ドロップでブロックを入れ替えたり、スラッシュでメニューが表示されたり、共同編集ができたり、ほぼNotionと遜色ない操作をすることができます。
まだ開発開始から半年も経ってもらず、Zennの記事も全然見当たりません。

https://github.com/TypeCellOS/BlockNote
https://www.blocknotejs.org/

できるもの

本記事は添付したXリンクの動画のような、最低限ドシンプル共同開発エディタを作ることを目標にします。
細かいカスタマイズや、入力した内容の取得などは行いませんが、大まかな内容については本記事末に記載しております。

https://fxtwitter.com/imaimai17468/status/1712443003946910164?s=20

また、作成したもののGitHubリポジトリはこちらになります。

https://github.com/imaimai17468/imaimai-whitespace

実際に触ってみよう

パッケージのインストール

npm i yjs y-websocket
npm i @blocknote/core @blocknote/react

Let's コーディング

まずエディタを表示するためのコンポーネントを作ります。
y-webrtcではなくy-websocketを用いてる点を除けばBlockNoteの共同編集exampleとほぼ同じです。

Editor.tsx
"use client";
// BlockNoteのコア機能をインポート
import { BlockNoteEditor } from "@blocknote/core";
// BlockNoteのReact関連の機能とスタイルをインポート
import { BlockNoteView, useBlockNote } from "@blocknote/react";
import "@blocknote/core/style.css";
// ランダムにユーザーの情報を取得する関数をインポート
import { getRandomUser } from "@/pages/utils/randomUser";
// Yjsの同期機能を使うためのライブラリをインポート
import * as Y from "yjs";

// Yjsのドキュメントを生成
const doc = new Y.Doc();

// y-websocketを使ったプロバイダを設定
const { WebsocketProvider } = require("y-websocket");
const provider = new WebsocketProvider("ws://localhost:1234", "draw-room", doc);

// Editorコンポーネント
export default function Editor() {
  // BlockNoteのエディタを取得。ここでリアルタイム同期の設定も行っている。
  const editor: BlockNoteEditor | null = useBlockNote({
    collaboration: {
      provider,  // プロバイダの設定
      fragment: doc.getXmlFragment("document-store"),  // ドキュメントのフラグメントを取得
      user: getRandomUser(),  // ランダムにユーザーの情報を設定
    },
  });

  // エディタのビューをレンダリング
  return <BlockNoteView editor={editor} />;
}

getRandomUser関数については、partykit+BlockNoteでの開発exampleから拝借しています。

この関数を用いてランダムにユーザー名と色を取得してますが、propsとして受け取ってしまってもOKです。

y-websocketのimportについてはimport { WebsocketProvider } from 'y-websocket' と書くはずでしたが、宣言ファイルが存在しないと怒られたのでrequireを用いて持ってきています。

次にこれをページに反映させていきます。
動的importをすることで、documentの生成をSSR回避しています。

index.ts
import dynamic from "next/dynamic";

const Editor = dynamic(() => import("./components/Editor"), { ssr: false });

export default function App() {
  return <Editor />;
}

改良・改善

カスタマイズ

BlockNoteのカスタマイズについてはドキュメントのEditor Setupのくくりに日通り記述されています。

https://www.blocknotejs.org/docs/editor

幅広くもろもろデザインを修正することができます。

  • スラッシュメニューのテーマ変更
  • 入力する列のスタイル変更
  • ドラッグする時に表示されるツールバーの項目の非表示切り替え
  • オリジナルのスラッシュメニューの追加
  • スラッシュメニューの項目
  • サイドメニューのカスタマイズ

実際に使う際の機能面においても問題なく使用できそうです。

  • 一部・すべてのブロックの内容の取得
  • カーソルのカスタマイズ
  • 選択されたブロックの取得
  • Markdown/HTMLの取得

Next.jsへの導入方法もDocumentにご丁寧に書かれています。
https://www.blocknotejs.org/docs/nextjs

エラー発生

最後に、私のコードだと以下のようなエラーが発生すると思います。

issueを見ると同様の事案が報告されており、collaboration機能を使うと起こるそうで、非常に難しい問題であり、頑張って修正してるっぽいです。
対応策は一応書かれていますが、それですべてのユーザーで正常に動作するかは不明だそうです。

https://github.com/TypeCellOS/BlockNote/issues/366

おわりに

本記事ではBlockNoteをテキストエディタとして用いましたが、そのほかにもTipTapやProseMirror、Quillなど連携できるものがいくつかありますので、試してみていかがでしょうか。

記事を読んでいただきありがとうございました。

Discussion