ホーム
おまかせ表示
最近の更新
特別ページ
コミュニティ・ポータル
設定
MochiuWiki : SUSE, EC, PCBについて
免責事項
MochiuWiki : SUSE, EC, PCB
検索
利用者メニュー
ログイン
Tauriの基礎 - 非同期Commandsのソースを表示
←
Tauriの基礎 - 非同期Commands
📢 Webサイト閉鎖と移転のお知らせ
このWebサイトは
2026年9月に閉鎖
いたします。
移転先は
https://mochiu.net/index.php?title=Tauriの基礎 - 非同期Commands
です。
新しい記事は移転先で追加しております。
(旧サイトでは記事を追加しておりません)
あなたには「このページの編集」を行う権限がありません。理由は以下の通りです:
この操作は、次のグループのいずれかに属する利用者のみが実行できます:
管理者
、new-group。
このページのソースの閲覧やコピーができます。
== 概要 == Tauriの非同期Commandsは、フロントエンドのUIをブロックせずに、時間のかかる処理をバックエンドで実行するための機能である。<br> <br> Rustのasync/await構文とtokioランタイムを活用することで、ファイル操作、ネットワーク通信、データベースアクセス等の重い処理を効率的に実行できる。<br> <br> 非同期Commandsを使用しない場合、長時間の処理がUIスレッドをブロックし、アプリケーションがフリーズしたように見える問題が発生する。<br> <br> Tauri v2では、Channel APIを使用したストリーミング通信もサポートされており、処理の進捗をリアルタイムでフロントエンドに通知できる。<br> <br> 非同期Commandsの主なメリットは以下の通りである。<br> * UIの応答性維持 *: メインスレッドをブロックせず、バックグラウンドで処理を実行する。 * 並行処理のサポート *: 複数の非同期タスクを同時に実行できる。 * プログレス通知 *: Channel APIを使用して、処理の進捗をリアルタイムで表示できる。 * キャンセル対応 *: 長時間処理の途中でキャンセルを受け付けられる。 <br><br> == 前提条件 == 非同期Commandsを使用するには、以下に示す前提条件を満たしている必要がある。<br> <br> ==== 必要な依存関係 ==== Cargo.tomlファイルに以下に示す依存関係を追加する。<br> <br> <syntaxhighlight lang="toml"> [dependencies] tauri = { version = "2", features = ["unstable"] } serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["full"] } futures = "0.3" </syntaxhighlight> <br> ==== tokioランタイムの理解 ==== tokioは、Rustの非同期ランタイムであり、非同期タスクのスケジューリングと実行を管理する。<br> <br> Tauriは内部でtokioを使用しており、非同期Commandsは自動的にtokioランタイム上で実行される。<br> <br> <center> {| class="wikitable" |+ tokioの主な機能 |- ! 機能 !! 説明 |- | 非同期タスク || asyncブロックを並行実行 |- | タイマー || sleep、interval等の時間制御 |- | ネットワーク || TCP/UDPの非同期I/O |- | ファイルI/O || 非同期ファイル操作 |- | チャネル || mpsc、oneshot等の通信手段 |} </center> <br><br> == async command定義 == 非同期Commandsは、Rustの <code>async fn</code> 構文を使用して定義する。<br> <br> ==== 基本的な非同期Command ==== <syntaxhighlight lang="rust"> use tauri::command; #[command] async fn fetch_data(url: String) -> Result<String, String> { // 非同期HTTPリクエスト let response = reqwest::get(&url) .await .map_err(|e| format!("Request failed: {}", e))?; let body = response.text() .await .map_err(|e| format!("Failed to read response: {}", e))?; Ok(body) } </syntaxhighlight> <br> ==== Tauriでの自動的な非同期処理 ==== Tauriは、<code>async fn</code> で定義されたCommandを自動的に非同期で実行する。<br> <br> フロントエンド側では、通常の <code>invoke</code> 関数を使用して呼び出すことができる。<br> <br> <syntaxhighlight lang="typescript"> import { invoke } from '@tauri-apps/api/core' const fetchData = async () => { try { const result = await invoke<string>('fetch_data', { url: 'https://api.example.com/data' }) console.log('Result:', result) } catch (error) { console.error('Error:', error) } } </syntaxhighlight> <br> ==== 並行実行の仕組み ==== 複数の非同期Commandを並行して実行できる。<br> <br> <syntaxhighlight lang="typescript"> import { invoke } from '@tauri-apps/api/core' interface UserInfo { id: number name: string } interface UserPosts { posts: Array<{ id: number; title: string }> } const fetchUserData = async (userId: number) => { // 並行実行 const [userInfo, userPosts] = await Promise.all([ invoke<UserInfo>('get_user_info', { userId }), invoke<UserPosts>('get_user_posts', { userId }) ]) return { userInfo, userPosts } } </syntaxhighlight> <br> <syntaxhighlight lang="rust"> // Rust側での並行処理 use tokio::join; #[command] async fn fetch_all_data(user_id: u32) -> Result<AllData, String> { let (info, posts) = join!( fetch_user_info(user_id), fetch_user_posts(user_id) ); Ok(AllData { info: info.map_err(|e| e.to_string())?, posts: posts.map_err(|e| e.to_string())?, }) } </syntaxhighlight> <br><br> == tokioランタイム == Tauriは内部でtokioランタイムを使用しており、非同期タスクを効率的に管理する。<br> <br> ==== Tauriのtokio統合 ==== Tauriアプリケーションは、起動時にtokioランタイムを初期化する。<br> <br> <syntaxhighlight lang="rust"> fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ fetch_data, fetch_all_data, process_file, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } </syntaxhighlight> <br> ==== 非同期タスクの実行 ==== <code>tokio::spawn</code> を使用して、バックグラウンドでタスクを実行できる。<br> <br> <syntaxhighlight lang="rust"> use tokio::spawn; use std::sync::Arc; use tokio::sync::Mutex; #[command] async fn start_background_task() -> Result<String, String> { let handle = spawn(async move { // バックグラウンドで実行される処理 for i in 0..10 { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; println!("Background task: {}", i); } "Task completed" }); // タスクの完了を待たずに戻る Ok("Task started in background".to_string()) } </syntaxhighlight> <br> ==== 並行処理のパターン ==== 複数の非同期タスクを管理するパターンを以下に示す。<br> <br> <syntaxhighlight lang="rust"> use tokio::task::JoinSet; #[command] async fn process_multiple_files(paths: Vec<String>) -> Result<Vec<FileResult>, String> { let mut set = JoinSet::new(); for path in paths { set.spawn(async move { process_single_file(path).await }); } let mut results = Vec::new(); while let Some(result) = set.join_next().await { match result { Ok(Ok(file_result)) => results.push(file_result), Ok(Err(e)) => return Err(e), Err(e) => return Err(format!("Task failed: {}", e)), } } Ok(results) } </syntaxhighlight> <br><br> == 借用参照の制約 == 非同期Commandsでは、借用参照の使用に制約がある。<br> <br> ==== String vs &str の違い ==== 非同期関数では、<code>&str</code> よりも <code>String</code> を使用することが推奨される。<br> <br> * 問題のある記述 *: <syntaxhighlight lang="rust"> // これは動作しない可能性がある #[command] async fn process_text(text: &str) -> Result<String, String> { tokio::time::sleep(Duration::from_millis(100)).await; Ok(text.to_uppercase()) } </syntaxhighlight> *: <br> * 推奨される記述 *: <syntaxhighlight lang="rust"> // Stringを使用する #[command] async fn process_text(text: String) -> Result<String, String> { tokio::time::sleep(Duration::from_millis(100)).await; Ok(text.to_uppercase()) } </syntaxhighlight> <br> ==== ライフタイムの考慮 ==== 非同期コンテキストでは、ライフタイムの管理が複雑になる。<br> <br> <syntaxhighlight lang="rust"> use serde::Deserialize; // データを所有する構造体 #[derive(Deserialize)] struct ProcessRequest { input: String, // Stringを使用 options: Vec<String>, // Vecを使用 } #[command] async fn process_request(request: ProcessRequest) -> Result<String, String> { // 非同期処理内でデータを安全に使用できる let result = process_with_options(&request.input, &request.options).await?; Ok(result) } </syntaxhighlight> <br> ==== 所有権の移動パターン ==== 非同期タスクにデータを渡す際は、所有権を移動する。<br> <br> <syntaxhighlight lang="rust"> #[command] async fn start_processing(data: Vec<u8>) -> Result<String, String> { // dataの所有権がこの関数に移動 // tokio::spawnに移動 let handle = tokio::spawn(async move { // dataの所有権がこのクロージャに移動 process_bytes(data).await }); let result = handle.await.map_err(|e| e.to_string())??; Ok(result) } </syntaxhighlight> <br> ==== 参照が必要な場合 ==== どうしても参照が必要な場合は、<code>Arc</code> を使用する。<br> <br> <syntaxhighlight lang="rust"> use std::sync::Arc; use tokio::sync::RwLock; #[command] async fn shared_access(data_id: u32) -> Result<String, String> { // グローバル状態へのアクセス let state = get_app_state().await; let data = state.read().await; match data.get(&data_id) { Some(value) => Ok(value.clone()), None => Err(format!("Data {} not found", data_id)), } } </syntaxhighlight> <br><br> == Channelによるストリーミング通信 == Tauri v2では、Channel APIを使用してストリーミング通信を実現できる。<br> <br> ==== Channel APIの概要 ==== Channelは、バックエンドからフロントエンドへのイベントストリームを提供する。<br> <br> <center> {| class="wikitable" |+ Channelの主な用途 ! 用途 !! 説明 |- | プログレス更新 || 長時間処理の進捗をリアルタイムで通知 |- | ログストリーミング || バックエンドのログをフロントエンドに表示 |- | イベント通知 || ファイル変更、ネットワークイベント等の通知 |} </center> <br> ==== イベントベースの通信 ==== * Rust側でのChannelの定義 *: <syntaxhighlight lang="rust"> use tauri::ipc::Channel; use serde::Serialize; // イベント型の定義 #[derive(Clone, Serialize)] enum DownloadEvent { Started { url: String, total_size: u64 }, Progress { downloaded: u64, percentage: f64 }, Finished { success: bool }, Error { message: String }, } #[command] async fn download_file( url: String, on_event: Channel<DownloadEvent> ) -> Result<(), String> { // 開始イベント on_event.send(DownloadEvent::Started { url: url.clone(), total_size: 0, }).map_err(|e| e.to_string())?; // ダウンロード処理のシミュレーション for i in 0..100 { tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; on_event.send(DownloadEvent::Progress { downloaded: (i + 1) * 1024 * 1024, percentage: (i + 1) as f64, }).map_err(|e| e.to_string())?; } // 完了イベント on_event.send(DownloadEvent::Finished { success: true }) .map_err(|e| e.to_string())?; Ok(()) } </syntaxhighlight> <br> ==== フロントエンドでのChannelの受信 ==== * TypeScript側での定義 *: <syntaxhighlight lang="typescript"> import { invoke, Channel } from '@tauri-apps/api/core' import { useState } from 'react' interface DownloadEvent { Started?: { url: string; total_size: number } Progress?: { downloaded: number; percentage: number } Finished?: { success: boolean } Error?: { message: string } } function DownloadComponent() { const [progress, setProgress] = useState(0) const [status, setStatus] = useState('idle') const [error, setError] = useState<string | null>(null) const handleDownload = async () => { setProgress(0) setStatus('downloading') setError(null) const channel = new Channel<DownloadEvent>() channel.onmessage = (event) => { if (event.Started) { setStatus('downloading') console.log('Download started:', event.Started.url) } else if (event.Progress) { setProgress(event.Progress.percentage) } else if (event.Finished) { setStatus('completed') console.log('Download finished:', event.Finished.success) } else if (event.Error) { setStatus('error') setError(event.Error.message) } } try { await invoke('download_file', { url: 'https://example.com/large-file.zip', onEvent: channel }) } catch (err) { setStatus('error') setError(err as string) } } return ( <div> <h2>File Download</h2> <button onClick={handleDownload} disabled={status === 'downloading'} > {status === 'downloading' ? 'Downloading...' : 'Download'} </button> {status === 'downloading' && ( <div> <progress value={progress} max={100} /> <span>{progress.toFixed(1)}%</span> </div> )} {status === 'completed' && ( <p style={{ color: 'green' }}>Download completed!</p> )} {error && ( <p style={{ color: 'red' }}>Error: {error}</p> )} </div> ) } export default DownloadComponent </syntaxhighlight> <br> ==== 双方向通信の定義 ==== * フロントエンドからバックエンドへのキャンセル通知 *: <syntaxhighlight lang="rust"> use std::sync::Arc; use tokio::sync::atomic::{AtomicBool, Ordering}; #[command] async fn cancellable_download( url: String, on_event: Channel<DownloadEvent>, cancel_flag: Arc<AtomicBool> ) -> Result<(), String> { on_event.send(DownloadEvent::Started { url: url.clone(), total_size: 0, }).map_err(|e| e.to_string())?; for i in 0..100 { // キャンセルチェック if cancel_flag.load(Ordering::Relaxed) { on_event.send(DownloadEvent::Finished { success: false }) .map_err(|e| e.to_string())?; return Ok(()); } tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; on_event.send(DownloadEvent::Progress { downloaded: (i + 1) * 1024 * 1024, percentage: (i + 1) as f64, }).map_err(|e| e.to_string())?; } on_event.send(DownloadEvent::Finished { success: true }) .map_err(|e| e.to_string())?; Ok(()) } </syntaxhighlight> <br><br> == プログレス表示パターン == Channel APIを使用したプログレス表示の定義パターンを示す。<br> <br> ==== 進捗更新の定義 ==== * 複数段階の処理の進捗を通知する例 *: <syntaxhighlight lang="rust"> use serde::Serialize; #[derive(Clone, Serialize)] struct ProcessingProgress { stage: String, current: u32, total: u32, message: String, } #[command] async fn process_large_file( file_path: String, on_progress: Channel<ProcessingProgress> ) -> Result<String, String> { // Stage 1 : 読み込み on_progress.send(ProcessingProgress { stage: "reading".to_string(), current: 0, total: 3, message: "Reading file...".to_string(), }).map_err(|e| e.to_string())?; let content = tokio::fs::read_to_string(&file_path) .await .map_err(|e| format!("Failed to read file: {}", e))?; // Stage 2 : 処理 on_progress.send(ProcessingProgress { stage: "processing".to_string(), current: 1, total: 3, message: "Processing content...".to_string(), }).map_err(|e| e.to_string())?; let lines: Vec<&str> = content.lines().collect(); let total_lines = lines.len(); for (i, line) in lines.iter().enumerate() { // 定期的な進捗更新 if i % 1000 == 0 { on_progress.send(ProcessingProgress { stage: "processing".to_string(), current: 1, total: 3, message: format!("Processing line {}/{}", i, total_lines), }).map_err(|e| e.to_string())?; } // 行の処理 process_line(line); } // Stage 3 : 書き込み on_progress.send(ProcessingProgress { stage: "writing".to_string(), current: 2, total: 3, message: "Writing output...".to_string(), }).map_err(|e| e.to_string())?; // 処理結果の書き込み... // 完了 on_progress.send(ProcessingProgress { stage: "completed".to_string(), current: 3, total: 3, message: "Processing completed!".to_string(), }).map_err(|e| e.to_string())?; Ok("File processed successfully".to_string()) } fn process_line(line: &str) { // 行の処理... } </syntaxhighlight> <br> ==== フロントエンドでの表示 ==== * Reactでのプログレス表示コンポーネント *: <syntaxhighlight lang="typescript"> import { useState } from 'react' import { invoke, Channel } from '@tauri-apps/api/core' interface ProcessingProgress { stage: string current: number total: number message: string } function FileProcessor() { const [progress, setProgress] = useState<ProcessingProgress | null>(null) const [isProcessing, setIsProcessing] = useState(false) const [logs, setLogs] = useState<string[]>([]) const addLog = (message: string) => { setLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]) } const processFile = async (filePath: string) => { setIsProcessing(true) setLogs([]) const channel = new Channel<ProcessingProgress>() channel.onmessage = (event) => { setProgress(event) addLog(event.message) } try { const result = await invoke<string>('process_large_file', { filePath, onProgress: channel }) addLog(`Result: ${result}`) } catch (error) { addLog(`Error: ${error}`) } finally { setIsProcessing(false) } } const handleFileSelect = async () => { // ファイル選択ダイアログを開く const filePath = await invoke<string>('select_file') if (filePath) { await processFile(filePath) } } return ( <div style={{ padding: '20px' }}> <h2>File Processor</h2> <button onClick={handleFileSelect} disabled={isProcessing} > {isProcessing ? 'Processing...' : 'Select File'} </button> {progress && ( <div style={{ marginTop: '20px' }}> <h3>Progress</h3> <p>Stage: {progress.stage}</p> <p>Step: {progress.current}/{progress.total}</p> <progress value={progress.current} max={progress.total} style={{ width: '100%' }} /> <p>{progress.message}</p> </div> )} <div style={{ marginTop: '20px' }}> <h3>Log</h3> <div style={{ height: '200px', overflow: 'auto', backgroundColor: '#f5f5f5', padding: '10px', fontFamily: 'monospace' }} > {logs.map((log, index) => ( <div key={index}>{log}</div> ))} </div> </div> </div> ) } export default FileProcessor </syntaxhighlight> <br> ==== キャンセル処理 ==== * 処理のキャンセルを定義する例 *: <syntaxhighlight lang="typescript"> import { useState, useRef } from 'react' import { invoke, Channel } from '@tauri-apps/api/core' interface DownloadProgress { percentage: number speed: string eta: string } function DownloadWithCancel() { const [progress, setProgress] = useState<DownloadProgress | null>(null) const [isDownloading, setIsDownloading] = useState(false) const cancelRef = useRef<boolean>(false) const startDownload = async (url: string) => { setIsDownloading(true) cancelRef.current = false setProgress(null) const channel = new Channel<DownloadProgress>() channel.onmessage = (event) => { if (!cancelRef.current) { setProgress(event) } } try { await invoke('download_with_cancel', { url, onProgress: channel, cancelToken: cancelRef.current }) } catch (error) { if (!cancelRef.current) { console.error('Download error:', error) } } finally { setIsDownloading(false) } } const cancelDownload = () => { cancelRef.current = true setProgress(null) setIsDownloading(false) } return ( <div> <button onClick={() => startDownload('https://example.com/file.zip')} disabled={isDownloading} > Start Download </button> {isDownloading && ( <button onClick={cancelDownload} style={{ marginLeft: '10px' }}> Cancel </button> )} {progress && ( <div style={{ marginTop: '10px' }}> <progress value={progress.percentage} max={100} /> <p>{progress.percentage.toFixed(1)}%</p> <p>Speed: {progress.speed}</p> <p>ETA: {progress.eta}</p> </div> )} </div> ) } export default DownloadWithCancel </syntaxhighlight> <br><br> == サンプルコード == ==== ファイルダウンロードの進捗表示 ==== * Rust側の定義 *: <syntaxhighlight lang="rust"> use serde::Serialize; // JSONシリアライズ機能を提供 use tauri::ipc::Channel; // TauriのIPCチャネル機能 (フロントエンドとのリアルタイム通信用) // ダウンロード状態を表す列挙型 #[derive(Clone, Serialize)] pub enum DownloadStatus { Idle, // アイドル状態 (ダウンロード未開始) Downloading { progress: f64 }, // ダウンロード中 (進捗率: 0.0〜100.0) Completed, // ダウンロード完了 Failed { error: String }, // ダウンロード失敗 (エラーメッセージ付き) } // ダウンロード進捗情報を格納する構造体 #[derive(Clone, Serialize)] pub struct DownloadProgress { pub status: DownloadStatus, // 現在の状態 pub bytes_downloaded: u64, // ダウンロード済みバイト数 pub total_bytes: u64, // 総バイト数 pub filename: String, // ファイル名 } // Tauriコマンドとしてマーク (フロントエンドから呼び出し可能) #[command] pub async fn download_with_progress( url: String, // ダウンロード対象のURL on_progress: Channel<DownloadProgress> // 進捗通知用チャネル ) -> Result<String, String> { // URLからファイル名を抽出 (最後のスラッシュ以降を取得) let filename = url.split('/').last().unwrap_or("download").to_string(); // ダウンロード開始を通知 (進捗0[%]) on_progress.send(DownloadProgress { status: DownloadStatus::Downloading { progress: 0.0 }, bytes_downloaded: 0, total_bytes: 0, filename: filename.clone(), }).map_err(|e| e.to_string())?; // 送信エラーを文字列に変換 // ダウンロード処理のシミュレーション let total_size = 10 * 1024 * 1024; // 総ファイルサイズ: 10[MB] let chunk_size = 100 * 1024; // チャンクサイズ: 100[KB] let mut downloaded = 0u64; // ダウンロード済みバイト数 // チャンクごとにダウンロードをシミュレート while downloaded < total_size { // 50ミリ秒待機 (実際のダウンロードの遅延をシミュレート) tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; // ダウンロード済みバイト数を更新 (総サイズを超えないように制限) downloaded = (downloaded + chunk_size).min(total_size); // 進捗率を計算(0.0〜100.0の範囲) let progress = (downloaded as f64 / total_size as f64) * 100.0; // 現在の進捗をフロントエンドに通知 on_progress.send(DownloadProgress { status: DownloadStatus::Downloading { progress }, bytes_downloaded: downloaded, total_bytes: total_size, filename: filename.clone(), }).map_err(|e| e.to_string())?; // 送信エラーを文字列に変換 } // ダウンロード完了を通知 on_progress.send(DownloadProgress { status: DownloadStatus::Completed, bytes_downloaded: total_size, total_bytes: total_size, filename: filename.clone(), }).map_err(|e| e.to_string())?; // 送信エラーを文字列に変換 // 成功メッセージを返す Ok(format!("Downloaded: {}", filename)) } </syntaxhighlight> *: <br> * TypeScript / React側の定義 *: <syntaxhighlight lang="typescript"> import { useState } from 'react' // ReactのuseStateフックをインポート (状態管理用) import { invoke, Channel } from '@tauri-apps/api/core' // Tauri APIからinvoke (コマンド実行) と Channel (リアルタイム通信) をインポート // ダウンロード進捗情報の型定義 interface DownloadProgress { status: { Downloading?: { progress: number } // ダウンロード中 (進捗率付き) Completed?: null // 完了状態 Failed?: { error: string } // 失敗状態 (エラーメッセージ付き) } bytes_downloaded: number // ダウンロード済みバイト数 total_bytes: number // 総バイト数 filename: string // ファイル名 } // ダウンロードマネージャーコンポーネント function DownloadManager() { // ダウンロード進捗情報を管理する状態 (URLをキーとするMap) const [downloads, setDownloads] = useState<Map<string, DownloadProgress>>(new Map()) // バイト数を人間が読みやすい形式に変換する関数 const formatBytes = (bytes: number): string => { if (bytes < 1024) return `${bytes} B` // バイト単位 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB` // キロバイト単位 return `${(bytes / (1024 * 1024)).toFixed(2)} MB` // メガバイト単位 } // ダウンロードを開始する非同期関数 const startDownload = async (url: string) => { // 進捗通知を受け取るチャネルを作成 const channel = new Channel<DownloadProgress>() // チャネルからメッセージを受信した時の処理 channel.onmessage = (event) => { // 状態を更新(該当URLの進捗情報を更新) setDownloads((prev) => { const newMap = new Map(prev) // 既存のMapをコピー newMap.set(url, event) // 新しい進捗情報を設定 return newMap // 新しいMapを返す }) } // Rust側のdownload_with_progressコマンドを呼び出し invoke('download_with_progress', { url, onProgress: channel }) .catch(console.error) // エラー時はコンソールに出力 } return ( <div style={{ padding: '20px' }}> <h2>Download Manager</h2> {/* ダウンロードボタンエリア */} <div style={{ marginBottom: '20px' }}> {/* file1.zipのダウンロードボタン */} <button onClick={() => startDownload('https://example.com/file1.zip')}> Download file1.zip </button> {/* file2.zipのダウンロードボタン */} <button onClick={() => startDownload('https://example.com/file2.zip')} style={{ marginLeft: '10px' }} > Download file2.zip </button> </div> {/* ダウンロード進捗リスト */} <div> {/* Mapのエントリを配列に変換してマップ */} {Array.from(downloads.entries()).map(([url, progress]) => ( <div key={url} style={{ border: '1px solid #ccc', padding: '10px', marginBottom: '10px', borderRadius: '4px' }} > {/* ファイル名表示 */} <h4>{progress.filename}</h4> {/* ダウンロード中の表示 */} {progress.status.Downloading && ( <> {/* プログレスバー */} <progress value={progress.status.Downloading.progress} max={100} style={{ width: '100%' }} /> {/* 進捗テキスト */} <p> {progress.status.Downloading.progress.toFixed(1)}% - {formatBytes(progress.bytes_downloaded)} / {formatBytes(progress.total_bytes)} </p> </> )} {/* 完了状態の表示 */} {progress.status.Completed && ( <p style={{ color: 'green' }}>Download completed!</p> )} {/* 失敗状態の表示 */} {progress.status.Failed && ( <p style={{ color: 'red' }}>Error: {progress.status.Failed.error}</p> )} </div> ))} </div> </div> ) } // コンポーネントをエクスポート export default DownloadManager </syntaxhighlight> <br><br> == 推奨される事柄 == ==== 非同期エラーの適切な処理 ==== 非同期処理では、エラーが発生しても適切にハンドリングする必要がある。<br> <br> <syntaxhighlight lang="rust"> // Tauriコマンドとしてマーク (フロントエンドから呼び出し可能) #[command] async fn safe_async_operation() -> Result<String, String> { // ファイルを非同期で読み込む let result = tokio::fs::read_to_string("config.json") .await // 非同期処理の完了を待機 // ファイル読み込みエラーをカスタムエラーメッセージに変換 .map_err(|e| format!("Failed to read config: {}", e))?; // JSON文字列をConfig構造体にパース let config: Config = serde_json::from_str(&result) // JSONパースエラーをカスタムエラーメッセージに変換 .map_err(|e| format!("Failed to parse config: {}", e))?; // 成功時は設定のnameフィールドを返す Ok(config.name) } </syntaxhighlight> <br> ==== リソースのクリーンアップ ==== 非同期タスクが中断された場合でも、リソースを適切に解放する。<br> <br> <syntaxhighlight lang="rust"> use tokio::io::AsyncWriteExt; #[command] async fn write_file_safe(path: String, content: String) -> Result<(), String> { let mut file = tokio::fs::File::create(&path) .await .map_err(|e| format!("Failed to create file: {}", e))?; // 書き込みに失敗した場合でも、ファイルを確実にフラッシュする let result = file.write_all(content.as_bytes()).await; // 常にフラッシュする let _ = file.flush().await; result.map_err(|e| format!("Failed to write: {}", e)) } </syntaxhighlight> <br> ==== タイムアウトの設定 ==== 長時間実行される可能性のある処理にはタイムアウトを設定する。<br> <br> <syntaxhighlight lang="rust"> // 非同期タイムアウト機能を提供するTokioの時間関連モジュールをインポート use tokio::time::{timeout, Duration}; // Tauriコマンドとしてマーク(フロントエンドから呼び出し可能) #[command] async fn fetch_with_timeout(url: String) -> Result<String, String> { // 30秒のタイムアウトを設定してHTTP GETリクエストを実行 let result = timeout( Duration::from_secs(30), // 最大待機時間 : 30秒 reqwest::get(&url) // 指定URLへのGETリクエスト ) .await // 非同期処理の完了を待機 // タイムアウトが発生した場合のエラーハンドリング .map_err(|_| "Request timed out".to_string())? // タイムアウト時はエラーメッセージを返す // HTTPリクエスト自体が失敗した場合のエラーハンドリング .map_err(|e| format!("Request failed: {}", e))?; // 失敗理由を含むエラーメッセージを返す // レスポンスボディをテキストとして取得 let body = result.text() .await // 非同期でテキスト読み込みを待機 // テキスト読み込みに失敗した場合のエラーハンドリング .map_err(|e| format!("Failed to read response: {}", e))?; // 成功時はレスポンスボディを返す Ok(body) } </syntaxhighlight> <br> ==== 同時実行数の制限 ==== リソースを消費する処理は、同時実行数を制限する。<br> <br> <syntaxhighlight lang="rust"> use tokio::sync::Semaphore; use std::sync::Arc; static SEMAPHORE: once_cell::sync::Lazy<Arc<Semaphore>> = once_cell::sync::Lazy::new(|| Arc::new(Semaphore::new(3))); #[command] async fn limited_operation(id: u32) -> Result<String, String> { let permit = SEMAPHORE.acquire() .await .map_err(|e| format!("Semaphore error: {}", e))?; // 最大3つのタスクが同時実行 let result = expensive_operation(id).await?; drop(permit); // 明示的に解放 Ok(result) } </syntaxhighlight> <br><br> == 関連情報 == * [[Tauriの基礎 - Commands]] *: Commandsの基本的な使用方法について * [[Tauriの基礎 - Commandsのエラーハンドリング]] *: エラー処理、thiserror、anyhowについて * [https://tauri.app/v2/guide/features/command/ Tauri公式ドキュメント - Commands] *: 公式のCommandsリファレンス * [https://tokio.rs/tokio/tutorial tokio公式チュートリアル] *: tokioの詳細な使用方法 * [https://docs.rs/tokio/latest/tokio/ tokio APIドキュメント] *: tokioのAPIリファレンス <br><br> {{#seo: |title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki |keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Tauri,Rust,React,TypeScript,Async,Commands,Channel,tokio,非同期,プログレス,ストリーミング |description={{PAGENAME}} - Tauri非同期Commandsの実装ガイド | This page is {{PAGENAME}} in our wiki about Tauri async commands |image=/resources/assets/MochiuLogo_Single_Blue.png }} __FORCETOC__ [[カテゴリ:Rust]]
Tauriの基礎 - 非同期Commands
に戻る。