Lesson 12-Tauri Basics Introduction

Introduction to Tauri and Its Advantages

What is Tauri?

Tauri is a framework for building cross-platform desktop applications, enabling developers to use web technologies (HTML, CSS, and JavaScript) for the frontend interface while leveraging Rust for backend logic. Tauri’s core goal is to create lightweight, fast, and secure desktop applications for Windows, macOS, and Linux.

  • Core Components:
    • Frontend: Supports any framework that compiles to HTML/CSS/JS (e.g., React, Vue, Svelte).
    • Backend: Rust-written core logic interacting with the frontend via Webview.
    • Webview: Utilizes the operating system’s native Webview (e.g., WebView2 on Windows, WKWebView on macOS).

Core Advantages of Tauri

  1. Lightweight:
    • Unlike Electron, which bundles a full Chromium engine, Tauri uses the system’s native Webview, resulting in significantly smaller application sizes (as low as 600KB for minimal apps).
  2. High Performance:
    • Rust delivers near-native execution speed with low memory usage.
  3. Security:
    • Rust’s memory safety features reduce vulnerabilities.
    • Offers sandboxing and permission controls.
  4. Cross-Platform:
    • A single codebase supports multiple platforms without additional adjustments.
  5. Flexibility:
    • Supports various frontend frameworks, allowing developers to use familiar tools.

Tauri vs. Electron Comparison

FeatureTauriElectron
Backend LanguageRustNode.js
WebviewSystem NativeChromium
App SizeSmall (<1MB)Large (>100MB)
Memory UsageLowHigh
SecurityHigh (Rust + Sandbox)Moderate
Learning CurveModerate (Rust required)Low (Pure JS)

Installing the Rust Programming Environment

Steps to Install Rust

Rust is a core dependency for Tauri, used to compile backend code.

Windows

  1. Download Rustup:
  2. Run the Installer:
rustup-init.exe
  • Select the default installation (press 1 and Enter).
  1. Configure Environment Variables:
  • The installer automatically adds Rust to the PATH.

macOS/Linux

  1. Install via curl:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  1. Follow Prompts:
  • Choose the default installation (enter 1 and press Enter).
  1. Refresh Terminal:
source $HOME/.cargo/env

Verify Rust Installation

rustc --version
cargo --version
  • Example Output:
rustc 1.77.0 (aedd173a2 2024-03-17)
cargo 1.77.0 (a1f873e 2024-03-03)

Installing Node.js and npm

Installing Node.js and npm

Node.js powers the Tauri CLI, and npm is required for frontend development.

Windows/macOS

  1. Download Installer:
    • Visit the Node.js website and download the LTS version (18.x or higher recommended).
  2. Run Installer:
    • Follow default settings to install, which includes npm.

Linux

  1. Use Package Manager:
sudo apt update
sudo apt install nodejs npm
  1. Verify Versions:
node --version
npm --version

Verify Installation

node -v
npm -v
  • Example Output:
v18.19.0
10.2.3

Installing Tauri CLI

Install Tauri CLI Using npm

npm install -g @tauri-apps/cli

Note

  • If you encounter permission issues (macOS/Linux), use:
sudo npm install -g @tauri-apps/cli

Verify Tauri CLI Installation

tauri --version
  • Example Output:
@tauri-apps/cli v1.5.12

Tauri Core Architecture

Frontend Framework Integration

Multi-Framework Adaptation Layer Design

// src-tauri/tauri-api/src/webview/mod.rs
pub struct Webview {
    inner: WebView<Window>,
    framework: FrameworkType, // React/Vue/Svelte
}

impl Webview {
    pub fn emit<F: Serialize>(&self, event: &str, payload: F) -> Result<()> {
        match self.framework {
            FrameworkType::React => {
                // React-specific event handling
                self.inner.emit(event, payload)
            }
            FrameworkType::Vue => {
                // Vue-specific event handling (supports reactive system)
                self.inner.emit(event, payload)
            }
            FrameworkType::Svelte => {
                // Svelte-specific event handling
                self.inner.emit(event, payload)
            }
        }
    }
}

// Frontend framework adapter example (React)
// src/hooks/useTauri.ts
import { useEffect } from 'react';

export function useTauri() {
  useEffect(() => {
    // Initialize Tauri API
    const tauri = window.__TAURI__;

    // Bridge Tauri events to React state
    const handleTauriEvent = (event: string, payload: any) => {
      // Convert Tauri events to React state updates
    };

    tauri.event.listen('tauri-event', handleTauriEvent);

    return () => {
      tauri.event.unlisten('tauri-event', handleTauriEvent);
    };
  }, []);
}

Framework-Specific Optimizations

// src-tauri/src/api/framework.rs
pub fn init_framework(framework: FrameworkType) -> Result<()> {
    match framework {
        FrameworkType::React => {
            // Inject React-specific polyfill
            js_sys::eval(
                r#"
                if (!window.__TAURI_REACT__) {
                    window.__TAURI_REACT__ = {
                        useState: /* React state bridge */,
                        useEffect: /* React effect bridge */
                    };
                }
                "#,
            )?;
        }
        FrameworkType::Vue => {
            // Vue reactive system integration
            js_sys::eval(
                r#"
                if (!window.__TAURI_VUE__) {
                    window.__TAURI_VUE__ = {
                        reactive: /* Vue reactive bridge */,
                        watch: /* Vue watch bridge */
                    };
                }
                "#,
            )?;
        }
        FrameworkType::Svelte => {
            // Svelte store integration
            js_sys::eval(
                r#"
                if (!window.__TAURI_SVELTE__) {
                    window.__TAURI_SVELTE__ = {
                        writable: /* Svelte store bridge */
                    };
                }
                "#,
            )?;
        }
    }
    Ok(())
}

Rust Backend and System Webview Interaction

Webview Lifecycle Management

// src-tauri/src/webview/mod.rs
pub struct WebViewManager {
    webviews: HashMap<WindowId, WebView<Window>>,
}

impl WebViewManager {
    pub fn create_webview(&mut self, window: Window) -> Result<()> {
        let webview = WebView::new(window, |builder| {
            // Configure Webview
            builder
                .with_web_context(web_context.clone())
                .with_devtools(false)
        })?;

        self.webviews.insert(window.id(), webview);
        Ok(())
    }

    pub fn evaluate_script(&self, window_id: WindowId, script: &str) -> Result<String> {
        if let Some(webview) = self.webviews.get(&window_id) {
            webview.evaluate_script(script)
        } else {
            Err(Error::WebviewNotFound)
        }
    }
}

System API Bridge Implementation

// src-tauri/src/api/system.rs
#[tauri::command]
fn get_system_info() -> SystemInfo {
    SystemInfo {
        os: std::env::consts::OS.to_string(),
        arch: std::env::consts::ARCH.to_string(),
        version: get_os_version(),
    }
}

// File system operation example
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
    tokio::fs::read_to_string(path)
        .await
        .map_err(|e| e.to_string())
}

Process Model and Communication Mechanism

Frontend-Backend Communication Architecture

// src-tauri/src/main.rs
fn main() {
    tauri::Builder::default()
        .setup(|app| {
            // Initialize IPC channel
            let window = app.get_window("main").unwrap();
            let (tx, rx) = flume::unbounded();

            // Background task
            std::thread::spawn(move || {
                while let Ok(msg) = rx.recv() {
                    // Process messages from frontend
                }
            });

            // Expose sender to frontend
            window.app_handle()
                .state::<MessageSender>()
                .set(tx)
                .unwrap();

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Communication Protocol Design

// src/api/tauri.ts
export interface TauriIPC {
  // Synchronous invocation
  invoke<P = any, R = any>(command: string, payload?: P): Promise<R>;

  // Asynchronous event
  listen<T = any>(event: string, handler: (event: TauriEvent<T>) => void): () => void;

  // State management
  emit(event: string, payload?: any): Promise<void>;
}

// Rust command registration
#[tauri::command]
fn fetch_data(params: FetchParams) -> Result<FetchResult, String> {
    // Process request
}

Application Packaging and Distribution

Lightweight Packaging Process

# src-tauri/tauri.conf.json
{
  "build": {
    "beforeBuildCommand": "npm run build",
    "beforeDevCommand": "npm run dev",
    "devPath": "http://localhost:3000",
    "distDir": "../dist"
  },
  "package": {
    "productName": "MyApp",
    "version": "0.1.0"
  }
}

Packaging Script Implementation

// src-tauri/src/build.rs
fn build_app(config: &Config) -> Result<()> {
    // 1. Build frontend resources
    if let Some(build_cmd) = &config.build.before_build_command {
        std::process::Command::new("sh")
            .arg("-c")
            .arg(build_cmd)
            .status()?;
    }

    // 2. Copy resources to packaging directory
    copy_resources(&config)?;

    // 3. Generate platform-specific package
    match config.target() {
        Target::Windows => build_windows(config)?,
        Target::Macos => build_macos(config)?,
        Target::Linux => build_linux(config)?,
    }

    Ok(())
}

Platform-Specific Packaging

// src-tauri/src/platform/windows.rs
fn build_windows(config: &Config) -> Result<()> {
    // 1. Generate NSIS script
    generate_nsis_script(config)?;

    // 2. Call makensis to compile installer
    let status = std::process::Command::new("makensis")
        .arg(&config.nsis_script_path())
        .status()?;

    if !status.success() {
        return Err(Error::BuildFailed);
    }

    Ok(())
}

Security Model

Least Privilege Principle Implementation

// src-tauri/src/api/permission.rs
#[tauri::command]
fn request_permission(permission: PermissionType) -> bool {
    let permissions = get_granted_permissions();

    if permissions.contains(&permission) {
        return true;
    }

    // Display permission request dialog
    let granted = show_permission_dialog(permission);

    if granted {
        grant_permission(permission);
    }

    granted
}

Sandbox Mechanism Design

// src-tauri/src/webview/sandbox.rs
pub struct SandboxConfig {
    pub allow_scripts: bool,
    pub allow_forms: bool,
    pub allow_popups: bool,
    pub allow_same_origin: bool,
}

impl Default for SandboxConfig {
    fn default() -> Self {
        Self {
            allow_scripts: false,
            allow_forms: false,
            allow_popups: false,
            allow_same_origin: false,
        }
    }
}

pub fn apply_sandbox(webview: &WebView<Window>, config: &SandboxConfig) {
    webview.set_sandbox(config);
}

Security Hardening Measures

// src-tauri/src/security.rs
pub fn harden_security(app: &AppHandle) {
    // 1. Disable Node.js integration
    app.config().tauri.security.node_integration = false;

    // 2. Enable context isolation
    app.config().tauri.security.context_isolation = true;

    // 3. Set CSP policy
    app.config().tauri.security.csp = Some(
        "default-src 'self'; script-src 'none'; object-src 'none'"
    );

    // 4. Restrict file system access
    app.config().tauri.allowlist.fs.scope = vec![
        "/tmp/tauri-*".into(),
        "${APPDATA}/myapp".into()
    ];
}

Creating Your First Tauri Application

Creating a Project with create-tauri-app

npx create-tauri-app
  • Interactive Prompts:
    1. Project name: Enter my-tauri-app.
    2. Package manager: Select npm.
    3. UI framework: Choose Vanilla (pure HTML/CSS/JS).
  • Navigate to the project directory:
cd my-tauri-app

Running Your First Application

npm run tauri dev
  • Process:
    • The first run downloads Rust dependencies and compiles.
    • Opens a window displaying the default interface.

Understanding Tauri Project Structure

Project Directory Overview

my-tauri-app/
├── node_modules/         # npm dependencies
├── src/                  # Frontend code
│   ├── index.html
│   ├── styles.css
│   └── main.js
├── src-tauri/            # Rust backend code
│   ├── Cargo.toml
│   ├── tauri.conf.json
│   └── src/
│       └── main.rs
├── package.json          # Project configuration
└── README.md

Core Files and Directories Explained

  • src/:
    • Frontend code directory containing HTML, CSS, and JavaScript.
  • src-tauri/:
    • Rust backend code and configuration.
    • Cargo.toml: Rust project dependency configuration.
    • tauri.conf.json: Tauri application configuration.
    • main.rs: Rust main entry file.
  • package.json:
    • Defines npm scripts and dependencies.

Configuring Tauri’s tauri.conf.json

Structure of tauri.conf.json

{
  "build": {
    "beforeDevCommand": "npm run dev",
    "beforeBuildCommand": "npm run build",
    "devPath": "http://localhost:3000",
    "distDir": "../dist"
  },
  "package": {
    "productName": "My Tauri App",
    "version": "0.1.0"
  },
  "tauri": {
    "allowlist": {
      "all": false,
      "fs": {
        "readFile": true
      }
    },
    "windows": [
      {
        "title": "My Tauri App",
        "width": 800,
        "height": 600
      }
    ],
    "bundle": {
      "identifier": "com.example.mytauriapp"
    }
  }
}

Key Configuration Options Explained

  • build:
    • beforeDevCommand: Command to run before development mode.
    • distDir: Output directory for production builds.
  • package:
    • productName: Application name.
  • tauri:
    • allowlist: Controls APIs accessible to the frontend.
    • windows: Defines window properties.
    • bundle.identifier: Unique application identifier.

Running and Debugging Tauri Applications

Running in Development Mode

npm run tauri dev
  • Features:
    • Starts the frontend development server.
    • Compiles Rust code and opens a window.
    • Supports hot reloading.

Debugging Techniques

  • Frontend Debugging:
    • Right-click the window and select “Inspect” to open developer tools.
  • Rust Debugging:
    • Add logs:
println!("Debug: {:?}", variable);
  • Use VS Code’s Rust debugging plugins:
  • Install rust-analyzer and CodeLLDB extensions.
  • Configure launch.json:
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "lldb",
      "request": "launch",
      "name": "Debug Tauri",
      "program": "${workspaceFolder}/src-tauri/target/debug/my-tauri-app",
      "args": [],
      "cwd": "${workspaceFolder}"
    }
  ]
}

Comprehensive Case Study: Simple Task Manager

Requirements Analysis

Build a task manager that:

  • Allows users to input and add tasks to a list.
  • Displays the task list.
  • Uses Rust to save tasks to a file.

Implementation Code Breakdown

Modify src-tauri/tauri.conf.json

{
  "tauri": {
    "allowlist": {
      "fs": {
        "writeFile": true,
        "readFile": true
      }
    }
  }
}

src-tauri/src/main.rs

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use tauri::api::file::{read_text, write_text};

#[tauri::command]
fn save_tasks(tasks: Vec<String>) -> Result<(), String> {
    write_text("tasks.txt", tasks.join("\n")).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
fn load_tasks() -> Result<Vec<String>, String> {
    match read_text("tasks.txt") {
        Ok(content) => Ok(content.lines().map(String::from).collect()),
        Err(_) => Ok(vec![]), // Return empty list if file doesn't exist
    }
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![save_tasks, load_tasks])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

src/main.js

const { invoke } = window.__TAURI__.tauri;

let tasks = [];

async function loadTasks() {
    tasks = await invoke('load_tasks');
    updateTaskList();
}

async function saveTasks() {
    await invoke('save_tasks', { tasks });
}

function updateTaskList() {
    const taskList = document.getElementById('taskList');
    taskList.innerHTML = tasks.map(task => `<li>${task}</li>`).join('');
}

document.getElementById('addButton').addEventListener('click', () => {
    const taskInput = document.getElementById('taskInput');
    const task = taskInput.value.trim();
    if (task) {
        tasks.push(task);
        taskInput.value = '';
        saveTasks();
        updateTaskList();
    }
});

window.addEventListener('DOMContentLoaded', loadTasks);

src/index.html

<!DOCTYPE html>
<html>
<head>
  <title>Task Manager</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <h1>Task Manager</h1>
  <input id="taskInput" placeholder="Enter task">
  <button id="addButton">Add</button>
  <ul id="taskList"></ul>
  <script src="main.js"></script>
</body>
</html>

src/styles.css

body { font-family: Arial, sans-serif; margin: 20px; }
ul { list-style: none; padding: 0; }
li { padding: 10px; background-color: #f0f0f0; margin-bottom: 5px; }
input { padding: 8px; width: 200px; }
button { padding: 8px 16px; background-color: #007bff; color: white; border: none; }

Run the Application

npm run tauri dev

Analysis

  • Rust Backend: Handles saving and loading tasks.
  • Frontend: Displays tasks and interacts with the Rust backend.
Share your love