tauri-app-dev について
Web フロントエンドと Rust バックエンドでデスクトップアプリを構築する Tauri 2.0 開発スキルです。IPC、権限、プラグイン、パッケージ化、テストを扱います。
主な機能
- 01Web フロントエンドと Rust バックエンドの構成
- 02コマンドとイベントによる IPC
- 03ケイパビリティと API 権限の設定
- 04ファイルシステムなどのプラグイン利用
- 05アプリのビルド、バンドル、テスト
活用シーン
- 01Web フロントエンドから Rust コマンドを呼び出す
- 02ウィンドウのファイルアクセス権限を設定
- 03ファイルダイアログと読み書きを実装
- 04実行中の Tauri アプリをテスト
MCP 対応
実行中のアプリの E2E テストに Tauri MCP ツールを使用し、chrome-devtools MCP はこの用途に使わないよう説明しています。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ xiaolai/ vmark --skill tauri-app-devインストール後、Claude Code で/tauri-app-devを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/bundling.mdcommands-and-ipc.mdpatterns.mdplugins.mdsecurity.md詳細
Tauri 2.0 アプリ開発
Tauri は、Web フロントエンドと Rust バックエンドを使用して、小さく、高速で、安全なデスクトップアプリを構築するためのフレームワークです。
アーキテクチャの概要
┌─────────────────────────────────────────┐
│ Frontend (Webview) │
│ HTML/CSS/JS • React/Vue/Svelte │
└────────────────┬────────────────────────┘
│ IPC (invoke/events)
┌────────────────▼────────────────────────┐
│ Tauri Core (Rust) │
│ Commands • State • Plugins • Events │
└────────────────┬────────────────────────┘
│ TAO (windows) + WRY (webview)
┌────────────────▼────────────────────────┐
│ Operating System │
│ macOS • Windows • Linux • Mobile │
└─────────────────────────────────────────┘
プロジェクト構成
my-app/
├── src/ # Frontend source
├── src-tauri/
│ ├── Cargo.toml # Rust dependencies
│ ├── tauri.conf.json # Tauri configuration
│ ├── capabilities/ # Security permissions (v2)
│ │ └── default.json
│ ├── src/
│ │ ├── main.rs # Desktop entry point
│ │ └── lib.rs # Main app logic + mobile entry
│ └── icons/
└── package.json
コマンド(フロントエンド → Rust)
Rust で #[tauri::command] を使ってコマンドを定義します。
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
std::fs::read_to_string(&path).map_err(|e| e.to_string())
}
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, read_file])
.run(tauri::generate_context!())
.expect("error running app");
}
フロントエンドから直接呼び出します。
import { invoke } from '@tauri-apps/api/core';
const greeting = await invoke<string>('greet', { name: 'World' });
const content = await invoke<string>('read_file', { path: '/tmp/test.txt' });
プロジェクト規約: invoke() を TanStack Query でラップし、キャッシュと状態管理を行います。
import { useQuery, useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
// Query (read operations)
const { data: content } = useQuery({
queryKey: ['file', path],
queryFn: () => invoke<string>('read_file', { path }),
});
// Mutation (write operations)
const { mutate: saveFile } = useMutation({
mutationFn: (content: string) => invoke('write_file', { path, content }),
});
重要なルール:
- 引数は
serde::Deserializeを実装する必要があります - 戻り値の型は
serde::Serializeを実装する必要があります - 失敗する可能性のある操作には
Result<T, E>を使用します - 非同期コマンドはスレッドプールで実行されます(ノンブロッキング)
- Rust のスネークケースは、JS の引数ではキャメルケースになります
状態管理
コマンド間で状態を共有します。
use std::sync::Mutex;
use tauri::State;
struct AppState {
counter: Mutex<i32>,
db: Mutex<Option<Database>>,
}
#[tauri::command]
fn increment(state: State<'_, AppState>) -> i32 {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
*counter
}
pub fn run() {
tauri::Builder::default()
.manage(AppState {
counter: Mutex::new(0),
db: Mutex::new(None),
})
.invoke_handler(tauri::generate_handler![increment])
.run(tauri::generate_context!())
.expect("error running app");
}
AppHandle 経由でのアクセス(バックグラウンドスレッド用):
use tauri::Manager;
#[tauri::command]
async fn background_task(app: tauri::AppHandle) {
let state = app.state::<AppState>();
// use state...
}
イベント(Rust → フロントエンド)
Rust からイベントを発行します。
use tauri::Emitter;
#[tauri::command]
fn start_process(app: tauri::AppHandle) {
std::thread::spawn(move || {
for i in 0..100 {
app.emit("progress", i).unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
}
app.emit("complete", "Done!").unwrap();
});
}
フロントエンドでリッスンします。
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen<number>('progress', (event) => {
console.log(`Progress: ${event.payload}%`);
});
// Clean up when done
unlisten();
必須プラグイン
プラグインをインストールします。src-tauri で cargo add <plugin>、フロントエンドで pnpm add <package> を実行します。
| プラグイン | Cargo クレート | NPM パッケージ | 用途 |
|---|---|---|---|
| ファイルシステム | tauri-plugin-fs | @tauri-apps/plugin-fs | ファイルの読み書き |
| ダイアログ | tauri-plugin-dialog | @tauri-apps/plugin-dialog | 開く/保存ダイアログ |
| クリップボード | tauri-plugin-clipboard-manager | @tauri-apps/plugin-clipboard-manager | コピー/貼り付け |
| シェル | tauri-plugin-shell | @tauri-apps/plugin-shell | 外部コマンドの実行 |
| ストア | tauri-plugin-store | @tauri-apps/plugin-store | キーと値の永続化 |
| アップデーター | tauri-plugin-updater | @tauri-apps/plugin-updater | 自動アップデート |
Rust で登録します。
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_clipboard_manager::init())
.run(tauri::generate_context!())
.expect("error running app");
}
セキュリティ:ケイパビリティと権限
Tauri 2.0 では、ケイパビリティ(src-tauri/capabilities/ 内)を使用して、各ウィンドウがアクセスできる API を制御します。
src-tauri/capabilities/default.json:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-read-text-file",
"dialog:default",
{
"identifier": "fs:scope",
"allow": [{ "path": "$APPDATA/**" }, { "path": "$DOCUMENT/**" }]
}
]
}
スコープ変数: $APPDATA、$APPCONFIG、$DOCUMENT、$DOWNLOAD、$HOME、$TEMP など。
ファイル操作(エディターパターン)
import { open, save } from '@tauri-apps/plugin-dialog';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
// Open file dialog
const path = await open({
filters: [{ name: 'Markdown', extensions: ['md'] }],
multiple: false,
});
if (path) {
const content = await readTextFile(path);
// Edit content...
await writeTextFile(path, modifiedContent);
}
// Save as dialog
const savePath = await save({
filters: [{ name: 'Markdown', extensions: ['md'] }],
defaultPath: 'untitled.md',
});
if (savePath) {
await writeTextFile(savePath, content);
}
ウィンドウ管理
実行時にウィンドウを作成します。
use tauri::{WebviewUrl, WebviewWindowBuilder};
#[tauri::command]
async fn open_settings(app: tauri::AppHandle) -> Result<(), String> {
WebviewWindowBuilder::new(&app, "settings", WebviewUrl::App("settings.html".into()))
.title("Settings")
.inner_size(600.0, 400.0)
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
tauri.conf.json で設定します。
{
"app": {
"windows": [
{
"label": "main",
"title": "My App",
"width": 1200,
"height": 800,
"decorations": true,
"resizable": true
}
]
}
}
カスタムタイトルバー
設定で decorations: false を指定し、次のようにします。
<div data-tauri-drag-region class="titlebar">
<span>My App</span>
<button id="minimize">−</button>
<button id="maximize">□</button>
<button id="close">×</button>
</div>
import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
document.getElementById('minimize')?.addEventListener('click', () => appWindow.minimize());
document.getElementById('maximize')?.addEventListener('click', () => appWindow.toggleMaximize());
document.getElementById('close')?.addEventListener('click', () => appWindow.close());
ビルドと配布
# Development
pnpm tauri dev
# Production build
pnpm tauri build
# Build specific targets
pnpm tauri build --target universal-apple-darwin # macOS universal
pnpm tauri build --bundles deb,appimage # Linux only
pnpm tauri build --bundles nsis # Windows NSIS
出力先:
- macOS:
target/release/bundle/macos/*.app、*.dmg - Windows:
target/release/bundle/nsis/*-setup.exe、msi/*.msi - Linux:
target/release/bundle/deb/*.deb、appimage/*.AppImage
クイックリファレンス
| タスク | リソース |
|---|---|
| コマンド、IPC、チャネル | references/commands-and-ipc.md を参照 |
| プラグインの使用と開発 | references/plugins.md を参照 |
| セキュリティ設定 | references/security.md を参照 |
| バンドルと配布 | references/bundling.md を参照 |
| 一般的なアプリのパターン | references/patterns.md を参照 |
テスト駆動開発(TDD)
重要: 必ず TDD に従い、実装の前にテストを書いてください。
TDD ワークフロー
1. RED → Write failing test first
2. GREEN → Write minimal code to pass
3. REFACTOR → Clean up, keep tests green
テストスタック
| レイヤー | ツール | 用途 |
|---|---|---|
| Rust 単体テスト | cargo test | コマンドとビジネスロジックのテスト |
| React 単体テスト | Vitest | コンポーネント、フック、ストアのテスト |
| 統合テスト | Vitest + MSW | モック IPC を使用したフロントエンドのテスト |
| E2E | Tauri MCP | 実行中のアプリのテスト(Chrome DevTools は使用しない) |
Tauri MCP による E2E テスト
重要: 実行中のアプリをテストする際は、必ず tauri_* MCP ツールを使用してください。chrome-devtools MCP は使用しないでください。ブラウザページ専用です。
// Tauri MCP workflow for E2E tests:
// 1. Start session (connect to running Tauri app)
tauri_driver_session({ action: 'start', port: 9223 })
// 2. Take snapshot (get DOM state)
tauri_webview_screenshot()
tauri_webview_find_element({ selector: '.editor-content' })
// 3. Interact with app
tauri_webview_interact({ action: 'click', selector: '#save-button' })
tauri_webview_keyboard({ action: 'type', selector: 'input', text: 'hello' })
// 4. Wait for results
tauri_webview_wait_for({ type: 'selector', value: '.success-toast' })
// 5. Verify IPC calls
tauri_ipc_monitor({ action: 'start' })
tauri_ipc_get_captured({ filter: 'save_file' })
// 6. Check backend state
tauri_ipc_execute_command({ command: 'get_app_state' })
// 7. Read logs for debugging
tauri_read_logs({ source: 'console', lines: 50 })
Rust 単体テスト
// src-tauri/src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
let result = greet("World".to_string());
assert_eq!(result, "Hello, World!");
}
#[test]
fn test_parse_markdown() {
let input = "# Hello";
let result = parse_markdown(input);
assert!(result.is_ok());
assert_eq!(result.unwrap().title, "Hello");
}
#[tokio::test]
async fn test_async_command() {
let result = read_file("/tmp/test.txt".to_string()).await;
// Test with temp files or mocks
}
}
実行:cd src-tauri && cargo test
React コンポーネントテスト(Vitest)
// src/components/Editor.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Editor } from './Editor'
// Mock Tauri invoke
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn()
}))
describe('Editor', () => {
it('should render editor content', () => {
render(<Editor initialValue="# Hello" />)
expect(screen.getByText('Hello')).toBeInTheDocument()
})
it('should call save on Ctrl+S', async () => {
const { invoke } = await import('@tauri-apps/api/core')
render(<Editor initialValue="test" />)
await userEvent.keyboard('{Control>}s{/Control}')
expect(invoke).toHaveBeenCalledWith('save_file', expect.any(Object))
})
})
Zustand ストアのテスト
// src/stores/editorStore.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { useEditorStore } from './editorStore'
describe('editorStore', () => {
beforeEach(() => {
// Reset store before each test
useEditorStore.setState({
content: '',
isDirty: false,
filePath: null
})
})
it('should update content and mark dirty', () => {
const { setContent } = useEditorStore.getState()
setContent('new content')
const state = useEditorStore.getState()
expect(state.content).toBe('new content')
expect(state.isDirty).toBe(true)
})
it('should clear dirty flag after save', () => {
useEditorStore.setState({ isDirty: true })
const { markSaved } = useEditorStore.getState()
markSaved()
expect(useEditorStore.getState().isDirty).toBe(false)
})
})
モック IPC を使用した統合テスト
// src/features/file/useFileOperations.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useFileOperations } from './useFileOperations'
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn()
}))
vi.mock('@tauri-apps/plugin-dialog', () => ({
open: vi.fn(),
save: vi.fn()
}))
describe('useFileOperations', () => {
let queryClient: QueryClient
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } }
})
vi.clearAllMocks()
})
it('should open file and load content', async () => {
const { invoke } = await import('@tauri-apps/api/core')
const { open } = await import('@tauri-apps/plugin-dialog')
vi.mocked(open).mockResolvedValue('/path/to/file.md')
vi.mocked(invoke).mockResolvedValue('# File Content')
const { result } = renderHook(() => useFileOperations(), {
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
})
await result.current.openFile()
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_file', { path: '/path/to/file.md' })
})
})
})
TDD の例:新機能の追加
// Step 1: RED - Write failing test first
// src/features/wordcount/useWordCount.test.ts
describe('useWordCount', () => {
it('should count words in content', () => {
const { result } = renderHook(() => useWordCount('hello world'))
expect(result.current.words).toBe(2)
})
it('should handle empty content', () => {
const { result } = renderHook(() => useWordCount(''))
expect(result.current.words).toBe(0)
})
it('should count characters', () => {
const { result } = renderHook(() => useWordCount('hello'))
expect(result.current.characters).toBe(5)
})
})
// Step 2: GREEN - Minimal implementation
// src/features/wordcount/useWordCount.ts
export function useWordCount(content: string) {
return {
words: content.trim() ? content.trim().split(/\s+/).length : 0,
characters: content.length
}
}
// Step 3: REFACTOR - Add memoization, types, etc.
export function useWordCount(content: string): WordCountResult {
return useMemo(() => ({
words: content.trim() ? content.trim().split(/\s+/).length : 0,
characters: content.length,
charactersNoSpaces: content.replace(/\s/g, '').length
}), [content])
}
テストの実行
# All tests
pnpm test
# Watch mode
pnpm test:watch
# Coverage
pnpm test:coverage
# Rust tests only
cd src-tauri && cargo test
# Type check + lint + test
pnpm check:all
デバッグのヒント
- 開発者ツール: 右クリックして「検証」を選択するか、
Cmd+Option+I(macOS)/Ctrl+Shift+I(Windows/Linux)を押します - Rust のログ:
logクレートとtauri-plugin-logを使用するか、println!を使用します(ターミナルに表示されます) - ケイパビリティの確認: 「許可されていません」というエラーは、ケイパビリティに権限が不足していることを意味します
- IPC エラー: 引数名が一致していることを確認します(Rust の snake_case → JS の camelCase)
- E2E デバッグ:
tauri_read_logs({ source: 'console' })を使用して、WebView のコンソールを確認します
関連するスキル
tauri-v2-integration— VMark 固有の Tauri IPC パターン(invoke/emit ブリッジ、メニューアクセラレーター)tauri-mcp-testing— MCP ツールを使用した、実行中の Tauri アプリの E2E テストrust-tauri-backend— VMark の Rust バックエンド(コマンド、メニュー項目、ファイルシステム)