Open25

Tauriに入門する

ピン留めされたアイテム
shuntakashuntaka

やりたいことを列挙してみる。単にRustの話もある、、

  • Commandを使ったWebViewプロセスとの連携
    • ファイルのW/R
    • SQLiteのCURD
    • HTTP APIの実行
  • アプリ起動時に、SQLiteのDB初期化
  • Tauri上でローカルの画像表示 inProgress

「アプリ起動時に、SQLiteのDB初期化」は、Next.jsの初期プロセスにCommandを実行する形かなぁ

サンドボックス用のリポジトリ
https://github.com/shuntaka9576/tauri-sandbox/tree/main

shuntakashuntaka

個人開発でデスクトップアプリが都合の良いケースができたので、折角なのでTauriを試そうと思う。
Rust経験は、2年前くらいにレーサー本をサラッと流した程度、、

shuntakashuntaka

Rustは導入済み、ただ古いかもしれないのでアップデートする

$ rustup update
$ rustc --version
rustc 1.64.0 (a55dd71d5 2022-09-19)
shuntakashuntaka

https://tauri.app/v1/guides/getting-started/setup/

frontendはNext.jsを使うことにする

yarn create next-app --typescript

指定の通り、package.jsonnext.config.jsの設定を変更する。差分は以下の通り。

diff --git a/next.config.js b/next.config.js
index ae88795..ef62ca3 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,7 +1,14 @@
 /** @type {import('next').NextConfig} */
+
 const nextConfig = {
   reactStrictMode: true,
   swcMinify: true,
+  // Note: This experimental feature is required to use NextJS Image in SSG mode.
+  // See https://nextjs.org/docs/messages/export-image-api for different workaroun
+  images: {
+    unoptimized: true,
+  },
 }

 module.exports = nextConfig
+
diff --git a/package.json b/package.json
index f7a791d..393225d 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,9 @@
   "scripts": {
     "dev": "next dev",
     "build": "next build",
+    "export": "next export",
     "start": "next start",
+    "tauri": "tauri",
     "lint": "next lint"
   },
   "dependencies": {
shuntakashuntaka

プロジェクトにtauriのCLIを追加

yarn add -D @tauri-apps/cli

下二つ以外はデフォルト

$ yarn tauri init
yarn run v1.22.19
$ tauri init
✔ What is your app name? · my-app
✔ What should the window title be? · my-app
? Where are your web assets (HTML/CSS/JS) located, relative to the "<current dir>/s
✔ Where are your web assets (HTML/CSS/JS) located, relative to the "<current dir>/src-tauri/tauri.conf.json" file that will be created? · ../out
✔ What is the url of your dev server? · http://localhost:3000
✔ What is your frontend dev command? · yarn dev
✔ What is your frontend build command? · yarn build && yarn export
✨  Done in 70.41s.
shuntakashuntaka

rsファイル開いた瞬間ゴリゴリcocが動き出した、、rust用のプラグインを入れておく

:CocInstall coc-rust-analyzer
shuntakashuntaka

起動した。tauriのアイコンかっこいいね。

yarn tauri dev

shuntakashuntaka

順番通りやってないので、apiモジュールが足りないことに気づいたため、インストール

yarn add @tauri-apps/api
shuntakashuntaka

こんな感じで書き換える

src-tauri/src/main.rs
src-tauri/src/main.rs
import { invoke } from "@tauri-apps/api";
import type { NextPage } from "next";
import Head from "next/head";
import styles from "../styles/Home.module.css";

const Home: NextPage = () => {
  const executeCommands = () => {
    invoke("simple_commnad");
  };

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main>
        <div>exec rust code</div>
        <button onClick={executeCommands}>Clickt execute command</button>
      </main>
    </div>
  );
};

export default Home;

pages/index.tsx
pages/index.tsx
import { invoke } from "@tauri-apps/api";
import type { NextPage } from "next";
import Head from "next/head";
import styles from "../styles/Home.module.css";

const Home: NextPage = () => {
  const executeCommands = () => {
    invoke("simple_commnad");
  };

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main>
        <div>exec rust code</div>
        <button onClick={executeCommands}>Clickt execute command</button>
      </main>
    </div>
  );
};

export default Home;

UI

実行結果

shuntakashuntaka

レンダラプロセスとサーバープロセスでここまで簡単に連携できるのはとても良い感じだ、、

shuntakashuntaka
/pages/index.tsx
 import { invoke } from "@tauri-apps/api";
+import { open } from "@tauri-apps/api/dialog";
 import type { NextPage } from "next";
 import Head from "next/head";
 import styles from "../styles/Home.module.css";

 const Home: NextPage = () => {
+  const openDialog = () => {
+    open().then((files) => console.log(files));
+  };
+
   const executeCommands = () => {
     invoke("simple_commnad");
   };
@@ -18,6 +23,8 @@ const Home: NextPage = () => {
       <main>
         <div>exec rust code</div>
         <button onClick={executeCommands}>Clickt execute command</button>
+        <div>open dialog</div>
+        <button onClick={openDialog}>openDialog</button>
       </main>
     </div>
   );

こんな感じでDialogが開いて、パスがコンソールに出力される。OS側のAPIラッパー関数といったところかな。Desktopアプリだと必ず使うやつ。


open関数でレンダラ側でパスが取得できたわけだが、ここから内容を見たい場合どうするか調べよう。

shuntakashuntaka

examples/apiを試す。
M1のせいか、README通りだと動作しない模様、、一応動作したのでリビジョンを載せておきます。

$ git rev-parse --short HEAD
4036e15f

apiサンプルの起動コマンド

$ pwd
/Users/shuntaka/repos/github.com/tauri-apps/tauri
$ bash .scripts/setup.sh
Tauri Rust CLI installed. Run it with '$ cargo tauri [COMMAND]'.
Do you want to install the Node.js CLI?
1) Yes # 1を選択
# M1のため追加
$ yarn add @tauri-apps/cli-darwin-arm64
$ cd examples/api
$ yarn
# うまく行かない、、
$ yarn tauri dev
yarn run v1.22.19
warning package.json: No license field
$ node ../../tooling/cli/node/tauri.js dev
     Running BeforeDevCommand (`yarn dev`)
warning package.json: No license field
$ vite --clearScreen false --port 5173

  vite v2.9.13 dev server running at:

  > Local: http://localhost:5173/
  > Network: use `--host` to expose

  ready in 433ms.

        Info Watching /Users/shuntaka/repos/github.com/tauri-apps/tauri/examples/api/src-tauri for changes...
    Updating git repository `https://github.com/tauri-apps/wry`
    Updating git repository `https://github.com/tauri-apps/tao`
    Updating crates.io index
error: failed to select a version for `uuid`.
    ... required by package `tao v0.14.0 (https://github.com/tauri-apps/tao?branch=dev#7c7ce8ab)`
    ... which satisfies git dependency `tao` of package `wry v0.21.1 (https://github.com/tauri-apps/wry?branch=dev#17d324b7)`
    ... which satisfies git dependency `wry` of package `tauri-runtime-wry v0.11.1 (/Users/shuntaka/repos/github.com/tauri-apps/tauri/core/tauri-runtime-wry)`
    ... which satisfies path dependency `tauri-runtime-wry` (locked to 0.11.1) of package `api v0.1.0 (/Users/shuntaka/repos/github.com/tauri-apps/tauri/examples/api/src-tauri)`
versions that meet the requirements `^1.2` are: 1.2.1

all possible versions conflict with previously selected packages.

  previously selected package `uuid v1.1.2`
    ... which satisfies dependency `uuid = "^1"` (locked to 1.1.2) of package `cfb v0.7.3`
    ... which satisfies dependency `cfb = "^0.7.0"` (locked to 0.7.3) of package `infer v0.9.0`
    ... which satisfies dependency `infer = "^0.9"` (locked to 0.9.0) of package `tauri v1.1.1 (/Users/shuntaka/repos/github.com/tauri-apps/tauri/core/tauri)`
    ... which satisfies path dependency `tauri` (locked to 1.1.1) of package `api v0.1.0 (/Users/shuntaka/repos/github.com/tauri-apps/tauri/examples/api/src-tauri)`

failed to select a version for `uuid` which could resolve this conflict
error Command failed with exit code 101.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

shuntakashuntaka

examples/api/src-tauri/Cargo.toml の依存関係を修正する

$ pwd
/Users/shuntaka/repos/github.com/tauri-apps/tauri/examples/api
$ git diff src-tauri/Cargo.toml
diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.to
index 168704e6..2e0e59a1 100644
--- a/examples/api/src-tauri/Cargo.toml
+++ b/examples/api/src-tauri/Cargo.toml
@@ -13,6 +13,7 @@ tauri-build = { path = "../../../core/tauri-build", features =
 serde_json = "1.0"
 serde = { version = "1.0", features = [ "derive" ] }
 tiny_http = "0.11"
+uuid = "1.2.1"

 [dependencies.tauri]
 path = "../../../core/tauri"
shuntakashuntaka

動いた🎉

$ yarn tauri dev

最後に差分をまとめておきます。↑手順通りやれば大丈夫です。それ以外の差分はlockファイルです。

diffまとめ
$ git diff
diff --git a/examples/api/src-tauri/Cargo.lock b/examples/api/src-tauri/Cargo.lo
index 52d19fff..6bd6e797 100644
--- a/examples/api/src-tauri/Cargo.lock
+++ b/examples/api/src-tauri/Cargo.lock
@@ -122,6 +122,7 @@ dependencies = [
  "tauri-build",
  "tauri-runtime-wry",
  "tiny_http",
+ "uuid 1.2.1",
  "window-shadows",
  "window-vibrancy",
 ]
@@ -339,7 +340,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be09582
 dependencies = [
  "byteorder",
  "fnv",
- "uuid 1.1.2",
+ "uuid 1.2.1",
 ]

 [[package]]
@@ -3035,8 +3036,7 @@ dependencies = [
 [[package]]
 name = "tao"
 version = "0.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43336f5d1793543ba96e2a1e75f3a5c7dcd592743be06a0ab3a190f4fcb4b934"
+source = "git+https://github.com/tauri-apps/tao?branch=dev#7c7ce8ab2d838a79ecdf
 dependencies = [
  "bitflags",
  "cairo-rs",
@@ -3074,7 +3074,7 @@ dependencies = [
  "scopeguard",
  "serde",
  "unicode-segmentation",
- "uuid 1.1.2",
+ "uuid 1.2.1",
  "windows 0.39.0",
  "windows-implement",
  "x11-dl",
@@ -3145,7 +3145,7 @@ dependencies = [
  "time",
  "tokio",
  "url",
- "uuid 1.1.2",
+ "uuid 1.2.1",
  "webkit2gtk",
  "webview2-com",
  "win7-notifications",
@@ -3189,7 +3189,7 @@ dependencies = [
  "tauri-utils",
  "thiserror",
  "time",
- "uuid 1.1.2",
+ "uuid 1.2.1",
  "walkdir",
 ]

@@ -3218,7 +3218,7 @@ dependencies = [
  "serde_json",
  "tauri-utils",
  "thiserror",
- "uuid 1.1.2",
+ "uuid 1.2.1",
  "webview2-com",
  "windows 0.39.0",
 ]
@@ -3234,7 +3234,7 @@ dependencies = [
  "raw-window-handle",
  "tauri-runtime",
  "tauri-utils",
- "uuid 1.1.2",
+ "uuid 1.2.1",
  "webkit2gtk",
  "webview2-com",
  "windows 0.39.0",
@@ -3609,9 +3609,9 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f666

 [[package]]
 name = "uuid"
-version = "1.1.2"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
+checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
 dependencies = [
  "getrandom 0.2.7",
 ]
@@ -4178,8 +4178,7 @@ dependencies = [
 [[package]]
 name = "wry"
 version = "0.21.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff5c1352b4266fdf92c63479d2f58ab4cd29dc4e78fbc1b62011ed1227926945"
+source = "git+https://github.com/tauri-apps/wry?branch=dev#17d324b70e4d580c43c9
 dependencies = [
  "base64",
  "block",
diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.to
index 168704e6..2e0e59a1 100644
--- a/examples/api/src-tauri/Cargo.toml
+++ b/examples/api/src-tauri/Cargo.toml
@@ -13,6 +13,7 @@ tauri-build = { path = "../../../core/tauri-build", features =
 serde_json = "1.0"
 serde = { version = "1.0", features = [ "derive" ] }
 tiny_http = "0.11"
+uuid = "1.2.1"

 [dependencies.tauri]
 path = "../../../core/tauri"
diff --git a/package.json b/package.json
index 9b24e628..19589c22 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
     "prettier": "^2.5.1"
   },
   "dependencies": {
+    "@tauri-apps/cli-darwin-arm64": "^1.1.1",
     "typescript": "^4.5.4"
   }
 }

念の為動く状態で、forkした

https://github.com/shuntaka9576/tauri/tree/81e18879e8703c58dc7fd04608efa5e2449adf6b

shuntakashuntaka

APIサンプルUIかっこいいなSvelteかーと思ったけど、中身はtailwindだね。とても参考になる。

shuntakashuntaka

昨日まで動いていたソースがいきなり動作しなくなった。内容は、navigator is not definedエラー。
おそらく、ホットリロードでソースを更新する場合には再現しない。
Next.js側の初期化処理で落ちているっぽかった。

$ yarn tauri dev
yarn run v1.22.19
$ tauri dev
     Running BeforeDevCommand (`yarn dev`)
$ next dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
event - compiled client and server successfully in 231 ms (173 modules)
        Info Watching /Users/shuntaka/repos/github.com/shuntaka9576/my-app/src-tauri for changes...
   Compiling app v0.1.0 (/Users/shuntaka/repos/github.com/shuntaka9576/my-app/src-tauri)
    Finished dev [unoptimized + debuginfo] target(s) in 4.91s
wait  - compiling / (client and server)...
event - compiled client and server successfully in 239 ms (205 modules)
error - ReferenceError: navigator is not defined
    at n (file:///Users/shuntaka/repos/github.com/shuntaka9576/my-app/node_modules/@tauri-apps/api/os-check-27fe6e2b.js:1:14)
    at file:///Users/shuntaka/repos/github.com/shuntaka9576/my-app/node_modules/@tauri-apps/api/path-5af4eed5.js:1:3723
    at ModuleJob.run (node:internal/modules/esm/module_job:185:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:281:24)
    at async importModuleDynamicallyWrapper (node:internal/vm/module:437:15) {
  page: '/'
}
wait  - compiling /_error (client and server)...
event - compiled client and server successfully in 28 ms (206 modules)
warn  - Fast Refresh had to perform a full reload. Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works
error - ReferenceError: navigator is not defined
    at n (file:///Users/shuntaka/repos/github.com/shuntaka9576/my-app/node_modules/@tauri-apps/api/os-check-27fe6e2b.js:1:14)
    at file:///Users/shuntaka/repos/github.com/shuntaka9576/my-app/node_modules/@tauri-apps/api/path-5af4eed5.js:1:3723
    at ModuleJob.run (node:internal/modules/esm/module_job:185:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:281:24)
    at async importModuleDynamicallyWrapper (node:internal/vm/module:437:15) {
  page: '/'
}

ドキュメントに該当のサンプルコードがあった。簡単に言えばクライアント側からtaruiのAPIを呼び出せというもの。

https://tauri.app/v1/guides/getting-started/setup/next-js/

import { invoke } from '@tauri-apps/api/tauri'

// 注意:開発でNext.jsを扱う場合、2つの実行コンテキストがあります。
// サーバーサイドでは、コンテキストから外れているため、タウリを呼び出すことができません。
// - サーバーサイドでは、コンテキストの外にあるため、タウリを呼び出すことができない // - クライアントサイドでは、タウリが実行できる。
// サーバ側かクライアント側かを知るには、以下のようにします。
const isClient = typeof window !== 'undefined'

// これでコマンドを呼び出すことができます!
// アプリケーションの背景を右クリックし、開発者ツールを開きます。
// コンソールに「Hello, World!
isClient &&
  invoke('greet', { name: 'World' }).then(console.log).catch(console.error)

or

import { invoke } from "@tauri-apps/api/tauri"

const Home: NextPage = () => {
  useEffect(() => {
    invoke('greet', { name: 'World' })
    .then(console.log)
    .catch(console.error)
  }, []);

以下の変更で動作するようになった。

一応動いた変更箇所
pages/index.tsx
-import { invoke } from "@tauri-apps/api";
+import { invoke } from "@tauri-apps/api/tauri";
 import { open } from "@tauri-apps/api/dialog";
 import type { NextPage } from "next";
...
 import Head from "next/head";
const Home: NextPage = () => {
   };
...
   const executeCommands = () => {
-    invoke("simple_commnad");
+    const isClient = typeof window !== "undefined";
+    isClient && invoke("simple_commnad");
   };
shuntakashuntaka

Next.jsの場合、サーバー側のコードではTauriは呼び出せないっぽい。なのでクライアント側かどうかを判断するために、windowオブジェクトを使う。

shuntakashuntaka

tailwindを導入する

https://tailwindcss.com/docs/guides/nextjs

$ yarn add -D tailwindcss postcss autoprefixer
$ npx tailwindcss init -p
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
+  content: [
+    "./pages/**/*.{js,ts,jsx,tsx}",
+    "./components/**/*.{js,ts,jsx,tsx}",
+  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

書き換える

styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
/pages/index.tsx
+        <h1 className="text-3xl text-red-300 font-bold underline">
+          Hello world!
+        </h1>

適用できた。ボタンとかはtailwindのリセットでデザインが変わった模様。

ここまでのコードはここに置いておく。
https://github.com/shuntaka9576/tauri-sandbox/tree/eebd57c5e15009758390a086b208294e0bf9660b

shuntakashuntaka

Tauri上でローカルの画像表示をやってみる。
モチベーションは、画像があるページ自体に認証をかけるのは簡単だが、Webアプリで画像自体に認証をかけると実装コストとそもそも認証の仕組みを自前で作らないといけない。
自分しか使わないWebアプリならそれでも良いが、他の人も使う場合認証基盤が必要になる。個人開発だととても面倒。デスクトップアプリの場合は、ローカルのストレージを使えば良い。

余談だが、画像自体に認証を設定する方法は以下が考えられる。

  • imgタグを利用する

    • <img src="https://fqdn?token=ey..のようにURLにtoken(jwtの場合は最低限の権限にscopeを絞る)を持たせる
      • この場合、tokenの有効期限が切れたらページ全体を更新する必要があるので筋が悪そう
    • cookie認証
      • 画像のURLがクロスドメインの場合(3rd Party cookie)
        • 画像を配信しているドメインのcookieを持っている場合は、リクエストCookieを付与(SameSite属性がついていたら不可) 参考1
      • 画像のURLが同一ドメインの場合
        • Same OriginだとCookie、クライアント証明書、認証ヘッダーの情報が送信される。crossorigin属性は不要 参考2
  • fetch APIで取得、取得したバイナリをcreateObjectURLでURLにして、imgタグで表示

GitHubやGoogle Photoのように推測し辛いURLを発行して、分からないようにすることは可能だが公開自体はされているため、公開自体をしたくない場合に使えない。

デスクトップアプリであれば、認証したストレージサーバーからローカルにダウンロードし、それをレンダラプロセスからサーバープロセスに取りにいくことで可能そうと考えた。

調べてみたら、以下の記事が参考になりそう。

https://zenn.dev/bpk_t/scraps/4f9523470ea151

shuntakashuntaka

公式的には、ここら辺
https://tauri.app/v1/api/js/tauri/#convertfilesrc

@tauri-apps/api/path使おうとして、ハマる
https://github.com/tauri-apps/tauri/issues/3554

エラーは前と同様ReferenceError: navigator is not definedで、yarn buildしても再現する。
nextでビルド資材を作る段階で落ちている。@tauri-apps/api/pathを動的にimportすることにした。

結果的に動作するコード
pages/index.tsx
import { invoke } from "@tauri-apps/api/tauri";
import { open } from "@tauri-apps/api/dialog";
import Head from "next/head";
import styles from "../styles/Home.module.css";
import { useEffect, useState } from "react";

const Home = () => {
  const [appDir, setAppDir] = useState<string>("");
  const openDialog = () => {
    open().then((files) => console.log(files));
  };

  const executeCommands = () => {
    invoke("simple_commnad");
  };

  useEffect(() => {
    (async () => {
      const { path } = await import("@tauri-apps/api");
      path.appDir().then((dir) => {
        console.log(dir);
        setAppDir(dir);
      });
    })();
  }, []);

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main>
        <h1 className="text-3xl text-black-300 font-bold">tauri-sandbox</h1>
        <div className="pb-2">
          <h2 className="font-bold">Command</h2>
          <div>invoke rust command</div>
          <button
            className="rounded-md border border-transparent bg-indigo-600 px-1 py-1 text-base font-medium text-white hover:bg-indigo-700"
            onClick={executeCommands}
          >
            invoke
          </button>
        </div>

        <div className="pb-2">
          <h2 className="font-bold">Tauri API</h2>
          <div>open dialog</div>
          <button
            className="rounded-md border border-transparent bg-indigo-600 px-1 py-1 text-base font-medium text-white hover:bg-indigo-700"
            onClick={openDialog}
          >
            openDialog
          </button>
          <div>get local image</div>
          <div>file</div>
          <p>appDir:{appDir}</p>
        </div>
      </main>
    </div>
  );
};

export default Home;

ここまでのリビジョン: ca41396

shuntakashuntaka

fsAPIを使い、/Users/shuntaka/Library/Application Support/com.tauri.dev/imagesを作成する場合は以下のコード。

  • com.tauri.devのschemaはデフォルトでは作成されていないので、注意。勿論この値はtauri.conf.jsonで書き換えが可能
  • allowlistが全てallowでもfsのscopeは適切に設定しないと、ディレクトリ作成は出来なかった
(中略)
  useEffect(() => {
    (async () => {
      await fs.createDir("images", {
        dir: fs.BaseDirectory.App,
        recursive: true,
      });
    })();
  }, []);
src-tauri/tauri.conf.json
@@ -12,7 +12,10 @@
   },
   "tauri": {
     "allowlist": {
-      "all": true
+      "all": true,
+      "fs": {
+        "scope": ["$APP/*"]
+      }
     },
     "bundle": {
       "active": true,

b5bd401