はじめに
Tarui Advent Calendar 2024 の1日目です!
今年は、Tauri v1 と Svlete 4 を使って業務アプリを作ったり、プライベートで作って遊んだりしていました。
Tauri に触れたおかげで Rust も結構書くようになったし、Svelte とのコラボの良さに GUI 開発がより楽しくなったのを覚えました。
さて、今年は新生 Tauri として Tauri の Version 2 (v2) がリリースされました。
モバイル対応が入ったり、コアレベルで扱えるプラグインが書けるようになったり、セキュリティをより細かく設定できるようになったり、コンフィグを環境によって扱えるようになったり…。
そして、Svelte も Version 5 になりました。
色々変更は追ってましたが、なるほどそういう実装になったかという事もあり、正式リリースをワクワク待ち望んでました。
それぞれ、数々の対応がありますが、とりあえずまずは触ってみないと!
っということでこのポストをしたためた次第です。
試しにつくってみつつ、来年になったらがっつり開発に取り掛かれるように準備運動をしておきたいですね!!
ということで本題開始!
サンプルコード
プロジェクトの作成
まずはプロジェクトの作成。
pnpm create tauri-app
途中のプロンプトで、以下を選択。
- Frontend
- TypeScript / JavaScript (pnpm, yarn, npm, bun)
- UI Template
- Svelte
そして、インストールを行う。
cd "Project Directory"
pnpm install
ここで作成されるものは、このポストの執筆時点では、
- Svelte: 5.0.0
- SvelteKit: 2.7.0
となっている。
Svelte / SvelteKit を含むモジュールのアップデート
Svelte/SvelteKit は、既にバージョン5になってからいくつかのアップデートがかかっている。
開発に取り掛かってからバージョンを上げるのも億劫なので、この時点でアップデートをかけてしまおう。
pnpm update
基本プロジェクト構造の把握
Tauri v1 を触った後に、ここは特に違っていて意識しないといけないな~ってところを、基本構造を把握する中で書き留めておく。
v1 と共通なところ
トップレベルの階層構造は基本的に変わらない。
ここは理解しやすいところだが、あえてちゃんとまとめる。
ディレクトリ
ディレクトリは必ず大きなくくりとしての意味を持つので、まずはここをおさえておく。
-
src
- フロントエンド側のソースコードを溜める場所
-
src-tauri
- Tarui の実装部分 (Rust) を溜める場所
-
static
- Svelte での、画像などの静的リソースを配置する場所
Tauri
-
src-tauri/src
- Tauri のバックエンドコードはこちらに各
- Rust のソースコード配置場所と同様
-
src-tauri/target
- デバッグビルドやリリースビルドを行うとこちらに書き出される
- Rust のビルド時と同様
Svelte/SvelteKit
-
src/routes
- この階層以下にあるファイル群は、ルーティング対象となる
- 詳しくは SvelteKit を参照
ファイル
次に既定ファイル構造
-
package.json
- npm 系のエントリーポイントかつ、パッケージ管理のコンフィグ
- 今回、僕としても pnpm にツールを変更したが、ここは変わらず使用
-
svelte.config.js
- Svelte の基本挙動 (基底レンダリング方式など) を定めておくスクリプタブルコンフィグ
-
tsconfig.json
- TypeScript 用のコンフィグ (js の場合は無い)
-
vite.config.js
- Vite 用のスクリプタブルコンフィグ
- Svelte ではデフォルトで Vite を開発構成で使用するため
Tauri
-
src-tauri/Cargo.toml
- Rust 用のパッケージコンフィグ (package.json 的なやつ)
-
src-tauri/tauri.conf.json
- Tauri のコンフィグ
- 中身のスキーマは変更されたが、ファイル名は変わらず
-
src-tauri/src/main.rs
- Rust としての規定エントリーポイント
Tauri v2 で変わったところ
Tauri v2 では、次の様に追加のものが出てきている。
-
src-tauri/capabilities/default.json
- セキュリティ定義を行うファイル
- もともとは、
tauri.conf.json
のスキーマ上に定義されたもので、そちらをいじっていたが、Tauri v2 では主にこちらを編集する- 一応、
tauri.conf.json
に直接書くことも可能
- 一応、
Tarui のセキュリティに関してはまた別途こちらにまとめた。
-
src-tauri/src/lib.rs
- Rust としてのライブラリエントリーポイント
- v1 では、
main.rs
を基本とした構造だったが、v2 のデフォルトはライブラリ構造との併用 - ここの変化は v1 の基本構造とは違う特筆すべきところなので、以下に詳しく
src-tauri/src/libs.rs
そもそもの Rust としてのエントリーポイントである src-tauri/src/main.rs を見てみると次のようにある。
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
for_qiita_lib::run()
}
最初の2行は Windows のためなので無視する (これは v1 とは変わらない) として、特筆すべきは main 関数の中身。
今回、 for_qiita
というプロジェクトで作成したので、 for_qiita_lib::run()
とある。
しかしながら、この for_qiita_lib
どこにもそんなモジュールはファイルとして存在しない。
では、どこに定義があるかというと src-tauri/Cargo.toml
の中。
[lib]
name = "for_qiita_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
こちらの表記方法は、Rust においてのライブラリを作る際のものとなっていて、標準では、 cargo new --lib
としたときに作成されるものと同様。
src-tauri/src/lib.rs
の中身を見てみると、以下の様にある。
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
これは、v1 の基本パッケージ構造では main.rs
の方で最初から書いていたものとまったく同様のもの。
つまるところ、Tauri v2 におけるパッケージ構造としては、「実行エントリーポイント」 と 「ライブラリ側としての機能実装」 との層を少し厚くした様だ。
Rust の実行ファイル/ライブラリ構成の違いに慣れていないと、ここで混乱するかもしれない。
確かに、エントリーポイントとしての構造はある意味特殊なものであるため、そこに実装を混ぜると、結局アプリケーションコードがスケールした際に肝心のエントリーポイントの把握 (特に Tauri のウィンドウビルドをする前になんらかの前処理をしたい場合など) がしづらくなってくるというのはある。
こうして分けていくことに意義があるのは分かるので、これを基底としてファイルを作成していくように切り替えていきたい。
アプリケーションが肥大化していくと次のようにどんどんとファイル・フォルダが増えていく。
さらにこうした中で、main.rs
の中でどういう事が起こるかというと、以下のような状況になる。
(以下は特に注意深く読まなくて良いい。こんな感じになりがちという空気感を感じ取ってもらえればと。
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use chrono::Utc;
use cron::Schedule;
use serde::Serialize;
use std::{env, str::FromStr, thread::sleep, time::Duration};
use tauri::Manager;
use commands::cmds_db
#[derive(Clone, Serialize)]
struct CountEventPayload {
count: u32,
}
fn main() {
tauri::Builder::default()
.invoke_handlers(tauri::generate_handler![
cmds_db::migration,
cmds_db::create,
cmds_db::read,
cmds_db::update,
cmds_db::delte,
cmds_conf::write,
cdms_conf::read,
])
.setup(|app| {
// Click event ==========
let id = app.listen_global("click", |event| {
println!("got `click` event with payload {:?}", event.payload());
});
match env::var("TAURI_UNLISTEN") {
Ok(val) => {
let value: u8 = val.parse().unwrap();
if value == 1 {
app.unlisten(id);
}
}
_ => (),
}
// Count Event ==========
let app_handle = app.app_handle();
// そのままループを作るとメイン処理が固まるので、tauri::async_runtime
// で非同期ランタイムを作成
let _count_handle = tauri::async_runtime::spawn(async move {
// cron 式で3秒ごとのイベントをスケジュールする。
let schedule = Schedule::from_str("0/3 * * * * *").unwrap();
let mut count: u32 = 0;
let mut next_tick = schedule.upcoming(Utc).next().unwrap();
loop {
let now = Utc::now();
// 1 カウントする
if now >= next_tick {
next_tick = schedule.upcoming(Utc).next().unwrap();
count += 1;
// イベントを emit。
let result =
app_handle.emit_all("count_event", CountEventPayload { count: count });
match result {
Err(ref err) => println!("{:?}", err),
_ => (),
}
}
sleep(Duration::from_secs(std::cmp::min(
(next_tick - now).num_seconds() as u64,
60,
)));
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
こうしてだんだんと main.rs
部分が肥大化していきやすくなり、肝心のエントリーポイント部分で何が起きているかを追いづらくなっていく。
もちろん日ごろから分けて書いている文化の場合には問題になりにくいだろうが、デフォルトでここを切り分けていないと、「そういうもの」としての文化を享受しづらいというのはあるだろう。
「実装はちゃんとライブラリとして分けましょうね。ちゃんと中身も分離しましょうね。」 という Tauri からのアプリケーション開発の教えとして素直に受け入れたい。
Svelte 5 の SvelteKit で変わったところ
実はファイル構造に関しては大きな変更は特に無い。
(サーチ不足もあるかもだが、少なくとも Tuari 様にフロントエンドを作るうちでは困ることは少ないだろう。)
Svelte 5 の SvelteKit の変化は実際のコード部分くらいにしかない。
なので、Svlete 4 からのファイル構造的な移行はかなりスムーズに行くだろう。
ウィンドウに機能を追加してみて、Tauri v2 での開発フローを学んでみる
とにもかくにも構造を理解するには、やはり実際に実装してみるのがいい。
ということで、v1 時代に実装方法を確認していたもので、今回は以下のことを実現したい。
いずれも既に Qiita 記事にしている実装ではあるが、これらを v2 に対応したい。
(後日、対応記事に実装方法をアップデート予定。
フレームレスウィンドウ
ここでは以下のことが学べる。
-
tauri.conf.json
の設定の仕方- 装飾 (タイトルバー) の設定
- セキュリティの設定
- Svelte 5 でのコンポーネントの取り回し
まずはやっぱりフレームレスウィンドウ。
見た目リッチにできるしね!
バックエンド
Tauri v2 では、decorations
を切るだけで、影落ちをのこしたまま装飾を切ることができるようになった。
{
"app": {
"windows": [
{
"title": "for-qiita",
"width": 800,
"height": 600,
+ "decorations": false
}
],
"security": {
"csp": null
}
},
}
フロントエンド
依存ライブラリの追加とセットアップ。
Tailwind CSS のインストールとセットアップ
おまけのアイコン用ライブラリをインストール
pnpm add lucide-svelte
実装
+layout.svelte
での実装。
Svelte 5 では、<slot />
ではなく、$props()
からの chidlren
を取得し、{@render children?.()}
を差し込むことによって実装するようになった。
<script>
import "../app.css";
import {
AlignHorizontalJustifyCenter,
Maximize,
Minus,
X,
} from "lucide-svelte";
import { getCurrentWindow } from "@tauri-apps/api/window";
const currentWindow = getCurrentWindow();
const minimize = () => {
currentWindow.minimize();
};
const maximize = async () => {
if (await currentWindow.isMaximized()) {
currentWindow.unmaximize();
} else {
currentWindow.maximize();
}
};
const close = () => {
currentWindow.close();
};
let { children } = $props();
</script>
<header class="bg-zinc-800 h-12 select-none bg-fixed">
<nav
data-tauri-drag-region
class="mx-auto flex items-center justify-between"
aria-label="Global"
>
<div class="mx-1 p-3">
<AlignHorizontalJustifyCenter class="square-4" />
</div>
<ul class="flex gap-x-1">
<li>
<button onclick={minimize} class="window-control-button">
<Minus size="16" />
</button>
</li>
<li>
<button onclick={maximize} class="window-control-button">
<Maximize size="16" />
</button>
</li>
<li>
<button onclick={close} class="window-control-button">
<X size="16" />
</button>
</li>
</ul>
</nav>
</header>
<div class="bg-zinc-900 h-[1px]"></div>
<main class="bg-zinc-800 h-screen">
{@render children?.()}
</main>
<style lang="postcss">
:global(body) {
overflow: hidden;
user-select: none;
color: white;
}
.window-control-button {
@apply hover:bg-zinc-600 p-4;
}
</style>
セキュリティへの対応
API 側からの操作は、デフォルトではセキュリティ設定により実行が不可能な状態になっている。
なので、セキュリティ設定を専用のコンフィグファイルに書き込まなければならない。
Tauri v2 では、セキュリティへの対応がかなり拡張され、厳密、かつ、広範囲に適用できるようになっている。
それ故に分かりづらくなっているが、これらは別のアドカレで別途まとめる予定。
とりあえず対応するには次のように capabilities/default.json
ファイルに書き込めば良い。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open",
+ "core:window:allow-close",
+ "core:window:allow-minimize",
+ "core:window:allow-maximize",
+ "core:window:allow-toggle-maximize",
+ "core:window:allow-start-dragging"
]
}
シングルインスタンス
ここでは以下のことが学べる。
- Tauri v2 でのプラグインの追加の仕方と実装の仕方
プラグインのインストール
まずはインストールする。
Taruvi v2 からは tauri
コマンドから追加できるようになった。
pnpm tauri add single-instance
これを実行すると、tauri コマンドが自動で lib.rs
ファイルに書き込んでくれる。
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
+ .plugin(tauri_plugin_single_instance::init())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
機能の実装
ほとんどは先のインストール時に実装が完了してしまうが、このままだと init()
内の必須引数が渡されていないためエラーが発生する。
なので、これに対応する実装を以下のように行う。
+ use tauri::Manager;
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
+ .plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
+ let _ = app
+ .get_webview_window("main")
+ .expect("no main window")
+ .set_focus();
+ }))
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
まとめ
Tauri v2 になって大きく変わったこと、Svelte 5 になって変わったこと。
その代償はあれど、こうして一つのポスト書くだけで全体像が少しずつつかめてくるのは本当に面白いですね。
色々ドキュメントの部分に粗は見るのですが、意外となんか実装できちゃう感あるのは、なんか Tauri の興味深いところですよね。
この後はよりマニアックにそれぞの機能を深堀していきたいです。
このポストの前後で、僕の過去のそれぞれの機能の記事を Tauri v2 への対応としてアップデートしていきますので、そちらも興味があればぜひ。