用 Tauri 做了一个系统监控小工具,能实时查看 CPU、内存、磁盘和网络状态,支持系统托盘和开机自启。完整走一遍从搭建到打包的过程。
为什么选 Tauri
之前用 Electron 写过类似的工具,打包出来 150MB+,内存占用 200MB 起步。对于一个系统监控工具来说,自己就是资源大户,多少有点讽刺。Tauri 用系统 WebView 渲染前端,后端是 Rust,打包体积 5~8MB,内存占用 30MB 左右,适合这种常驻后台的工具。
项目初始化
# 安装 Tauri CLI
cargo install create-tauri-app
# 创建项目,前端选 vanilla(不用框架,够简单)
cargo create-tauri-app sys-monitor --template vanilla
cd sys-monitor
在 Cargo.toml 中添加系统信息采集的依赖:
[dependencies]
tauri = { version = "1.5", features = ["system-tray", "shell-open"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sysinfo = "0.30"
后端:采集系统信息
核心是用 sysinfo crate 采集各项指标。定义一个数据结构,通过 Tauri 的 command 机制暴露给前端:
use serde::Serialize;
use sysinfo::{CpuExt, DiskExt, NetworkExt, NetworksExt, System, SystemExt};
use std::sync::Mutex;
use tauri::State;
#[derive(Serialize, Clone)]
pub struct SystemInfo {
cpu_usage: Vec<f32>, // 每个核心的使用率
cpu_avg: f32, // 平均 CPU 使用率
memory_total: u64, // 总内存 (bytes)
memory_used: u64, // 已用内存
memory_usage: f32, // 内存使用率
disks: Vec<DiskInfo>,
network_rx: u64, // 接收字节数
network_tx: u64, // 发送字节数
uptime: u64, // 系统运行时间(秒)
hostname: String,
}
#[derive(Serialize, Clone)]
pub struct DiskInfo {
name: String,
total: u64,
available: u64,
usage: f32,
}
pub struct SysState(pub Mutex<System>);
#[tauri::command]
fn get_system_info(state: State<SysState>) -> SystemInfo {
let mut sys = state.0.lock().unwrap();
sys.refresh_all();
let cpu_usage: Vec<f32> = sys.cpus().iter().map(|c| c.cpu_usage()).collect();
let cpu_avg = cpu_usage.iter().sum::<f32>() / cpu_usage.len() as f32;
let disks: Vec<DiskInfo> = sys.disks().iter().map(|d| {
let total = d.total_space();
let available = d.available_space();
DiskInfo {
name: d.name().to_string_lossy().to_string(),
total,
available,
usage: (total - available) as f32 / total as f32 * 100.0,
}
}).collect();
let (rx, tx) = sys.networks().iter().fold((0u64, 0u64), |(rx, tx), (_, data)| {
(rx + data.received(), tx + data.transmitted())
});
SystemInfo {
cpu_usage,
cpu_avg,
memory_total: sys.total_memory(),
memory_used: sys.used_memory(),
memory_usage: sys.used_memory() as f32 / sys.total_memory() as f32 * 100.0,
disks,
network_rx: rx,
network_tx: tx,
uptime: sys.uptime(),
hostname: sys.host_name().unwrap_or_default(),
}
}
在 main.rs 中注册:
fn main() {
let sys = SysState(Mutex::new(System::new_all()));
tauri::Builder::default()
.manage(sys)
.invoke_handler(tauri::generate_handler![get_system_info])
.system_tray(build_tray())
.on_system_tray_event(handle_tray_event)
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
前端:实时图表
前端用 Chart.js 做实时折线图。通过 setInterval 每秒调用一次后端 command:
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<div class="dashboard">
<div class="card">
<h3>CPU 使用率</h3>
<canvas id="cpuChart"></canvas>
<span id="cpuValue" class="value">0%</span>
</div>
<div class="card">
<h3>内存使用率</h3>
<canvas id="memChart"></canvas>
<span id="memValue" class="value">0 / 0 GB</span>
</div>
<div class="card">
<h3>磁盘空间</h3>
<div id="diskBars"></div>
</div>
<div class="card">
<h3>网络流量</h3>
<canvas id="netChart"></canvas>
</div>
</div>
const { invoke } = window.__TAURI__.tauri;
const MAX_POINTS = 60; // 保留最近 60 秒数据
const cpuData = { labels: [], data: [] };
const memData = { labels: [], data: [] };
const netData = { labels: [], rx: [], tx: [] };
let prevRx = 0, prevTx = 0;
// 初始化 CPU 图表
const cpuChart = new Chart(document.getElementById('cpuChart'), {
type: 'line',
data: {
labels: cpuData.labels,
datasets: [{
label: 'CPU %',
data: cpuData.data,
borderColor: '#4fc3f7',
backgroundColor: 'rgba(79, 195, 247, 0.1)',
fill: true,
tension: 0.3,
pointRadius: 0,
}]
},
options: {
responsive: true,
scales: { y: { min: 0, max: 100 } },
animation: { duration: 0 },
plugins: { legend: { display: false } }
}
});
// 内存和网络图表类似初始化...
async function updateStats() {
const info = await invoke('get_system_info');
const now = new Date().toLocaleTimeString();
// CPU
cpuData.labels.push(now);
cpuData.data.push(info.cpu_avg.toFixed(1));
if (cpuData.labels.length > MAX_POINTS) {
cpuData.labels.shift();
cpuData.data.shift();
}
cpuChart.update();
document.getElementById('cpuValue').textContent = `${info.cpu_avg.toFixed(1)}%`;
// 内存
const memGB = (info.memory_used / 1024 / 1024 / 1024).toFixed(1);
const totalGB = (info.memory_total / 1024 / 1024 / 1024).toFixed(1);
document.getElementById('memValue').textContent = `${memGB} / ${totalGB} GB`;
// 网络速率(差值计算)
const rxSpeed = prevRx ? (info.network_rx - prevRx) : 0;
const txSpeed = prevTx ? (info.network_tx - prevTx) : 0;
prevRx = info.network_rx;
prevTx = info.network_tx;
// 磁盘条形图
const diskBars = document.getElementById('diskBars');
diskBars.innerHTML = info.disks.map(d => `
<div class="disk-item">
<span>${d.name || 'Disk'}</span>
<div class="bar"><div class="fill" style="width:${d.usage.toFixed(0)}%"></div></div>
<span>${d.usage.toFixed(0)}%</span>
</div>
`).join('');
}
setInterval(updateStats, 1000);
updateStats();
系统托盘
Tauri 原生支持系统托盘,关闭窗口时最小化到托盘而不是退出:
use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu, SystemTrayEvent, Manager};
fn build_tray() -> SystemTray {
let menu = SystemTrayMenu::new()
.add_item(CustomMenuItem::new("show", "显示窗口"))
.add_item(CustomMenuItem::new("quit", "退出"));
SystemTray::new().with_menu(menu)
}
fn handle_tray_event(app: &tauri::AppHandle, event: SystemTrayEvent) {
match event {
SystemTrayEvent::DoubleClick { .. } => {
let window = app.get_window("main").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
}
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"show" => {
let window = app.get_window("main").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
}
"quit" => std::process::exit(0),
_ => {}
},
_ => {}
}
}
窗口关闭时拦截,改为隐藏:
// 在 Builder 链中添加
.on_window_event(|event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
event.window().hide().unwrap();
api.prevent_close();
}
})
开机自启动
Windows 下通过注册表实现,Linux 下通过 .desktop 文件。Tauri 社区有 tauri-plugin-autostart:
# Cargo.toml
[dependencies]
tauri-plugin-autostart = "0.1"
use tauri_plugin_autostart::MacosLauncher;
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec![]),
))
// ... 其他配置
}
前端提供一个开关让用户控制:
const { isEnabled, enable, disable } = window.__TAURI_PLUGIN_AUTOSTART__;
async function toggleAutostart(checked) {
if (checked) {
await enable();
} else {
await disable();
}
}
打包发布
Tauri 打包非常简单:
# 开发
cargo tauri dev
# 打包 release
cargo tauri build
Windows 下生成 .msi 和 .exe 安装包,Linux 下生成 .deb 和 .AppImage,macOS 下生成 .dmg。
最终效果:
- Windows 安装包:6.2 MB
- 运行内存占用:约 28 MB
- CPU 开销:<0.5%(1 秒刷新间隔)
对比之前 Electron 版本的 150MB 安装包和 200MB 内存,差距巨大。Tauri 的缺点是 Rust 编译慢(全量编译 2~3 分钟)、WebView 在不同平台的渲染一致性不如 Chromium。但对于这种工具类应用,完全可以接受。