Tauri

Rustバックエンドとウェブフロントエンドを組み合わせた次世代デスクトップフレームワーク。Electronより大幅に軽量(2.5-3MB)で高いセキュリティを実現。任意のフロントエンドフレームワークとの統合が可能。

デスクトップクロスプラットフォームRustWebView軽量

GitHub概要

tauri-apps/tauri

Build smaller, faster, and more secure desktop and mobile applications with a web frontend.

ホームページ:https://tauri.app
スター94,586
ウォッチ525
フォーク2,967
作成日:2019年7月13日
言語:Rust
ライセンス:Apache License 2.0

トピックス

desktop-apphigh-performancemobile-appnative-apprustweb-frontendwebview

スター履歴

tauri-apps/tauri Star History
データ取得日時: 2025/7/16 12:20

フレームワーク

Tauri

概要

TauriはRustで構築された軽量なデスクトップアプリ開発フレームワークです。OSネイティブのWebViewを使用し、Electronと比較して90%のメモリ削減と75%の軽量化を実現します。

詳細

Tauri(タウリ)は、Rust言語で開発された次世代のデスクトップアプリケーションフレームワークです。2025年にGitHub 7万スター超えで急成長しており、軽量・高性能・セキュアなデスクトップアプリ開発の新選択肢として注目を集めています。

TauriはElectronとは異なるアプローチを採用し、Chromiumを内包する代わりにOSネイティブのWebView(Windows: WebView2、macOS: WKWebView、Linux: WebKitGTK)を使用します。これにより、アプリケーションサイズの大幅な削減とメモリ使用量の最適化を実現しています。

フロントエンドには任意のWebフレームワーク(React、Vue、Angular、Svelteなど)を使用でき、バックエンドはRustで記述されます。Rustの安全性と性能を活かしつつ、Web技術によるUIを組み合わせることで、高性能でセキュアなデスクトップアプリケーションの開発が可能です。

メリット・デメリット

メリット

  • 軽量・高性能: Electronより90%メモリ削減、75%サイズ削減
  • OSネイティブ: プラットフォーム固有のWebViewを使用
  • セキュリティ重視: Rustの安全性とサンドボックス化
  • 柔軟なフロントエンド: React、Vue、Angular、Svelteなど自由選択
  • 高速起動: ネイティブアプリに近い起動時間
  • 小さいバンドル: 配布パッケージサイズが大幅に小さい
  • 急成長コミュニティ: 活発な開発とコミュニティサポート

デメリット

  • Rust学習コスト: バックエンド開発にRust知識が必要
  • 比較的新しい: Electronほど成熟していない生態系
  • WebView制約: プラットフォームのWebView実装に依存
  • プラットフォーム差異: WebViewの違いによる動作差異の可能性
  • デバッグの複雑さ: Rust・Web技術両方の知識が必要

主要リンク

書き方の例

基本的なTauriアプリ

// src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use tauri::Manager;

#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

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

Tauri設定ファイル

// src-tauri/tauri.conf.json
{
  "build": {
    "beforeBuildCommand": "npm run build",
    "beforeDevCommand": "npm run dev",
    "devPath": "http://localhost:3000",
    "distDir": "../dist"
  },
  "package": {
    "productName": "my-tauri-app",
    "version": "0.1.0"
  },
  "tauri": {
    "allowlist": {
      "all": false,
      "fs": {
        "all": true
      },
      "dialog": {
        "all": true
      }
    },
    "bundle": {
      "active": true,
      "targets": "all",
      "identifier": "com.tauri.dev",
      "icon": [
        "icons/32x32.png",
        "icons/128x128.png",
        "icons/icon.ico"
      ]
    },
    "security": {
      "csp": null
    },
    "windows": [
      {
        "fullscreen": false,
        "height": 600,
        "resizable": true,
        "title": "my-tauri-app",
        "width": 800
      }
    ]
  }
}

フロントエンド(React例)

// src/App.tsx
import { useState } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import "./App.css";

function App() {
  const [greetMsg, setGreetMsg] = useState("");
  const [name, setName] = useState("");

  async function greet() {
    setGreetMsg(await invoke("greet", { name }));
  }

  return (
    <div className="container">
      <h1>Welcome to Tauri!</h1>

      <div className="row">
        <form
          onSubmit={(e) => {
            e.preventDefault();
            greet();
          }}
        >
          <input
            id="greet-input"
            onChange={(e) => setName(e.currentTarget.value)}
            placeholder="Enter a name..."
          />
          <button type="submit">Greet</button>
        </form>
      </div>

      <p>{greetMsg}</p>
    </div>
  );
}

export default App;

Rustコマンドの定義

// src-tauri/src/main.rs
use std::fs;
use tauri::State;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Person {
    name: String,
    age: u32,
}

#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
    fs::read_to_string(path).map_err(|e| e.to_string())
}

#[tauri::command]
fn write_file(path: String, content: String) -> Result<(), String> {
    fs::write(path, content).map_err(|e| e.to_string())
}

#[tauri::command]
fn get_person_info(name: String, age: u32) -> Person {
    Person { name, age }
}

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

ファイル操作のフロントエンド

// src/FileManager.tsx
import { useState } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import { open, save } from "@tauri-apps/api/dialog";

function FileManager() {
  const [fileContent, setFileContent] = useState("");
  const [currentFile, setCurrentFile] = useState<string | null>(null);

  const openFile = async () => {
    try {
      const filePath = await open({
        filters: [
          { name: "Text", extensions: ["txt"] },
          { name: "All", extensions: ["*"] }
        ]
      });
      
      if (filePath) {
        const content = await invoke<string>("read_file", { 
          path: filePath 
        });
        setFileContent(content);
        setCurrentFile(filePath as string);
      }
    } catch (error) {
      console.error("Error opening file:", error);
    }
  };

  const saveFile = async () => {
    try {
      let filePath = currentFile;
      
      if (!filePath) {
        filePath = await save({
          filters: [
            { name: "Text", extensions: ["txt"] }
          ]
        });
      }
      
      if (filePath) {
        await invoke("write_file", { 
          path: filePath, 
          content: fileContent 
        });
        setCurrentFile(filePath);
      }
    } catch (error) {
      console.error("Error saving file:", error);
    }
  };

  return (
    <div>
      <div>
        <button onClick={openFile}>ファイルを開く</button>
        <button onClick={saveFile}>保存</button>
      </div>
      
      <textarea
        value={fileContent}
        onChange={(e) => setFileContent(e.target.value)}
        placeholder="ファイル内容..."
        rows={20}
        cols={80}
      />
      
      {currentFile && <p>現在のファイル: {currentFile}</p>}
    </div>
  );
}

export default FileManager;

状態管理とイベント

// src-tauri/src/main.rs
use std::sync::Mutex;
use tauri::{Manager, State};

struct AppState {
    counter: Mutex<i32>,
}

#[tauri::command]
fn increment_counter(state: State<AppState>) -> i32 {
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    *counter
}

#[tauri::command]
fn get_counter(state: State<AppState>) -> i32 {
    *state.counter.lock().unwrap()
}

#[tauri::command]
fn emit_custom_event(app: tauri::AppHandle, message: String) {
    app.emit_all("custom-event", message).unwrap();
}

fn main() {
    tauri::Builder::default()
        .manage(AppState {
            counter: Mutex::new(0),
        })
        .invoke_handler(tauri::generate_handler![
            increment_counter,
            get_counter,
            emit_custom_event
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Cargo.tomlの設定

# src-tauri/Cargo.toml
[package]
name = "my-tauri-app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"

[build-dependencies]
tauri-build = { version = "1.0", features = [] }

[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0", features = ["api-all"] }

[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]

プロジェクト作成と実行

# Tauri CLIのインストール
npm install -g @tauri-apps/cli
# または
cargo install tauri-cli

# 新しいTauriプロジェクトの作成
npm create tauri-app
# または
yarn create tauri-app
# または
pnpm create tauri-app

# プロジェクトディレクトリに移動
cd tauri-app

# 依存関係のインストール
npm install

# 開発サーバーの起動
npm run tauri dev

# プロダクションビルド
npm run tauri build

# Rustのみでの実行
cd src-tauri
cargo run

# Rustでリリースビルド
cargo run --release