はじめに
例えば、GUI ツールのログを出していている際、その処理自体はバックエンドの Rust 側で起きていることだが、これをリアルタイムかつ、 TypeScript + フロントエンド上で扱い、表示をしたいと言ったこともあると思う。
こうした、サービス型のプロセスは、ユーザー自ら実行するタイプの Tauri コマンドではできない。
そこで、Tauri には Events という機能があるという事を見つけた。
これを使う事で解決が可能ではないかと思い立ったので、これについての実装を検証してみることにする
環境
- Windows 11 Pro 23H2
- Tauri 1.5.3
- Rust 1.74.1
- Svelte 4.2.7
サンプルコード
Tauri Events
Events はドキュメントによると、フロントエンドとバックエンド間でマルチプロデューサー・マルチコンシューマー コミュニケーションを可能にする機能とのこと。
Tauri Events には二種類あり、「グローバルイベント (Global Event) 」と「ウィンドウイベント (Window Event)」がある。
それぞれに使い方の差異はあるようだが、設定の方法はあまり変わらないためここでは「グローバルイベント」の設定方法をベースに記述する。
フロントエンド -> バックエンド
<script lang="ts">
import { emit, listen } from '@tauri-apps/api/event';
async function onClicked() {
emit('click', { theMessage: 'Tauri is awsome!' });
}
</script>
<button on:click={onClicked}>Hello</button>
use tauri::Manager;
fn main() {
tauri::Builder::default()
.setup(|app| {
let id = app.listen_global("click", |event| {
println!("got `click` event with payload {:?}", event.payload());
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
以下のように Hello ボタンをクリックすると、バックエンド側の出力にメッセージがプリントされる。
特定の条件下でリッスン状態を止めたい
例えば、環境変数に「TAURI_UNLISTEN=1」が設定されていればリッスン状態を解除する。
fn main() {
tauri::Builder::default()
.setup(|app| {
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);
+ }
+ }
+ _ => (),
+ }
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
フロントエンド <- バックエンド
こちらは、バックエンド (Rust) 側での操作をフロントエンド側へ反映したい場合に用いる。
こちらの用法が Tarui Events としての利用頻度がかなり高くなる手法。
例えば、以下の様にバックエンド側でイベントループを発生させている際に、定時実行でフロントエンド側に反映させたいとるする。この場合は、フロントエンド側からの Tarui Command の実行は期待できないので、バックエンド側から能動的に反映処理を行う必要がある。この場合には Tauri Events を使う事になる。
use chrono::Utc;
use cron::Schedule;
use serde::Serialize;
use std::{env, str::FromStr, thread::sleep, time::Duration};
use tauri::Manager;
#[derive(Clone, Serialize)]
struct CountEventPayload {
count: u32,
}
fn main() {
tauri::Builder::default()
.setup(|app| {
// Click event ==========
// ... 省略
// Count Event ==========
// アプリハンドルを先に作成しておく。
// つい、app を渡したくなってしまうが、この app は &mut App といった
// Mutabble な値なため、非同期時の値の整合性の保証がなくなってしまう。
// それ故、そのまま非同期ランタイムに渡そうとするとコンパイラに怒られる。
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();
// 条件に入った際の処理
if now >= next_tick {
next_tick = schedule.upcoming(Utc).next().unwrap();
// 1 カウントする
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");
}
<script lang="ts">
import { Button, Indicator } from 'flowbite-svelte';
import { listen } from '@tauri-apps/api/event';
import { EnvelopeSolid } from 'flowbite-svelte-icons';
type Payload = {
count: number;
};
let count = 0;
$: listen('count_event', (event) => {
let payload = event.payload as Payload;
count = payload.count;
});
</script>
<Button class="relative" size="sm">
<EnvelopeSolid class="text-white dark:text-white" />
<span class="sr-only">Notifications</span>
<Indicator color="blue" border size="xl"
placement="top-right" class="text-xs font-bold"
>
{count}
</Indicator>
</Button>
ちなみに、処理自体は全部バックエンドで管理していあるので、UI 上で再処理がかかって初期値が当てられても、バックエンド上の値が定時で返ってくる。
(実際はタブの切り替えに Tauri Command を仕込んで取得すると言ったことをするだろう。)
まとめ
Tauri Events は Tauri Command と同じく、頻繁に使用していく機能だとわかった。
特に Events で重要だと感じたのは、バックエンド -> フロントエンドへの更新のしやすさ。基本的に、バックエンド -> フロントエンドでフロントエンド更新を行いたい場合は Tauri Events を使うことになるだろう。
実際にもう少し作り込んでいけば、かなりトリッキーなこともここら辺を使ってできそうなので、記録しておく。