tauri-app-dev

安装量: 57
排名: #13073

安装

npx skills add https://github.com/xiaolai/vmark --skill tauri-app-dev

Tauri 2.0 App Development Tauri is a framework for building small, fast, secure desktop apps using web frontends and Rust backends. Architecture Overview ┌─────────────────────────────────────────┐ │ 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 │ └─────────────────────────────────────────┘ Project Structure 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 Commands (Frontend → Rust) Define commands in Rust with

[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" ) ; } Call from frontend (direct): 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' } ) ; Project convention: Wrap invoke() with TanStack Query for caching and state management: 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 } ) , } ) ; Key rules: Arguments must implement serde::Deserialize Return types must implement serde::Serialize Use Result for fallible operations Async commands run on thread pool (non-blocking) Snake_case in Rust → camelCase in JS arguments State Management Share state across commands: 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" ) ; } Access via AppHandle (for background threads): use tauri :: Manager ;

[tauri::command]

async fn background_task ( app : tauri :: AppHandle ) { let state = app . state :: < AppState

( ) ; // use state... } Events (Rust → Frontend) Emit events from 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 ( ) ; } ) ; } Listen in frontend: import { listen } from '@tauri-apps/api/event' ; const unlisten = await listen < number

( 'progress' , ( event ) => { console . log ( Progress: ${ event . payload } % ) ; } ) ; // Clean up when done unlisten ( ) ; Essential Plugins Install plugins: cargo add in src-tauri, pnpm add in frontend. Plugin Cargo Crate NPM Package Purpose File System tauri-plugin-fs @tauri-apps/plugin-fs Read/write files Dialog tauri-plugin-dialog @tauri-apps/plugin-dialog Open/save dialogs Clipboard tauri-plugin-clipboard-manager @tauri-apps/plugin-clipboard-manager Copy/paste Shell tauri-plugin-shell @tauri-apps/plugin-shell Run external commands Store tauri-plugin-store @tauri-apps/plugin-store Key-value persistence Updater tauri-plugin-updater @tauri-apps/plugin-updater Auto-updates Register in 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" ) ; } Security: Capabilities & Permissions Tauri 2.0 uses capabilities (in src-tauri/capabilities/ ) to control what APIs each window can access. 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/" } ] } ] } Scope variables: $APPDATA , $APPCONFIG , $DOCUMENT , $DOWNLOAD , $HOME , $TEMP , etc. File Operations (Editor Pattern) 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 ) ; } Window Management Create windows at runtime: 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 ( ( ) ) } Configure in tauri.conf.json: { "app" : { "windows" : [ { "label" : "main" , "title" : "My App" , "width" : 1200 , "height" : 800 , "decorations" : true , "resizable" : true } ] } } Custom Titlebar Set decorations: false in config, then: < 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 ( ) ) ; Building & Distribution

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

Output locations: macOS: target/release/bundle/macos/.app , .dmg Windows: target/release/bundle/nsis/-setup.exe , msi/.msi Linux: target/release/bundle/deb/.deb , appimage/.AppImage Quick Reference Task Resource Commands, IPC, channels See references/commands-and-ipc.md Plugin usage & development See references/plugins.md Security configuration See references/security.md Bundling & distribution See references/bundling.md Common app patterns See references/patterns.md Test-Driven Development (TDD) CRITICAL: Always follow TDD - write tests BEFORE implementation. TDD Workflow 1. RED → Write failing test first 2. GREEN → Write minimal code to pass 3. REFACTOR → Clean up, keep tests green Testing Stack Layer Tool Purpose Rust Unit cargo test Test commands, business logic React Unit Vitest Test components, hooks, stores Integration Vitest + MSW Test frontend with mocked IPC E2E Tauri MCP Test running app (NOT Chrome DevTools) E2E Testing with Tauri MCP IMPORTANT: Always use tauri_* MCP tools for testing the running app. Do NOT use chrome-devtools MCP - it's for browser pages only. // 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 Unit Tests // 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 } } Run: cd src-tauri && cargo test React Component Tests (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 Store Tests // 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 ) } ) } ) Integration Tests with Mocked 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 Example: Adding a New Feature // 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 ] ) } Running Tests

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 Debugging Tips DevTools: Right-click → Inspect, or Cmd+Option+I (macOS) / Ctrl+Shift+I (Windows/Linux) Rust logs: Use log crate + tauri-plugin-log or println! (visible in terminal) Check capabilities: "Not allowed" errors mean missing permissions in capabilities IPC errors: Ensure argument names match (snake_case Rust → camelCase JS) E2E debugging: Use tauri_read_logs({ source: 'console' }) to see webview console

返回排行榜