Tauriに入門する
やりたいことを列挙してみる。単にRustの話もある、、
-
Commandを使ったWebViewプロセスとの連携
- ファイルのW/R
- SQLiteのCURD
- HTTP APIの実行
- アプリ起動時に、SQLiteのDB初期化
-
Tauri上でローカルの画像表示
inProgress
「アプリ起動時に、SQLiteのDB初期化」は、Next.jsの初期プロセスにCommandを実行する形かなぁ
サンドボックス用のリポジトリ
個人開発でデスクトップアプリが都合の良いケースができたので、折角なのでTauriを試そうと思う。
Rust経験は、2年前くらいにレーサー本をサラッと流した程度、、
まずは公式チュートリアル
Rustは導入済み、ただ古いかもしれないのでアップデートする
$ rustup update
$ rustc --version
rustc 1.64.0 (a55dd71d5 2022-09-19)
frontendはNext.jsを使うことにする
yarn create next-app --typescript
指定の通り、package.json
とnext.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": {
プロジェクトに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.
rsファイル開いた瞬間ゴリゴリcocが動き出した、、rust用のプラグインを入れておく
:CocInstall coc-rust-analyzer
起動した。tauriのアイコンかっこいいね。
yarn tauri dev
次はCommandを試したい。Commandは、WebView Process から Core Process の Rust コードを呼び出す仕組み
を参考に進めていく
順番通りやってないので、apiモジュールが足りないことに気づいたため、インストール
yarn add @tauri-apps/api
こんな感じで書き換える
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
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
実行結果
レンダラプロセスとサーバープロセスでここまで簡単に連携できるのはとても良い感じだ、、
先ほど同様bookを参考に、TaruiのAPIを試そう
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関数でレンダラ側でパスが取得できたわけだが、ここから内容を見たい場合どうするか調べよう。
examplesすごい充実してそう、、
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.
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"
動いた🎉
$ 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した
APIサンプルUIかっこいいなSvelteかーと思ったけど、中身はtailwindだね。とても参考になる。
昨日まで動いていたソースがいきなり動作しなくなった。内容は、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を呼び出せというもの。
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)
}, []);
以下の変更で動作するようになった。
一応動いた変更箇所
-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");
};
Next.jsの場合、サーバー側のコードではTauriは呼び出せないっぽい。なのでクライアント側かどうかを判断するために、windowオブジェクトを使う。
tailwindを導入する
$ yarn add -D tailwindcss postcss autoprefixer
$ npx tailwindcss init -p
/** @type {import('tailwindcss').Config} */
module.exports = {
+ content: [
+ "./pages/**/*.{js,ts,jsx,tsx}",
+ "./components/**/*.{js,ts,jsx,tsx}",
+ ],
theme: {
extend: {},
},
plugins: [],
}
書き換える
@tailwind base;
@tailwind components;
@tailwind utilities;
+ <h1 className="text-3xl text-red-300 font-bold underline">
+ Hello world!
+ </h1>
適用できた。ボタンとかはtailwindのリセットでデザインが変わった模様。
ここまでのコードはここに置いておく。
Tauri上でローカルの画像表示をやってみる。
モチベーションは、画像があるページ自体に認証をかけるのは簡単だが、Webアプリで画像自体に認証をかけると実装コストとそもそも認証の仕組みを自前で作らないといけない。
自分しか使わないWebアプリならそれでも良いが、他の人も使う場合認証基盤が必要になる。個人開発だととても面倒。デスクトップアプリの場合は、ローカルのストレージを使えば良い。
余談だが、画像自体に認証を設定する方法は以下が考えられる。
-
imgタグを利用する
-
<img src="https://fqdn?token=ey..
のようにURLにtoken(jwtの場合は最低限の権限にscopeを絞る)を持たせる- この場合、tokenの有効期限が切れたらページ全体を更新する必要があるので筋が悪そう
- cookie認証
-
-
fetch APIで取得、取得したバイナリを
createObjectURL
でURLにして、imgタグで表示
GitHubやGoogle Photoのように推測し辛いURLを発行して、分からないようにすることは可能だが公開自体はされているため、公開自体をしたくない場合に使えない。
デスクトップアプリであれば、認証したストレージサーバーからローカルにダウンロードし、それをレンダラプロセスからサーバープロセスに取りにいくことで可能そうと考えた。
調べてみたら、以下の記事が参考になりそう。
公式的には、ここら辺
@tauri-apps/api/path
使おうとして、ハマる
エラーは前と同様ReferenceError: navigator is not defined
で、yarn build
しても再現する。
nextでビルド資材を作る段階で落ちている。@tauri-apps/api/path
を動的にimportすることにした。
結果的に動作するコード
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
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,
});
})();
}, []);
@@ -12,7 +12,10 @@
},
"tauri": {
"allowlist": {
- "all": true
+ "all": true,
+ "fs": {
+ "scope": ["$APP/*"]
+ }
},
"bundle": {
"active": true,