☀️

M5Stackで蓄積したデータベースをグラフ表示するWebアプリを作る。1(準備編)

2024/10/26に公開

https://zenn.dev/akihiro_ya/articles/36e8ef662c6dba
環境データをSDカードにSqlite3データーベースファイルで保存できたので、続いてグラフ表示するWebアプリをReactを使って作る。

TypeScript + SQLite3 WASMならたくさん記事があったので、今回はPureScript言語を採用してVite + React + PureScript + SQLite3 WASMの構成にする。

Vite+Reactプロジェクトを作成する。

~$ npm create vite@latest

> npx
> create-vite

✔ Project name: … vite-react-purs
✔ Select a framework: › React
✔ Select a variant: › JavaScript

Scaffolding project in /home/-----/vite-react-purs...

Done. Now run:

  cd vite-react-purs
  npm install
  npm run dev

~$ cd vite-react-purs
~/vite-react-purs$ npm install

~/vite-react-purs$ npm run dev

  VITE v5.4.10  ready in 145 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

ブラウザでhttp://localhost:5173/を開いてページが正しく表示されているか確認後にCtrl+Cを押下してコンソールに戻る。

PureScriptのインストール

このページを参考にPureScriptをインストールする。
https://zenn.dev/eagle/articles/purescript-react-introduction

自分はもうすでにPureScriptがインストールされているので、特に何もしない。

PureScript+Reactのセットアップ

~/vite-react-purs$ spago init
[info] Initializing a sample project or migrating an existing one..
[info] Updating package-set tag to "psc-0.15.15-20241016"
Fetching the new one and generating hashes.. (this might take some time)
[info] Generating new hashes for the package set file so it will be cached.. (this might take some time)
[info] Found existing directory "src", skipping copy of sample sources
[info] Found existing file ".gitignore", not overwriting it
[info] Set up a local Spago project.
[info] Try running `spago build`

~/vite-react-purs$ spago build
~/vite-react-purs$ spago test
purs compile: No files found using pattern: src/**/*.purs
[info] Build succeeded.
🍝
You should add some tests.
[info] Tests succeeded.

テストが成功したことを確認する。

React+JavaScriptからReact+PureScriptに変更

purescript-react-basic-hooksのExampleを参考にする。
https://pursuit.purescript.org/packages/purescript-react-basic-hooks/8.2.0

  • index.htmlを編集する。
index.html
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Vite + React</title>
</head>

<body>
  <div id="root"></div>
  <script type="module" src="/src/index.js"></script>
</body>

</html>
  • src/index.jsを新規作成する。
src/index.js
import { main } from "../output/Main";
main();
  • src/Main.pursを新規作成する。
src/Main.purs
module Main where

import Prelude
import App.Pages.Home (mkHome)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Exception (throw)
import React.Basic.DOM.Client (createRoot, renderRoot)
import Web.DOM.NonElementParentNode (getElementById)
import Web.HTML (window)
import Web.HTML.HTMLDocument (toNonElementParentNode)
import Web.HTML.Window (document)

main :: Effect Unit
main = do
  maybeRoot <- getElementById "root" =<< (map toNonElementParentNode $ document =<< window)
  case maybeRoot of
    Nothing -> throw "Root element not found."
    Just r -> do
      home <- mkHome
      root <- createRoot r
      renderRoot root (home unit)
  • src/App/Pagesディレクトリを作成して、src/App/Pages/Home.pursを新規作成する。
src/App/Pages/Home.purs
module App.Pages.Home where

import Prelude
import React.Basic.DOM as DOM
import React.Basic.DOM.Events (capture_)
import React.Basic.Hooks (Component, component, useState, (/\))
import React.Basic.Hooks as React

type HomeProps
  = Unit

mkHome :: Component HomeProps
mkHome = do
  component "Home" \_props -> React.do
    counter /\ setCounter <- useState 0
    pure
      $ DOM.div
          { children:
              [ DOM.h1_ [ DOM.text "Home" ]
              , DOM.p_ [ DOM.text "Try clicking the button!" ]
              , DOM.button
                  { onClick:
                      capture_ do
                        setCounter (_ + 1)
                  , children:
                      [ DOM.text "Clicks: "
                      , DOM.text (show counter)
                      ]
                  }
              ]
          }
  • src/main.jsxは削除。
  • src/index.cssは削除。
  • src/App.cssは削除。
  • src/App.jsxは削除。

npm run devしてブラウザでPureScript+Reactが動作しているか確認する。

sqlite3-wasmのインストール

~$ npm install --save @sqlite.org/sqlite-wasm

vite.config.jsを編集する

Origin-Private-File-Systemを使うために

      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',

をvite.config.jsに追加する。

vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },
  },
  optimizeDeps: {
    exclude: ['@sqlite.org/sqlite-wasm'],
  },
});

SQLite3 WASM Worker1 APIをPureScript FFIでラップする

https://sqlite.org/wasm/doc/trunk/api-index.md

おすすめのようなのでSQLite3 WASMのWorker API #1を使うことにして、config-getメソッドを呼ぶところまでをやってみる。

  • src/Sqlite3Wasmディレクトリを作成して、src/Sqlite3Wasm/Sqlite3Wasm.jsを新規作成する。
    最小限config-getメソッドを呼ぶためにcreateWorker1PromiserImplconfigGetImpl関数を定義する。
src/Sqlite3Wasm/Sqlite3Wasm.js
import {
    sqlite3Worker1Promiser
} from '@sqlite.org/sqlite-wasm';

/* SQLite3 WASM Worker1 Promiser API */
export const createWorker1PromiserImpl = () => { return sqlite3Worker1Promiser.v2({}); };

export const configGetImpl = (promiser) => { return promiser('config-get', {}); };
  • src/Sqlite3Wasm/Sqlite3Wasm.pursを新規作成する。
    深く考えずにAPIのドキュメントを見ながらPurescript FFIでjavascriptに型を定義した(エラー処理はしていない)。
src/Sqlite3Wasm/Sqlite3Wasm.purs
module Sqlite3Wasm.Sqlite3Wasm
  ( ConfigGetResponse
  , SQLite3Version
  , Sqlite3Worker1Promiser
  , configGet
  , createWorker1Promiser
  ) where

import Prelude
import Control.Promise (Promise, toAffE)
import Effect (Effect)
import Effect.Aff (Aff)
import Effect.Uncurried (EffectFn1, runEffectFn1)

-- SQLite Version numbers
type SQLite3Version
  = { libVersion :: String
    , libVersionNumber :: Number
    , sourceId :: String
    , downloadVersion :: Number
    }

{-
  https://sqlite.org/wasm/doc/trunk/api-worker1.md
  SQLite3 WASM Worker1 API
-}
foreign import data Sqlite3Worker1Promiser :: Type

foreign import createWorker1PromiserImpl :: Effect (Promise Sqlite3Worker1Promiser)

-- sqlite3Worker1Promiserを得る
createWorker1Promiser :: Aff Sqlite3Worker1Promiser
createWorker1Promiser = createWorker1PromiserImpl # toAffE

-- config-getメソッドの応答
type ConfigGetResponse a
  = { type :: String -- "config-get"である
    , messageId :: a -- 何らかの値が返却されてくる
    , result ::  -- 処理結果
        { version :: SQLite3Version
        , bigIntEnabled :: Boolean
        , vfsList :: Array String
        }
    }

foreign import configGetImpl :: forall a. EffectFn1 Sqlite3Worker1Promiser (Promise (ConfigGetResponse a))

-- config-getメソッド呼び出し
configGet :: forall a. Sqlite3Worker1Promiser -> Aff (ConfigGetResponse a)
configGet promiser = runEffectFn1 configGetImpl promiser # toAffE

SQLite3 WASM のバージョンをコンソールに出してみる

src/Main.pursを編集して、main関数からいま作ったSqlite3Wasm.Sqlite3Wasmモジュールを使ってSQLite3 WASMのconfig-getメソッドを呼んでみる。

src/Main.purs
module Main where

import Prelude
import App.Pages.Home (mkHome)
import Data.Maybe (Maybe(..))
import Data.Either (either)
import Effect (Effect)
import Effect.Aff (Aff)
import Effect.Aff as Aff
import Effect.Class.Console (log, logShow)
import Effect.Exception (throw)
import React.Basic.DOM.Client (createRoot, renderRoot)
import Sqlite3Wasm.Sqlite3Wasm (ConfigGetResponse)
import Sqlite3Wasm.Sqlite3Wasm as Sq3
import Web.DOM.NonElementParentNode (getElementById)
import Web.HTML (window)
import Web.HTML.HTMLDocument (toNonElementParentNode)
import Web.HTML.Window (document)

sqlite3ConfigGet :: forall a. Aff (ConfigGetResponse a)
sqlite3ConfigGet = Sq3.createWorker1Promiser >>= Sq3.configGet

main :: Effect Unit
main = do
  -- SQLite3 WASM Worker1 API config-getメソッドを呼び出す
  Aff.runAff_ (either (log <<< Aff.message) (\r -> logShow r.result)) sqlite3ConfigGet
  -- React
  maybeRoot <- getElementById "root" =<< (map toNonElementParentNode $ document =<< window)
  case maybeRoot of
    Nothing -> throw "Root element not found."
    Just r -> do
      home <- mkHome
      root <- createRoot r
      renderRoot root (home unit)

途中適当にspago buildして不足するモジュールをspago installする(コンパイルエラーに出てくるはず)。

実行結果

client:495 [vite] connecting...
client:614 [vite] connected.
react-dom.development.js:29895 Download the React DevTools for a better development experience: https://reactjs.org/link/react-devtools
foreign.js:3 { bigIntEnabled: true, version: { downloadVersion: 3470000.0, libVersion: "3.47.0", libVersionNumber: 3047000.0, sourceId: "2024-10-21 16:30:22 03a9703e27c44437c39363d0baf82db4ebc94538a0f28411c85dda156f82636e" }, vfsList: ["unix-none","opfs","memdb","unix-excl","unix-dotfile","unix"] }

PureScript FFIを通じて SQLite3 WASMのバージョンが取得できた。

今回はここまで。

Discussion