🦆

Duckdb-Wasm+OPFS+Reactでダッシュボードを作ってLTしてみた話

2024/11/03に公開

勉強会でのDuckDB-Wasmに関するLT発表

参加させていただいた勉強会

https://classmethod.connpass.com/event/331322/

LTで使用したスライド

https://speakerdeck.com/nkforwork/duckdb-wasmderokarudatusiyubodowozuo-tutemita

概要

今回初めてLTさせていただきました。
内容はスライドに書いているのであまり踏み込めなかった部分と今回のLTについて書こうと思います。

DuckDB-Wasmとは

最近、X(Twitter)で頻繁にDuckDB-Wasmについての情報が流れてくるため、「なぜこんなに人気があるのか?」と興味が湧き、調べてみました。スライドにも記載しましたが、DuckDB-Wasmはほぼすべてをフロントエンドに含めて動作させられる点が魅力だと感じました。

さらに、通常デスクトップアプリケーションを作成する際は、使用するプログラミング言語ごとに異なるGUIライブラリを学ぶ必要がありますが、この方法ならReactで完結できるため、開発がシンプルになります。(もちろんReactではなく他のライブラリでも可能。htmlでも可)詳しい技術内容については、別の記事も参考にしていただければと思います。

Duckdb-wasm部分

worker作成

//別の部分でfilereaderでcsvを読み込んでいる。
//結果を文字列として取得している。

const csvData = e.target?.result as string;

      // OPFSにファイルを保存
      //読み込んだ際にsaveToOPFSを読んでOPFSへとデータを保存している
      //読み込む際にOPFSを読んでOPFSからデータをロードする方法の方が良い?

      await saveToOPFS("uploaded_data.csv", csvData);

      //DuckDBのファイルがセットになったバンドル一覧を取得
      //JSDelivrは、ライブラリを配信するためのCDN
      //初回ロードはオンライン必須か要検証
      //selectBundle:最適なバンドルを選ぶ(ブラウザの環境に合わせて自動的に)
      const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
      const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);

      //new Blob([...], { type: "text/javascript" }) は、
      //JavaScriptコードを含む新しいデータのかたまり(Blob)を作成
      //importScripts("${bundle.mainWorker!}"); で、
      //bundle.mainWorker URLからDuckDBのスクリプトを読み込む
      //bundle.mainWorker はDuckDBバンドルへのURL
      //このURLを使ってDuckDBのスクリプトをWeb Workerに読み込ませる。
      //一時的なURLを作成する。
      
      const worker_url = URL.createObjectURL(
        new Blob([`importScripts("${bundle.mainWorker!}");`], {
          type: "text/javascript",
        })
      );
      //worker:new Workerで別スレッドで動作する作業場を用意
      //logger:実行ログを表示できるようにしている。(デバッグ用)
      //db:duckdbを非同期で利用できるようにしている
      //bundle.mainModule と bundle.pthreadWorker 2つのファイルを指定して、 
      //DuckDBの本体とスレッド管理用のコードを読みこみ初期化
      //作成した一時的なURLを解放する
      const worker = new Worker(worker_url);
      const logger = new duckdb.ConsoleLogger();
      const db = new duckdb.AsyncDuckDB(logger, worker);
      await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
      URL.revokeObjectURL(worker_url);

OPFS保存

  const saveToOPFS = async (fileName: string, content: string) => {
    try {
      // OPFSのルートディレクトリを取得
      // ただしGoogleで拡張機能であるOPFSを有効にしている場合
      // 他ブラウザは不明
      // File System Access API の一部らしいので対応していない場合あり
      const rootDir = await navigator.storage.getDirectory();

      // 新しいファイルを作成
      //固定の指定ファイル名なので保存ごとにファイル名を変更する場合は処理が必要
      const fileHandle = await rootDir.getFileHandle(fileName, {
        create: true,
      });
      // 書き込みストリームを開く
      
      const writable = await fileHandle.createWritable();
      // コンテンツを書き込む
      await writable.write(content);
      // ストリームを閉じて保存を完了
      await writable.close();
      console.log("OPFSにファイルが保存されました:", fileName);
    } catch (error) {
      console.error("OPFSにファイルを保存中にエラーが発生しました:", error);
    }
  };

ダッシュボード部分

今回やりたかったことの一つ、Reactで比較的アニメーションのついたダッシュボード
framer-motion と MUIのdatagridなどを使用しました。
MUIのチャート系のライブラリは素の状態でアニメーションがついている。華美なものが必要ない場合は他のライブラリで十分だと思う。

Datagridコンポーネント

いわゆる表の部分

import React from "react";
import { DataGrid, GridColDef } from "@mui/x-data-grid";

interface DataGridDisplayProps {
  rows: any[];
  columns: GridColDef[];
}
// MUIのdatagridは単純なソート機能付きの表を作成してくれるライブラリ
// DataGridだと行列を別々に渡す必要があるがEvidenceなら必要はないかも。
// getRowIdで日付があれば一意なIDを作成。なければIDをランダムで作成しているがUUIDの方が
// 良い気がする。
// オブションでページ数やチェックボックスを追加,昇順・降順でソート可能にしている。
const DataGridDisplay: React.FC<DataGridDisplayProps> = ({ rows, columns }) => {
  return (
    <div style={{ height: 400, width: "100%" }}>
      <DataGrid
        rows={rows}
        columns={columns}
        getRowId={(row) => {
          // インデックスを使用して一意のIDを生成
          return row["DATE"] || Math.random().toString(36).substr(2, 9);
        }}
        pageSize={5}
        rowsPerPageOptions={[5, 10, 20]}
        checkboxSelection
        sortingOrder={["desc", "asc"]} // 昇順・降順の指定
      />
    </div>
  );
};

export default DataGridDisplay;

framermotionによるアニメーション

//上からフェードインしてくるアニメーション
//motion.div内:カーソルホバー時に少し大きくなりクリック時に少し小さくなる。
<Box
        display="flex"
        alignItems="flex-start"
        justifyContent="space-around"
        gap={4}
        marginBottom={4}
        component={motion.div}
        initial={{ opacity: 0, y: -20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.5 }}
      >
        <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
          <CsvUpload onDataLoaded={handleDataLoaded} />
        </motion.div>

LTして良かったこと

今回の勉強会は作業効率化の向上をテーマにした発表でしたが、他の参加者の発表では、デバイスや自作コードの紹介など、さまざまなテーマがあって面白かったです。

反省点として、もう少し小規模な技術に集中した方がわかりやすかったかもしれません。また、発表時間をオーバーしてしまった可能性があるので、次回はタイム測定をしたいです。

質疑応答では和やかに話ができ、DuckDBに普段から触れている方の使用例も聞けました。意外に広がりがある技術だと感じ、次回はそういった人にも満足いただける内容にしたいと思います。
また自分が思いもしない需要(AWS関連)について知ることもでき、次の目標も決めることができました。

Discussion