在 Tauri 应用中,异常和崩溃可能来自两个完全不同的世界:一是前端 WebView 中的 JavaScript 错误,二是 Rust 后端的 panic 或未捕获的异常。处理得好,应用可以优雅降级;处理不好,轻则白屏无响应,重则进程直接退出,用户一头雾水。下面从这两个方向分别说明。
前端异常处理
前端错误通常表现为 UI 渲染异常、接口调用失败或未处理的 Promise rejection。Tauri 本身不会捕获这些错误,所以需要开发者主动兜底。
1. 全局错误捕获
在入口 JavaScript 文件中注册全局错误处理,避免因单个组件崩溃导致整个页面不可用:
window.addEventListener('error', (event) => {
console.error('全局错误:', event.error);
// 可以在这里调用 Rust 命令上报错误日志
// or显示友好的错误提示界面
});
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 拒绝:', event.reason);
event.preventDefault(); // 阻止默认的控制台警告
});
如果使用 React 等框架,可以配合错误边界(Error Boundary)实现组件级崩溃隔离,即使某个组件报错,其他部分依然可操作。
2. 错误信息回传 Rust
很多时候你需要把前端的异常收集起来,写入日志文件或发送到远程服务器。通过 Tauri 的命令可以安全地实现:
import { invoke } from '@tauri-apps/api/core';
window.addEventListener('error', async (event) => {
await invoke('log_frontend_error', {
message: event.error?.message || '未知错误',
stack: event.error?.stack || ''
});
});
对应的 Rust 命令:
#[tauri::command]
fn log_frontend_error(message: String, stack: String) {
eprintln!("前端错误: {}\n{}", message, stack);
// 写入文件或上报
}
3. 降级 UI 展示
在发生致命错误后,可以展示一个静态的“出错页面”,而不是让用户面对白屏。Tauri 允许加载本地 HTML 文件作为降级界面:
window.addEventListener('error', () => {
// 简单替换 body 内容
document.body.innerHTML = '<div style="padding:20px;text-align:center;"><h2>应用遇到错误,请重启</h2></div>';
});
Rust 后端异常处理
Rust 的 panics 如果未捕获,会直接终止整个进程,这比前端崩溃更严重。因此必须做好防护。
1. 设置全局 panic hook
在 Rust 入口(如 main 函数)中注册自定义的 panic 处理,让崩溃至少输出一个可追踪的记录:
use std::panic;
fn main() {
// 设置 panic hook,避免完全无声地崩溃
panic::set_hook(Box::new(|info| {
let location = info.location().map(|l| format!("{}:{}", l.file(), l.line())).unwrap_or_default();
let payload = info.payload();
let msg = if let Some(s) = payload.downcast_ref::<&str>() {
*s
} else if let Some(s) = payload.downcast_ref::<String>() {
s.as_str()
} else {
"未知 panic"
};
eprintln!("进程崩溃: {} in {}", msg, location);
// 可尝试写入日志文件,再显示对话框通知用户
}));
// 其余 tauri::Builder 等初始化代码
}
注意:设置了 hook 后,默认的 panic 打印会被覆盖,但进程仍旧会退出。如果需要更高级的恢复,可以使用 std::panic::catch_unwind。
2. 用 catch_unwind 保护关键调用
对于可能触发 panics 的第三方库或复杂逻辑,可以在命令里包裹一层,防止整个应用崩溃:
#[tauri::command]
fn risky_operation() -> Result<String, String> {
std::panic::catch_unwind(|| {
// 可能 panic 的代码
Some("成功")
})
.map_err(|_| "操作异常".to_string())
.and_then(|res| res.ok_or("未知错误".to_string()))
}
这样,即使内部 panic,命令也只是返回一个错误字符串给前端,而不会杀掉进程。
3. 日志记录与持久化
无论是 panic 还是普通错误,都应该持久化到本地文件,方便用户反馈时提供日志。可以使用 log 和 simplelog 等 crate:
use simplelog::*;
use std::fs::File;
fn setup_logger() {
WriteLogger::init(
LevelFilter::Info,
Config::default(),
File::create("app.log").unwrap(),
).ok();
}
然后在 panic hook 或关键位置调用 log::error! 记录详细上下文。
崩溃后的用户通知
当 Rust 进程意外终止时,通常伴随 WebView 窗口的消失。可以在启动时使用系统对话框提醒用户上次是否异常退出,或者在 panic hook 中调用系统的消息框(Windows 的 MessageBoxW、macOS 的 NSAlert)。但注意在 panic 状态下直接调用 UI 可能不安全,更好的做法是在重启时检查退出标记。
一个简单方法:正常退出时删除一个特定文件,panic 时不删除。下次启动时检测该文件是否存在,若存在则表示上一次是异常退出,可以给出提示。
// 启动时
if std::path::Path::new("crash_flag").exists() {
// 弹出提示或者通知前端显示恢复界面
std::fs::remove_file("crash_flag").ok();
}
// 正常退出时确保删除此文件
// 在 panic hook 中创建这个文件
一句话总结:前端兜底保证界面不白屏,Rust 兜底保证进程不莫名消失,再辅以统一的日志采集和友好提示,就能让 Tauri 应用在面对异常和崩溃时,依然给用户留下可靠的印象。