森田です。 先日 BlockNote について紹介しましたが、今回は BlockNote と yjs と Liveblocks を用いた共同編集機能の実装をご紹介します。
共同編集と聞くと難しそうに感じますが、そんなことは全くなくとても簡単に実装できます。
早速実装といきたいところですが、少しだけ yjs と Liveblocks について説明を...
yjs とは
yjs は CRDT(Conflict-free Replicated Data Type)という技術を持ちたフレームワークです。つまりコンフリクトしないデータの型を扱うことができます。
詳しくはこちら ↓
Liveblocks とは
Liveblocks はリアルタイムでの共同編集やカーソル共有などのコラボレーション機能を簡単に追加できるツールキットです。
公式ドキュメントがとても充実しているので詳しくはこちらを参照ください ↓
準備
Nextjs を作成。今回は App Router + Typescript でいきます。そのほかは好みで設定してもらえればいいと思います。
$ npx create-next-app collaborative-editor
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Liveblocks 周りの設定
Liveblocks 関連のライブラリをインストールします
npm i @liveblocks/client @liveblocks/react @liveblocks/node @liveblocks/yjs
liveblocks を初期化
npx create-liveblocks-app@latest --init --framework react
何やら質問されるので、y で実行しましょう。
$ npx create-liveblocks-app@latest --init --framework react
Need to install the following packages:
create-liveblocks-app@2.20240816.0
Ok to proceed? (y) y
▀█████▀ ▄
▀██▀ ▄██▄
▀ ▄█████▄
Liveblocks
✨ liveblocks.config.ts has been generated!
これでアプリ側の設定は完了。
続いて API キーを取得しにいきましょう。
まずこちらからサインアップhttps://liveblocks.io/
API keys を選択
Generate key...をクリックして
キーを生成!
Public key と Secret key は後々使うのでメモしておきましょう。
BlockNote の設定
先日の記事と同じように blocknote のライブラリもインポートしておきましょう
npm i @blocknote/core @blocknote/react @blocknote/mantine
実装
ではいよいよ実装に入っていきましょう。
まず先程取得した API キーを環境変数に設定しておきましょう。
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= LIVEBLOCKS_SECRET_KEY=
続いてバックエンドを作成します。
import { Liveblocks } from "@liveblocks/node";
import { NextResponse } from "next/server";
import { nanoid } from "nanoid";
const liveblocks = new Liveblocks({
secret: process.env.LIVEBLOCKS_SECRET_KEY!,
});
export async function POST() {
const liveSession = liveblocks.prepareSession(nanoid());
liveSession.allow("*", liveSession.FULL_ACCESS);
const { body, status } = await liveSession.authorize();
return new NextResponse(body, { status });
}
続いてLiveblocks のライブラリを用いてルームの作成接続などの機能のコンポーネントを作成します。
"use client";
import { LiveblocksProvider, RoomProvider, ClientSideSuspense } from "@liveblocks/react/suspense";
import { ReactNode } from "react";
type Props = {
children: ReactNode;
};
export const Room = ({ children }: Props) => {
return (
<LiveblocksProvider authEndpoint={"/api/liveblocks-auth"}>
<RoomProvider id="test-room">
<ClientSideSuspense fallback={<div>...loading</div>}>{children}</ClientSideSuspense>
</RoomProvider>
</LiveblocksProvider>
);
};
authEndpoint
は先程作成したバックエンドのエンドポイントを設定しましょう。
次にBlockNoteにyjsを組み込んだエディタを作成します。
"use client";
import { useEffect, useState } from "react";
import { BlockNoteEditor } from "@blocknote/core";
import { useCreateBlockNote } from "@blocknote/react";
import { BlockNoteView } from "@blocknote/mantine";
import * as Y from "yjs";
import { LiveblocksYjsProvider } from "@liveblocks/yjs";
import { useRoom } from "@liveblocks/react/suspense";
import "@blocknote/core/fonts/inter.css";
import "@blocknote/mantine/style.css";
type EditorProps = {
doc: Y.Doc;
provider: any;
};
export function Editor() {
const room = useRoom();
const [doc, setDoc] = useState<Y.Doc>();
const [provider, setProvider] = useState<any>();
useEffect(() => {
const yDoc = new Y.Doc();
const yProvider = new LiveblocksYjsProvider(room, yDoc);
setDoc(yDoc);
setProvider(yProvider);
return () => {
yDoc?.destroy();
yProvider?.destroy();
};
}, [room]);
if (!doc || !provider) {
return null;
}
return <BlockNote doc={doc} provider={provider} />;
}
function BlockNote({ doc, provider }: EditorProps) {
const editor: BlockNoteEditor = useCreateBlockNote({
collaboration: {
provider,
fragment: doc.getXmlFragment("document-store"),
user: {
name: "User",
color: "#ff0000",
},
},
});
return <BlockNoteView editor={editor} />;
}
作成したコンポーネントをページに組み込んで...
import { Room } from "../Components/Room";
import { Editor } from "../Components/Editor";
const Page = () => {
return (
<Room>
<Editor />
</Room>
);
};
export default Page;
はい完成🎉
ちょっと簡単すぎじゃないですかね。
おまけ
ルームの接続設定について
Room
コンポーネントのRoomProvider
のidでルーム選択ができるようになっています。
<RoomProvider id="test-room">
今回はtest-room
になっていますね。
また Liveblocks のダッシュボードから作成されたルームを管理することもできます。
test-room
が作成されていますね。
またルームの id に制約をかけることもできます。
liveSession.allow("room:*", liveSession.FULL_ACCESS);
このようにroom:*
とするとルーム id にroom:
がついていないと接続できないようになります。
今ルーム id をtest-room
にしているのでそのままでは接続できないようになっているはずです。
room:test-room
にすると再度接続できるようになるということです。
接続中の表示
ClientSideSuspense
のfallback
で接続中に表示されるないようを変更することができます。以下のように変更してみましょう。
<ClientSideSuspense fallback={<div>...読み込み中</div>}>{children}</ClientSideSuspense>
任意のコンポーネントで指定できるので、ローディングでよくあるクルクルするやつなどを表示するのもいいかもしれないですね。
ユーザー名と色の変更
相手に表示される自分のユーザー名と色を変更できるようにしてみましょう。
以下でを設定しています。
user: {
name: "User",
color: "#ff0000",
},
レンダリング時に設定されてないといけないので、それも踏まえてEditor
コンポーネントを編集してみましょう。
"use client";
import { useEffect, useState } from "react";
import { BlockNoteEditor } from "@blocknote/core";
import { useCreateBlockNote } from "@blocknote/react";
import { BlockNoteView } from "@blocknote/mantine";
import * as Y from "yjs";
import { LiveblocksYjsProvider } from "@liveblocks/yjs";
import { useRoom } from "@liveblocks/react/suspense";
import "@blocknote/core/fonts/inter.css";
import "@blocknote/mantine/style.css";
type EditorProps = {
doc: Y.Doc;
provider: any;
name: string;
color: string;
};
export function Editor() {
const room = useRoom();
const [doc, setDoc] = useState<Y.Doc>();
const [provider, setProvider] = useState<any>();
const [name, setName] = useState("");
const [color, setColor] = useState("");
const [showEditor, setShwoEditor] = useState(false);
useEffect(() => {
const yDoc = new Y.Doc();
const yProvider = new LiveblocksYjsProvider(room, yDoc);
setDoc(yDoc);
setProvider(yProvider);
return () => {
yDoc?.destroy();
yProvider?.destroy();
};
}, [room]);
if (!doc || !provider) {
return null;
}
if (showEditor) {
return (
<BlockNote doc={doc} provider={provider} name={name} color={color} />
);
} else {
return (
<div style={{ display: "flex", justifyItems: "center" }}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="名前"
/>
<input
value={color}
onChange={(e) => setColor(e.target.value)}
type="color"
/>
<button onClick={() => setShwoEditor((prev) => !prev)}>接続</button>
</div>
);
}
}
function BlockNote({ doc, provider, name, color }: EditorProps) {
const editor: BlockNoteEditor = useCreateBlockNote({
collaboration: {
provider,
fragment: doc.getXmlFragment("document-store"),
user: {
name: name,
color: color,
},
},
});
return <BlockNoteView editor={editor} />;
}
名前と色を入力したらテキストを変種できるようにしたという感じですね。
というわけで実装できました。
おわり
今回は共同編集の実装を紹介しました。 Liveblocksはテキストの編集以外にも相手のカーソル表示やリアルタイムの通知機能など様々な機能が用意されています。 yjsについてもMap型やArray型などあり、使い方によって様々に活用できると思うので、ぜひいろいろ試していただければと思います。
今回テキストエディタはBlockNoteを使用しましたが、他のエディタでもほとんど同様に実装できるようになっています。この辺りの話はLiveblocksのドキュメントを参照してみてください。
ではまた。