14
4

More than 1 year has passed since last update.

Fluid Frameworkを使って共同編集可能なお絵描きアプリに挑戦する

Last updated at Posted at 2021-11-30

最近Teamsで共有されたファイルを直接編集することが多く、その際の共同編集体験が良かったので調べてみました。
どうやらFluid Frameworkというマイクロソフト謹製のライブラリーが用いられているとのことです。こちらオープンソース化もされています。
公式サイトでは付箋アプリなどおもしろいサンプルも提供しています。

難しいこともいろいろと書いてあるので手を動かして体験してみようと思います。
テキストの共有はサンプルもあったので画像にしてみようかと共同編集可能なお絵描きアプリに挑戦してみます。

Fluid Framework

その前に簡単にFluid Frameworkとは。

What is Fluid Framework?
Fluid Framework is a collection of client libraries for distributing and synchronizing shared state. These libraries allow multiple clients to simultaneously create and operate on shared data structures using coding patterns similar to those used to work with local data

引用(https://fluidframework.com/docs/#what-is-fluid-framework)

一言でいうと各クライアントの状態を共有、同期するためのクライアントライブラリーです。
特徴的な点はクライアントの状態を共有してマージするのは各クライアントで行う点です。

想定されている流れは以下の通りです。

The following is a typical flow.

  • Client code changes data locally.
  • Fluid runtime sends that change to the Fluid service.
  • Fluid service sequences that operation and broadcasts it to all clients.
  • Fluid runtime incorporates that operation into local data and raises a “valueChanged” event.
  • Client code handles that event (updates view, runs business logic).

引用(https://fluidframework.com/docs/#how-fluid-works)

大きく分けてFluid ContainerとFluid Serviceのふたつの要素で構成されています。
Fluid Containerはローカルの編集状態をFluid Serviceに送り、Fluid Serviceはシーケンス化した操作を各クライアントへ配布します。
Fluid Frameworkはサーバーでは操作のシーケンスを配布するだけで、状態のマージは各クライアントが行います。その結果遅延時間を劇的に削減しているとのことです。

他にも興味深い内容が書かれていますので公式サイトをぜひご覧ください。

お絵描きアプリ開発

今回は下記を参考にReactで作成します。

環境

node -v
> v16.7.0

yarn -v
> 1.22.11

npx create-react-app --version
> 4.0.3

Reactの構築

npx create-react-app fluid-canvas --template typescript
cd fluid-canvas

typescriptのバージョンは4.1.2です。

Fluid Framework インストール

yarn add @fluidframework/tinylicious-client fluid-framework

バージョンは0.52.1です。

fluid-framework

Fluid Framework本体。クライアント間のデータを同期するためのライブラリーです。

@fluidframework/tinylicious-client

Tinyliciousは開発目的のローカルのインメモリーを用いたFluidサービスです。
Tinylicious ClientはTinyliciousへの接続やFluid Containerのスキーマを定義します。
本来はFluid Loaderや接続手順、データ取得の手続きなどを構築しないといけなさそうなんですがカプセル化してくれています。

Fluidサービスは他にもAzure Fluid Relay用や独自構築できるRouterliciousが提供されています。

react-signature-pad-wrapper インストール

yarn add react-signature-pad-wrapper

描画にはSignature PadをReact用にラッパーしたreact-signature-pad-wrapper(v2.0.2)を使用します。
Signature Padはスムーズな署名を描くためのライブラリーと謳っており、シンプルでアウトプットもPNG, JPEG, SVGに変換できて使い勝手が良いです。

お絵描き部分作成

App.tsx
import React from 'react';
- import logo from './logo.svg';
- import './App.css';
+ import SignaturePad from "react-signature-pad-wrapper"

function App() {
+  const signaturePadRef = React.useRef<SignaturePad>(null);

+  return (
+    <SignaturePad ref={signaturePadRef} />
+  );

-  return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.tsx</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-         Learn React
-       </a>
-     </header>
-   </div>
- );
}

export default App;

signaturePadRef は描画した絵を取得、更新する際に使用します。ついでにlogo.svgなどは削除しました。
起動して動作確認します。

yarn start

animation.gif

マウスドラッグで絵が描けるようになりました。
本家ではクリアや色変更などのデモもあります。もっとリッチにできますが今回はこのまま進めます。

Fluid Framework導入

App.tsx
import React from "react";
import SignaturePad from "react-signature-pad-wrapper"
+ import { TinyliciousClient } from "@fluidframework/tinylicious-client";
+ import { SharedMap } from "fluid-framework";

TinyliciousClient と SharedMap をimportします。SharedMap はFluid Frameworkが提供するDDS(distributed data structures)の一種で、Key-Valueデータを提供します。
DDSはローカルデータを扱うように操作して、各クライアント間で状態を共有することができます。

コンテナーの生成または取得

App.tsx
import React from "react";
import SignaturePad from "react-signature-pad-wrapper"
import { TinyliciousClient } from "@fluidframework/tinylicious-client";
import { SharedMap } from "fluid-framework";

+ const dataKey = "drawing";
+ const containerSchema = {
+   initialObjects: { view: SharedMap }
+ };

+ const client = new TinyliciousClient();
+ const getViewData = async (): Promise<SharedMap> => {
+   let container;
+   const containerId = window.location.hash.substring(1);
+   if (!containerId) {
+     ({ container } = await client.createContainer(containerSchema));
+     const id = await container.attach();
+     window.location.hash = id;
+   } else {
+     ({ container } = await client.getContainer(containerId, containerSchema));
+   }

+   return container.initialObjects.view as SharedMap;
+ }

function App() {
  ...
}

dataKey はShardMapに設定するKeyです。

containerSchema はFluid Containerの定義です。initialObjectsはコンテナーの作成時に作成され、コンテナーが有効な間存在します。各クライアントはinitialObjects を介してアクセスし、分散された状態を共有します。
他にDynamic objects(dynamicObjectTypes)があります。Dynamic objectsはアプリで扱うデータサイズが大きいため遅延ロードしたり、必要なデータがユーザーの操作に依存する場合にUXを守るために利用するようです。

getViewData はコンテナーからShardMapを取得します。コンテナーが未作成の場合は新しくコンテナーを作成し、コンテナーIDをURLハッシュに設定します。URLハッシュにコンテナーIDが設定されている場合はコンテナーを取得します。
サンプルではURLハッシュへのアクセスがlocation.hashになっていますが、eslintのno-restricted-globalsに引っかかるのでwindow.location.hashに変更しています。

Fluidデータの取得

App.tsx
function App() {
  const signaturePadRef = React.useRef<SignaturePad>(null);

+  const [fluidData, setFluidData] = React.useState<SharedMap>();
+  React.useEffect(() => {
+    getViewData().then(view => setFluidData(view));
+  }, []);

  ...
}

useEffectでgetViewDataをコンポーネント生成時に1度だけ呼び出します(第2引数に空配列を設定)。
fluidDataにcontainerSchema で定義したview(ShardMap)を設定しています。

Fluidデータの同期

App.tsx
function App() {
  ...

+  React.useEffect(() => {
+    if (!fluidData) {
+      return;
+    }
+
+    const syncView = () => {
+      if (signaturePadRef.current) {
+        signaturePadRef.current.fromDataURL(fluidData.get(dataKey) as string);
+      }
+    }

+    syncView();
+    fluidData.on("valueChanged", syncView);
+    return () => { fluidData.off("valueChanged", syncView) }
+  }, [fluidData]);

  ...
}

useEffectをもうひとつ追加してFluidデータとUIを同期します。UIの更新処理はsyncViewにてfluidDataの値を取得して、Signature PadのfromDataURLに設定します。
また、Fluidデータは別クライアントが変更する可能性があります。Fluidデータに変更があるとvalueChanged イベントが発生するので、syncViewでUIの更新処理を行います。

Fluidデータの更新

App.tsx
function App() {
  ...

+  const [imageData, setImageData] = React.useState<string>();
+  React.useEffect(() => {
+    if (imageData) {
+      fluidData?.set(dataKey, imageData);
+    }
+  }, [imageData, fluidData]);

+  const onEnd = React.useCallback(() => {
+    const signaturePad = signaturePadRef.current;
+    const dataUrl = signaturePad?.toDataURL("image/svg+xml");
+    setImageData(dataUrl);
+  }, [setImageData]);

  return (
-    <SignaturePad ref={signaturePadRef} />
+    <SignaturePad ref={signaturePadRef} options={{onEnd: onEnd}} />
  );
}

ローカル用にimageDataを用意します。imageDataに変更がある場合はfluidDataにも設定し、別クライアントへ変更を共有します。
onEnd はSignature Pad の描画終了時にコールされます1。Signature PadのtoDataURLの値をimageDataに設定します。

お絵描きアプリ実行

Fluidサービス(サーバー)としてtinylicious を起動します。

npx tinylicious@latest

アプリを実行します。

yarn start

animation.gif

2つのブラウザから同じURLにアクセスして描画が同期しています。

複数端末からアクセスしてみる

ただし、tinylicious はローカルでしかアクセスできないので別端末からもアクセスできるようにしてみます。公式にてngrok を使ってみよ、とあるのでやってみます。
アカウント登録や認証用設定が必要なので公式の手順を参照ください。

ngrok http実行後にForwardingに表示される転送用のドメインをTinyliciousClientProps のconnection のdomain に設定します。設定したTinyliciousClientProps をTinyliciousClient の引数にします。

App.tsx
+ const clientProps: TinyliciousClientProps = {
+   connection: { port: 443, domain: "https://forwarding-domain.ngrok.io" }
+ }

- const client = new TinyliciousClient();
+ const client = new TinyliciousClient(clientProps);

httpで設定する場合はportを80にしてください。

npx tinylicious@latest
ngrok http 7070
yarn start

tinyliciousのPortのデフォルトは7070です。

animation.gif

わかりづらいですが2つの端末から描画しています。

まとめ

クライアントロジックだけで共同編集機能を実現しているFluid Frameworkはとても強力です。
今はバージョン1ではなく、「まだ製品品質のソリューションを提供できる状態ではありません」とのことなので今後が楽しみです。

今回作成したソースはこちらです。


  1. 本記事を書いている最中にSignature Padが3年ぶりのメジャーアップデートをしたようでonEndがなくなっていました。react-signature-pad-wrapperの方もそのうち対応するかもしれません。 

14
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
4