6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tauri のイベントハンドリングについて - Tauri Events -

Last updated at Posted at 2024-03-07

はじめに

例えば、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)」がある。

それぞれに使い方の差異はあるようだが、設定の方法はあまり変わらないためここでは「グローバルイベント」の設定方法をベースに記述する。

マルチプロデューサー/マルチコンシューマーとは
そもそも、プロデューサー(生産者)/コンシューマー(消費者)という構造はマルチスレッドやネットワーク通信でよく用いられる用語らしい。

今回のケースは後者の方で、こちらによると、

プロデューサはデータを作成して次々に送信していきます。
コンシューマ側では受信したデータの中に格納されたIDを確認し、自分に関連するID情報を順番に処理していきます。

とあります。
Tauri Events の場合は、これをより複数送信者・受信者を持った状態で通信できる機構といったことの様です。

フロントエンド -> バックエンド

+page.svelte
<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>

main.rs
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 ボタンをクリックすると、バックエンド側の出力にメッセージがプリントされる。

output-palette.gif

特定の条件下でリッスン状態を止めたい
例えば、環境変数に「TAURI_UNLISTEN=1」が設定されていればリッスン状態を解除する。

main.rs
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 を使う事になる。

main.rs
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");
}
CountEvent.svelte
<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>

output-palette2.gif

ちなみに、処理自体は全部バックエンドで管理していあるので、UI 上で再処理がかかって初期値が当てられても、バックエンド上の値が定時で返ってくる。
(実際はタブの切り替えに Tauri Command を仕込んで取得すると言ったことをするだろう。)

output-palette4.gif

まとめ

Tauri Events は Tauri Command と同じく、頻繁に使用していく機能だとわかった。

特に Events で重要だと感じたのは、バックエンド -> フロントエンドへの更新のしやすさ。基本的に、バックエンド -> フロントエンドでフロントエンド更新を行いたい場合は Tauri Events を使うことになるだろう。

実際にもう少し作り込んでいけば、かなりトリッキーなこともここら辺を使ってできそうなので、記録しておく。

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?