Tauri用Rust替代Electron的Node.js后端,配合SvelteKit的轻量前端,可以构建体积小、性能好的桌面应用。本文从项目搭建到SQLite持久化再到打包发布,走完全流程。
为什么选Tauri + SvelteKit
Tauri vs Electron:
- 打包体积:Tauri应用通常3-10MB,Electron动辄100MB+
- 内存占用:Tauri使用系统WebView,不内嵌Chromium
- 后端语言:Rust vs Node.js,性能和安全性优势明显
- 系统集成:Tauri的Rust后端可以直接调用系统API
SvelteKit vs React/Vue:
- 编译时框架,运行时几乎零开销
- 原生支持SSG/SPA模式,适合Tauri
- 写起来更简洁,模板语法直观
- 内置路由、数据加载等
项目搭建
# 创建SvelteKit项目
npm create svelte@latest my-tauri-app
cd my-tauri-app
npm install
# 添加Tauri
npm install -D @tauri-apps/cli@latest
npx tauri init
tauri init会问几个问题:
- App name:
my-tauri-app - Window title:
My App - Dev server URL:
http://localhost:5173(SvelteKit默认端口) - Dev command:
npm run dev - Build command:
npm run build
生成的项目结构:
my-tauri-app/
├── src/ # SvelteKit前端
│ ├── routes/
│ │ ├── +layout.svelte
│ │ └── +page.svelte
│ └── lib/
├── src-tauri/ # Rust后端
│ ├── src/
│ │ └── main.rs
│ ├── Cargo.toml
│ └── tauri.conf.json
├── package.json
└── svelte.config.js
SvelteKit需要配置为SPA模式(Tauri不需要服务端渲染):
npm install -D @sveltejs/adapter-static
// svelte.config.js
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter({
fallback: 'index.html' // SPA fallback
})
}
};
<!-- src/noutes/+layout.ts -->
export const prerender = true;
export const ssr = false;
Rust命令:前后端通信
Tauri的核心通信机制是Command——前端通过invoke调用Rust函数。
定义Rust命令
// src-tauri/src/main.rs
use tauri::command;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Note {
id: Option<i64>,
title: String,
content: String,
created_at: String,
}
#[command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[command]
async fn create_note(title: String, content: String) -> Result<Note, String> {
// 后面会接入SQLite
Ok(Note {
id: Some(1),
title,
content,
created_at: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
})
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
greet,
create_note,
])
.run(tauri::generate_context!())
.expect("error running tauri application");
}
前端调用
<!-- src/noutes/+page.svelte -->
<script>
import { invoke } from '@tauri-apps/api/core';
let name = '';
let greeting = '';
let noteTitle = '';
let noteContent = '';
async function handleGreet() {
greeting = await invoke('greet', { name });
}
async function handleCreateNote() {
try {
const note = await invoke('create_note', {
title: noteTitle,
content: noteContent
});
console.log('Created note:', note);
} catch (err) {
console.error('Failed to create note:', err);
}
}
</script>
<main>
<input bind:value={name} placeholder="Enter a name" />
<button on:click={handleGreet}>Greet</button>
<p>{greeting}</p>
<h2>New Note</h2>
<input bind:value={noteTitle} placeholder="Title" />
<textarea bind:value={noteContent} placeholder="Content"></textarea>
<button on:click={handleCreateNote}>Save</button>
</main>
invoke是类型安全的异步调用。参数名必须和Rust函数参数名一致(name、title、content)。
SQLite持久化
桌面应用需要本地数据存储,SQLite是最佳选择。Tauri生态有官方的SQLite插件,但直接用rusqlite更灵活。
# src-tauri/Cargo.toml
[dependencies]
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rusqlite = { version = "0.31", features = ["bundled"] }
chrono = "0.4"
数据库管理模块:
// src-tauri/src/db.rs
use rusqlite::{Connection, Result, params};
use std::sync::Mutex;
use crate::Note;
pub struct Database {
conn: Mutex<Connection>,
}
impl Database {
pub fn new(db_path: &str) -> Result<Self> {
let conn = Connection::open(db_path)?;
conn.execute_batch("
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(created_at DESC);
")?;
Ok(Database { conn: Mutex::new(conn) })
}
pub fn create_note(&self, title: &str, content: &str) -> Result<Note> {
let conn = self.conn.lock().unwrap();
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
conn.execute(
"INSERT INTO notes (title, content, created_at, updated_at) VALUES (?1, ?2, ?3, ?3)",
params![title, content, now],
)?;
let id = conn.last_insert_rowid();
Ok(Note {
id: Some(id),
title: title.to_string(),
content: content.to_string(),
created_at: now,
})
}
pub fn list_notes(&self) -> Result<Vec<Note>> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, title, content, created_at FROM notes ORDER BY created_at DESC"
)?;
let notes = stmt.query_map([], |row| {
Ok(Note {
id: Some(row.get(0)?),
title: row.get(1)?,
content: row.get(2)?,
created_at: row.get(3)?,
})
})?.collect::<Result<Vec<_>>>()?;
Ok(notes)
}
pub fn delete_note(&self, id: i64) -> Result<bool> {
let conn = self.conn.lock().unwrap();
let affected = conn.execute("DELETE FROM notes WHERE id = ?1", params![id])?;
Ok(affected > 0)
}
}
在Tauri中注册为State:
// src-tauri/src/main.rs
mod db;
use db::Database;
use tauri::State;
#[command]
async fn create_note(
db: State<'_, Database>,
title: String,
content: String
) -> Result<Note, String> {
db.create_note(&title, &content).map_err(|e| e.to_string())
}
#[command]
async fn list_notes(db: State<'_, Database>) -> Result<Vec<Note>, String> {
db.list_notes().map_err(|e| e.to_string())
}
#[command]
async fn delete_note(db: State<'_, Database>, id: i64) -> Result<bool, String> {
db.delete_note(id).map_err(|e| e.to_string())
}
fn main() {
let db = Database::new("app_data.db").expect("Failed to init database");
tauri::Builder::default()
.manage(db) // 注册为全局State
.invoke_handler(tauri::generate_handler![
create_note,
list_notes,
delete_note,
])
.run(tauri::generate_context!())
.expect("error running tauri application");
}
前端列表页:
<!-- src/noutes/+page.svelte -->
<script>
import { invoke } from '@tauri-apps/api/core';
import { onMount } from 'svelte';
let notes = [];
let title = '';
let content = '';
onMount(async () => {
notes = await invoke('list_notes');
});
async function addNote() {
if (!title.trim()) return;
const note = await invoke('create_note', { title, content });
notes = [note, ...notes];
title = '';
content = '';
}
async function removeNote(id) {
await invoke('delete_note', { id });
notes = notes.filter(n => n.id !== id);
}
</script>
<div class="container">
<h1>Notes</h1>
<form on:submit|preventDefault={addNote}>
<input bind:value={title} placeholder="Title" required />
<textarea bind:value={content} placeholder="Write something..." />
<button type="submit">Add Note</button>
</form>
<div class="notes-list">
{#each notes as note (note.id)}
<div class="note-card">
<h3>{note.title}</h3>
<p>{note.content}</p>
<small>{note.created_at}</small>
<button on:click={() => removeNote(note.id)}>Delete</button>
</div>
{/each}
</div>
</div>
打包与分发
# 开发模式
npm run tauri dev
# 打包
npm run tauri build
打包产物在src-tauri/target/nelease/bundle/下:
- Windows:
.msi安装包和.exe(NSIS安装器) - macOS:
.dmg和.app - Linux:
.deb、.AppImage
打包配置在tauri.conf.json中:
{
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.example.my-tauri-app",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
最终的Windows安装包大约4MB——同样功能的Electron应用至少80MB。
小结
Tauri + SvelteKit是2025年构建桌面应用的一个很好的技术组合。Tauri提供了轻量、安全的桌面运行时,SvelteKit负责高效的UI层,Rust后端处理业务逻辑和数据存储。学习曲线主要在Rust——如果你还不熟悉Rust,可以先从简单的Command开始,逐步深入。生态在快速成熟中,官方插件覆盖了文件系统、HTTP请求、通知等常见需求。