Разработка десктоп-приложения на Tauri
Tauri — фреймворк для десктоп-приложений, где бэкенд написан на Rust, а фронтенд — любой веб-стек. В отличие от Electron, Tauri не поставляет собственный Chromium: используется системный WebView (WebKit на macOS/Linux, WebView2 на Windows). Результат — дистрибутив от 4 до 15 МБ против 80+ МБ у Electron.
Архитектура
Frontend (HTML/CSS/JS → любой фреймворк)
↕ invoke() / events
Tauri Core (Rust)
↕ системные вызовы
ОС (файловая система, нотификации, трей)
Весь небезопасный код выполняется в Rust. Фронтенд может вызывать только явно экспонированные Rust-команды через invoke.
Создание проекта
# Предварительно: Rust + системные зависимости
# macOS: xcode-select --install
# Windows: Visual Studio Build Tools + WebView2
# Linux: webkit2gtk, libayatana-appindicator
npm create tauri-app@latest
# Выбираем: TypeScript, React, Vite
Структура проекта:
my-app/
├── src/ # React/Vue/Svelte фронтенд
├── src-tauri/
│ ├── src/
│ │ ├── main.rs # точка входа Rust
│ │ └── lib.rs # команды и плагины
│ ├── Cargo.toml
│ └── tauri.conf.json
├── package.json
└── vite.config.ts
Rust-команды: основа взаимодействия
// src-tauri/src/lib.rs
use tauri::State;
use std::sync::Mutex;
struct AppState {
counter: Mutex<i32>,
}
// Простая команда
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// Асинхронная команда с состоянием
#[tauri::command]
async fn increment(state: State<'_, AppState>) -> Result<i32, String> {
let mut counter = state.counter.lock().map_err(|e| e.to_string())?;
*counter += 1;
Ok(*counter)
}
// Команда с доступом к файловой системе
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
std::fs::read_to_string(&path).map_err(|e| e.to_string())
}
// Команда с app_handle для работы с окнами
#[tauri::command]
async fn open_child_window(app: tauri::AppHandle) -> Result<(), String> {
tauri::WebviewWindowBuilder::new(&app, "child", tauri::WebviewUrl::App("child.html".into()))
.title("Child Window")
.inner_size(400.0, 300.0)
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
pub fn run() {
tauri::Builder::default()
.manage(AppState { counter: Mutex::new(0) })
.invoke_handler(tauri::generate_handler![greet, increment, read_file, open_child_window])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Вызов Rust-команд из фронтенда
// src/api/tauri.ts
import { invoke } from '@tauri-apps/api/core';
export async function greet(name: string): Promise<string> {
return invoke('greet', { name });
}
export async function increment(): Promise<number> {
return invoke('increment');
}
export async function readFile(path: string): Promise<string> {
return invoke('read_file', { path });
}
// Использование в React компоненте
function App() {
const [count, setCount] = useState(0);
const handleIncrement = async () => {
const newCount = await increment();
setCount(newCount);
};
return <button onClick={handleIncrement}>Count: {count}</button>;
}
Система событий
// Rust → Frontend
use tauri::{Manager, Emitter};
#[tauri::command]
async fn start_long_task(app: tauri::AppHandle) {
tokio::spawn(async move {
for i in 0..=100 {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
app.emit("progress", i).unwrap();
}
app.emit("task-complete", "done").unwrap();
});
}
// Frontend — подписка на события
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen<number>('progress', (event) => {
setProgress(event.payload);
});
const unlistenComplete = await listen('task-complete', () => {
setDone(true);
unlisten();
unlistenComplete();
});
await invoke('start_long_task');
Плагины экосистемы
Tauri имеет официальные плагины для типичных задач:
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"
tauri-plugin-notification = "2"
tauri-plugin-shell = "2"
tauri-plugin-store = "2"
tauri-plugin-updater = "2"
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
// src-tauri/src/lib.rs
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_store::Builder::default().build())
.invoke_handler(tauri::generate_handler![...])
.run(tauri::generate_context!())
.expect("error running app");
}
// Frontend — использование плагина store
import { Store } from '@tauri-apps/plugin-store';
const store = await Store.load('settings.json');
await store.set('theme', 'dark');
await store.save();
const theme = await store.get<string>('theme');
Конфигурация разрешений (Capability система)
В Tauri 2.x доступ к API ограничен через capabilities — нужно явно объявить, что разрешено:
// src-tauri/capabilities/default.json
{
"identifier": "default",
"description": "Default capabilities",
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read-text-file",
"fs:allow-write-text-file",
"fs:allow-app-read-recursive",
"dialog:allow-open",
"dialog:allow-save",
"notification:allow-notify"
]
}
Это значит фронтенд не может обратиться к fs если нет разрешения — даже если API доступно в JS.
Сборка и дистрибуция
# Разработка
npm run tauri dev
# Сборка под текущую платформу
npm run tauri build
# Кросс-компиляция через GitHub Actions
# Нужно запускать build на каждой целевой платформе
tauri build производит:
- macOS:
.app+.dmg - Windows:
.exe(NSIS) +.msi - Linux:
.deb+.AppImage+.rpm
Когда выбирать Tauri вместо Electron
Tauri оправдан когда: размер дистрибутива критичен, нужна максимальная производительность нативного кода, команда знает Rust или готова его освоить.
Electron оправдан когда: нужна одинаковая среда выполнения без зависимости от системного WebView, команда чисто JS/TS, приложение активно использует экосистему npm-пакетов в основном процессе.
Разница в размере: Tauri ~8 МБ, Electron ~120 МБ. Разница во времени рендеринга: незначительна для типичных UI-задач. Разница в потреблении памяти: Tauri ~30-60 МБ vs Electron 100-200 МБ при старте.







