前言:为什么选择Bun+Tauri
作为一个经常需要开发桌面工具的开发者,我一直在寻找更好的技术组合。Electron虽然生态成熟,但动辄上百MB的应用体积让人头疼;Node.js虽然稳定,但启动速度和数据处理效率总是差强人意。直到我接触到Bun和Tauri这对组合,才发现桌面应用开发原来可以这么高效。
Bun是Zig语言编写的JavaScript运行时,2026年已经成为前端开发者的新宠。它的启动速度比Node.js快4倍,TypeScript执行无需额外转译,内置的SQLite支持让本地数据处理变得轻而易举。更重要的是,Bun与现代Web标准完全兼容,你可以直接用它运行Express、Fastify等主流框架的代码。
Tauri则用Rust编写后端逻辑,相比Electron的Chromium方案,生成的应用体积可以小10倍以上。它不像Electron那样内置完整的Chromium浏览器,而是通过系统WebView渲染页面,这意味着一个功能完整的中型应用打包后可能只有几MB。更妙的是,Tauri原生支持调用系统API和本地进程,这意味着你可以轻松实现文件操作、子进程管理这些Electron里需要额外配置的功能。
这两者的结合,本质上是“轻量前端运行时+Rust高性能后端”的完美组合。对于需要本地AI能力的桌面应用来说,这种架构既能保证前端交互的流畅性,又能利用Rust的高效计算能力处理AI模型的预处理和后处理工作。
在实际开发中,我发现这套组合特别适合开发内部工具、数据处理软件、AI助手类应用。尤其是需要调用本地AI模型(如Ollama部署的Llama、Qwen等开源模型)的场景,Tauri的Rust后端可以高效管理模型生命周期,Bun的前端则能快速构建现代化的用户界面。

一、开发环境配置
在开始之前,我们需要配置好开发环境。整个过程在Windows、macOS、Linux三大平台都是一致的,这也是Bun和Tauri的共同优势。
1.1 安装Bun
Bun的安装非常简单,打开终端执行一条命令即可。以macOS和Linux为例:
bash
curl -fsSL https://bun.sh/install | bash
Windows用户可以使用PowerShell:
powershell
powershell -c "irm bun.sh/install.ps1 | iex"
安装完成后,验证一下版本:
bash
bun --version
# 应该输出类似 1.2.15 这样的版本号
Bun自带了包管理器,npm、yarn、pnpm的常用命令它都支持。你可以直接用bun add安装依赖,用bun run执行脚本,甚至用bun test运行测试。这对于习惯了Node.js生态的开发者来说几乎没有学习成本。
1.2 安装Rust和Tauri CLI
Tauri基于Rust,所以需要先安装Rust工具链。在各平台上安装方式统一:
bash
# macOS和Linux
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Windows下载 rustup-init.exe 运行即可
安装完成后,终端输入rustc --version确认安装成功。然后安装Tauri CLI:
bash
cargo install tauri-cli
或者如果你更喜欢通过npm管理工具链,可以用Bun安装Tauri的JavaScript绑定:
bash
bun add -D @tauri-apps/cli
安装完成后,在项目目录下运行bunx tauri init即可初始化Tauri项目。
1.3 安装Ollama(本地AI支持)
为了在桌面应用中集成AI能力,我们使用Ollama运行本地开源模型。先下载安装Ollama:
bash
# macOS和Linux
curl -fsSL https://ollama.com/install.sh | sh
# Windows从 https://ollama.com/download 下载安装
安装完成后,拉取一个轻量级模型试试水:
bash
ollama pull llama3.2:3b
这个3B参数的模型大小约2GB,在大多数电脑上都能流畅运行,适合开发和测试。后续可以根据需要换成qwen2.5、deepseek等国产模型。
二、项目初始化与目录结构
2.1 创建项目
用Bun创建Vite项目作为前端,Tauri作为桌面包装:
bash
# 创建前端项目
bun create vite ai-desktop-app --template vanilla-ts
cd ai-desktop-app
# 安装依赖
bun install
# 添加Tauri
bun add -D @tauri-apps/cli @tauri-apps/api
然后初始化Tauri配置:
bash
bunx tauri init
初始化过程中会询问几个问题,按如下配置回答:
yaml
App name: AI Desktop Assistant
Window title: AI Desktop Assistant
Dev server port: 5173
Frontend dist: ../dist
2.2 目录结构
项目初始化完成后,目录结构如下:
plaintext
ai-desktop-app/
├── src/ # 前端源代码(Bun/Vite/TypeScript)
│ ├── main.ts # 前端入口
│ ├── styles.css # 样式文件
│ └── app.ts # 应用主逻辑
├── src-tauri/ # Tauri后端(Rust)
│ ├── src/
│ │ └── main.rs # Rust入口
│ ├── Cargo.toml # Rust依赖
│ └── tauri.conf.json # Tauri配置
├── public/ # 静态资源
├── index.html # HTML入口
├── package.json # npm配置
├── tsconfig.json # TypeScript配置
├── vite.config.ts # Vite配置
└── SPEC.md # 项目规范
这种前后端分离的目录结构很清晰。前端用Bun+Vite+TypeScript开发,后端Rust代码在src-tauri目录下,通过Tauri的IPC机制与前端通信。
三、核心功能实现
3.1 前端界面设计
先来实现用户界面。打开src/app.ts,编写聊天界面的核心逻辑:
typescript
interface Message {
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
class AIChatApp {
private messages: Message[] = [];
private isLoading = false;
private messageContainer: HTMLElement;
private userInput: HTMLTextAreaElement;
private sendButton: HTMLButtonElement;
constructor() {
this.messageContainer = document.getElementById('messages')!;
this.userInput = document.getElementById('user-input') as HTMLTextAreaElement;
this.sendButton = document.getElementById('send-btn') as HTMLButtonElement;
this.init();
}
private init() {
this.sendButton.addEventListener('click', () => this.sendMessage());
this.userInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
// 加载历史记录
this.loadHistory();
}
private async sendMessage() {
const content = this.userInput.value.trim();
if (!content || this.isLoading) return;
this.userInput.value = '';
this.isLoading = true;
this.addMessage('user', content);
try {
const response = await this.callAI(content);
this.addMessage('assistant', response);
} catch (error) {
this.addMessage('assistant', `出错了: ${error}`);
} finally {
this.isLoading = false;
}
}
private async callAI(userMessage: string): Promise<string> {
// 调用Tauri后端命令
const { invoke } = await import('@tauri-apps/api/core');
try {
const response = await invoke<string>('chat_with_ai', {
message: userMessage
});
return response;
} catch (error) {
// 如果后端未配置,降级到直接API调用
console.warn('后端调用失败,尝试直接API:', error);
return await this.fallbackChat(userMessage);
}
}
private async fallbackChat(message: string): Promise<string> {
// Ollama本地API调用
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'llama3.2:3b',
messages: [
{ role: 'user', content: message }
],
stream: false
})
});
const data = await response.json();
return data.message.content;
}
private addMessage(role: 'user' | 'assistant', content: string) {
const message: Message = {
role,
content,
timestamp: new Date()
};
this.messages.push(message);
const div = document.createElement('div');
div.className = `message ${role}`;
div.innerHTML = `
<div class="avatar">${role === 'user' ? '👤' : '🤖'}</div>
<div class="content">
<div class="text">${this.escapeHtml(content)}</div>
<div class="time">${message.timestamp.toLocaleTimeString()}</div>
</div>
`;
this.messageContainer.appendChild(div);
div.scrollIntoView({ behavior: 'smooth' });
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
private loadHistory() {
// 从localStorage加载历史记录
const saved = localStorage.getItem('chat-history');
if (saved) {
const history: Message[] = JSON.parse(saved);
history.forEach(msg => this.addMessage(msg.role, msg.content));
}
}
saveHistory() {
localStorage.setItem('chat-history', JSON.stringify(this.messages.slice(-50)));
}
}
// 启动应用
document.addEventListener('DOMContentLoaded', () => {
const app = new AIChatApp();
window.addEventListener('beforeunload', () => app.saveHistory());
});
这段代码实现了一个完整的聊天界面,包含消息发送、AI响应、历史记录持久化等功能。关键点是通过Tauri的invoke函数调用Rust后端,如果后端未配置则降级到直接调用Ollama API。
3.2 Rust后端实现
现在来实现Tauri的Rust后端。打开src-tauri/src/main.rs:
rust
use tauri::State;
use std::sync::Mutex;
struct AppState {
chat_history: Mutex<Vec<ChatMessage>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct ChatMessage {
role: String,
content: String,
}
#[tauri::command]
async fn chat_with_ai(
message: String,
state: State<'_, AppState>,
) -> Result<String, String> {
// 添加用户消息到历史
{
let mut history = state.chat_history.lock().map_err(|e| e.to_string())?;
history.push(ChatMessage {
role: "user".to_string(),
content: message.clone(),
});
}
// 调用Ollama API
let client = reqwest::Client::new();
let response = client
.post("http://localhost:11434/api/chat")
.json(&serde_json::json!({
"model": "llama3.2:3b",
"messages": state.chat_history.lock()
.map_err(|e| e.to_string())?
.iter()
.map(|m| serde_json::json!({
"role": m.role,
"content": m.content
}))
.collect::<Vec<_>>(),
"stream": false
}))
.send()
.await
.map_err(|e| e.to_string())?;
let data: serde_json::Value = response
.json()
.await
.map_err(|e| e.to_string())?;
let assistant_message = data["message"]["content"]
.as_str()
.unwrap_or("无法获取回复")
.to_string();
// 保存助手回复
{
let mut history = state.chat_history.lock().map_err(|e| e.to_string())?;
history.push(ChatMessage {
role: "assistant".to_string(),
content: assistant_message.clone(),
});
}
Ok(assistant_message)
}
#[tauri::command]
fn clear_history(state: State<'_, AppState>) -> Result<(), String> {
let mut history = state.chat_history.lock().map_err(|e| e.to_string())?;
history.clear();
Ok(())
}
#[tauri::command]
async fn check_ollama_status() -> Result<bool, String> {
let client = reqwest::Client::new();
match client
.get("http://localhost:11434/api/tags")
.timeout(std::time::Duration::from_secs(3))
.send()
.await
{
Ok(resp) => Ok(resp.status().is_success()),
Err(_) => Ok(false),
}
}
fn main() {
tauri::Builder::default()
.manage(AppState {
chat_history: Mutex::new(Vec::new()),
})
.invoke_handler(tauri::generate_handler![
chat_with_ai,
clear_history,
check_ollama_status
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Rust后端主要负责维护聊天历史和与Ollama的通信。注意这里我们用了reqwest库来发送HTTP请求,需要在Cargo.toml中添加依赖。
3.3 配置Rust依赖
编辑src-tauri/Cargo.toml:
toml
[package]
name = "ai-desktop-app"
version = "0.1.0"
edition = "2021"
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
配置devtools可以在开发时调试前端,打开src-tauri/tauri.conf.json:
json
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "AI Desktop Assistant",
"identifier": "com.aidesktop.app",
"version": "0.1.0",
"build": {
"devtools": true,
"beforeDevCommand": "bun run dev",
"devUrl": "http://localhost:5173",
"beforeBuildCommand": "bun run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "AI Desktop Assistant",
"width": 900,
"height": 700,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
3.4 样式美化
为了让界面更专业,添加一些样式到src/styles.css:
css
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-tertiary: #0f3460;
--text-primary: #e8e8e8;
--text-secondary: #a0a0a0;
--accent: #e94560;
--user-bubble: #0f3460;
--assistant-bubble: #16213e;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: var(--bg-secondary);
padding: 16px 24px;
border-bottom: 1px solid var(--bg-tertiary);
display: flex;
align-items: center;
gap: 12px;
}
header h1 {
font-size: 18px;
font-weight: 600;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: #4caf50;
}
.status-indicator.offline {
background: #f44336;
}
#messages {
flex: 1;
overflow-y: auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
gap: 12px;
max-width: 80%;
}
.message.user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message .avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.message .content {
background: var(--assistant-bubble);
padding: 12px 16px;
border-radius: 12px;
}
.message.user .content {
background: var(--user-bubble);
}
.message .time {
font-size: 11px;
color: var(--text-secondary);
margin-top: 4px;
}
.input-area {
background: var(--bg-secondary);
padding: 16px 24px;
border-top: 1px solid var(--bg-tertiary);
display: flex;
gap: 12px;
}
#user-input {
flex: 1;
background: var(--bg-primary);
border: 1px solid var(--bg-tertiary);
border-radius: 8px;
padding: 12px 16px;
color: var(--text-primary);
font-size: 14px;
resize: none;
min-height: 48px;
max-height: 120px;
}
#user-input:focus {
outline: none;
border-color: var(--accent);
}
#send-btn {
background: var(--accent);
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
#send-btn:hover {
opacity: 0.9;
}
#send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
这个深色主题的样式设计兼顾了美观和实用性,在长时间使用时也不会造成视觉疲劳。
四、开发调试与问题排查
4.1 启动开发服务器
在项目根目录执行:
bash
bun run tauri dev
这条命令会同时启动Vite开发服务器和Tauri应用。第一次启动会下载Rust依赖,可能需要几分钟时间。后续开发时,这个过程会快很多,因为依赖已经缓存好了。
如果遇到问题,可以分步调试:
bash
# 终端1:启动前端
bun run dev
# 终端2:启动Tauri(不自动打开窗口)
bunx tauri dev --no-watch
4.2 常见问题处理
Ollama连接失败:确保Ollama服务正在运行。Windows上可以在系统托盘查看,Linux/macOS上执行ollama serve。
模型未找到:先下载模型ollama pull llama3.2:3b,确认ollama list能看到已安装的模型。
Rust编译错误:执行rustup update更新Rust工具链,然后重新运行。
端口占用:如果5173或11434端口被占用,修改tauri.conf.json中的端口配置,或查找并关闭占用进程。
4.3 性能监控
Tauri应用可以通过内置的日志功能查看性能数据。在Rust代码中添加日志:
rust
use log::{info, error};
#[tauri::command]
async fn chat_with_ai(...) -> Result<String, String> {
let start = std::time::Instant::now();
info!("开始处理用户请求");
// ... 处理逻辑
info!("请求处理完成,耗时: {:?}", start.elapsed());
Ok(response)
}
运行bunx tauri dev时,日志会输出到终端。
五、打包与发布
5.1 生成应用图标
Tauri需要特定格式的图标才能打包。在src-tauri/icons/目录下放置以下文件:
32x32.png– 小尺寸图标128x128.png– 中等尺寸图标128x128@2x.png– Retina显示图标icon.icns– macOS专用icon.ico– Windows专用
如果没有现成的图标,可以用在线工具(如CloudConvert)将PNG转换为所需格式。
5.2 构建生产版本
bash
bun run tauri build
构建过程大约需要5-10分钟,取决于电脑性能和首次构建。完成后,在src-tauri/target/release/目录下会生成可执行文件:
- Windows:
.exe文件 - macOS:
.app文件 - Linux: 可执行文件或
.deb、.AppImage包
5.3 安装与分发
Windows:双击生成的.exe或.msi安装包,按提示完成安装。也可以将.exe文件打包成ZIP分发。
macOS:将.app拖入Applications文件夹即可。如果提示”无法打开,因为来自未验证的开发者”,需要在”系统偏好设置 > 安全性与隐私”中允许。
Linux:Ubuntu/Debian系统执行sudo dpkg -i xxx.deb安装,或直接运行.AppImage文件并添加执行权限。
5.4 应用体积对比
这是Tauri相比Electron最大的优势。以下是我们项目的实际打包结果:
表格
| 技术栈 | 发行包大小 | 安装后大小 |
|---|---|---|
| Electron | 180MB | 350MB |
| Tauri | 15MB | 45MB |
差距是惊人的。Tauri应用之所以这么小,是因为它复用了系统的WebView,而不是像Electron那样捆绑整个Chromium。对于需要分发的内部工具或小型应用,这个优势非常明显。
六、进阶功能扩展
6.1 添加系统托盘
让应用在后台运行时保持最小化的系统托盘。修改src-tauri/src/main.rs:
rust
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
};
fn main() {
tauri::Builder::default()
// ... 其他配置
.setup(|app| {
let quit = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
let show = MenuItem::with_id(app, "show", "显示窗口", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &quit])?;
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.menu_on_left_click(false)
.on_menu_event(|app, event| {
match event.id.as_ref() {
"quit" => {
app.exit(0);
}
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
_ => {}
}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
这段代码在系统托盘添加了图标和右键菜单,点击图标可以显示/隐藏主窗口,右键菜单包含”显示窗口”和”退出”两个选项。
6.2 文件拖拽支持
有时候用户想直接拖文件到聊天窗口中处理。添加拖拽支持:
typescript
const dropZone = document.getElementById('drop-zone')!;
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', async (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const files = e.dataTransfer?.files;
if (files?.length) {
const file = files[0];
const content = await file.text();
this.userInput.value = `请分析以下内容:\n\n${content}`;
}
});
配合Rust后端的文件读取能力,还可以实现文件摘要、代码审查等功能。
6.3 多模型切换
很多开发者会同时运行多个本地模型。添加模型切换功能:
typescript
interface ModelInfo {
name: string;
size: string;
modified: string;
}
// 获取可用模型列表
async function getAvailableModels(): Promise<ModelInfo[]> {
const response = await fetch('http://localhost:11434/api/tags');
const data = await response.json();
return data.models || [];
}
// 切换当前使用的模型
let currentModel = 'llama3.2:3b';
async function switchModel(modelName: string) {
currentModel = modelName;
// 更新后端配置
const { invoke } = await import('@tauri-apps/api/core');
await invoke('set_model', { model: modelName });
}
七、项目完整代码汇总
目录结构最终版
plaintext
ai-desktop-app/
├── src/
│ ├── main.ts # 前端入口
│ ├── app.ts # 应用主逻辑(AI聊天)
│ ├── styles.css # 样式
│ └── icons/ # 前端图标资源
├── src-tauri/
│ ├── src/
│ │ ├── main.rs # Rust入口
│ │ └── lib.rs # 库文件
│ ├── Cargo.toml # Rust依赖
│ ├── tauri.conf.json # Tauri配置
│ ├── build.rs # 构建脚本
│ └── icons/ # 应用图标
├── package.json
├── tsconfig.json
├── vite.config.ts
├── bunfig.toml # Bun配置
└── SPEC.md # 项目规范
package.json关键依赖
json
{
"name": "ai-desktop-app",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2.0.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0",
"typescript": "^5.4.0",
"vite": "^5.4.0"
}
}
八、总结与展望
通过这个实战项目,我们完整掌握了Bun+Tauri开发高性能桌面应用的全流程。这套技术组合的优势总结如下:
性能方面:Bun的启动速度是Node.js的4倍,执行效率提升显著;Tauri生成的包体积比Electron小10倍,内存占用也更精简。对于需要快速响应用户操作的AI应用来说,这种性能优势直接转化为更好的用户体验。
开发体验:Bun兼容npm生态,几乎所有Node.js项目都可以无缝迁移;TypeScript原生支持让类型检查更加可靠;Vite的HMR在开发时提供丝滑的页面刷新体验。Tauri的Rust后端虽然有一定学习曲线,但一旦熟悉了命令注册和IPC通信,开发效率很高。
应用场景:这套组合特别适合开发需要本地AI能力的工具类产品。比如代码审查助手、文档处理工具、数据分析软件、内部管理系统等。相比纯Web应用,桌面应用可以直接访问本地文件系统、调用系统API、运行后台进程,功能边界更宽广。
未来方向:基于这个基础项目,你可以继续探索更多功能:集成更强大的本地AI模型(如Qwen2.5、DeepSeek)、添加插件系统支持扩展功能、开发跨平台通知功能、集成系统快捷键等。Tauri 2.0已经支持移动端,后续还可以扩展到iOS和Android平台。
总的来说,Bun+Tauri代表了2026年桌面应用开发的新趋势:轻量化、高性能、现代感。如果你正考虑开发桌面工具类应用,这套组合值得深入研究。
相关工具推荐:

发表回复