Tauri 跨平台开发实践(一)
什么是 Tauri?
Tauri 是一个相对较新的跨平台 GUI 框架,可以帮助开发者为主流平台制作应用程序(如 MacOS,Windows,Linux,iOS,Android 等)。几乎支持所有现有的前端框架(如 React, Vue 等)。
我们日常使用的许多跨平台应用,如 VS Code、Notion 和 Apifox 都在 Electron 框架上运行。但 Electron 的缺点是,由于每个应用都需要携带完整的 Chromium 内核,因此占用大量磁盘空间。而且 Chromium 在处理器和内存使用方面效率不高。Tauri 与 Electron 最大的区别在于,Tauri 使用系统原生的 Webview,这使得 Tauri 应用程序的体积更小、性能更高,且后端的语言也不同,Electron 使用 NodeJS,而 Tauri 使用 Rust。
在本文中,我将分享开发一个壁纸工具(系统托盘应用)的一些细节。
项目创建
项目创建可参考官方文档:Tauri 官方文档,本文使用 Tauri 2.0, 与 Tauri 1.0 相比有较大改动。
前端使用 Vue3 + Vite + TypeScript 的组合。
UI 实现
UI 方面使用了 TailwindCSS 搭建了一个简洁的界面,写法和 Web 是一样的。
毛玻璃及圆角效果的实现
使用 window-vibrancy 实现,不支持 Linux, 且圆角效果只支持 MacOS,需根据不同平台进行不同的设置。
index.html
html, body { background: transparent }
tauri.conf.json
使用 macOSPrivateApi 会导致 App Store 拒绝通过
{
"app": {
"windows": [
{
"transparent": true
}
],
"macOSPrivateApi": true
}
}
src/main.rs
use window_vibrancy::*;
tauri::Builder::default()
.setup(move |app| {
let window = app.get_webview_window("main").unwrap();
#[cfg(target_os = "macos")]
apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, Some(8.0))
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
#[cfg(target_os = "windows")]
apply_blur(&window, Some((18, 18, 18, 125)))
.expect("Unsupported platform! 'apply_blur' is only supported on Windows");
});
常驻系统托盘,添加右键菜单
使用 Tauri 自带的 SystemTray 模块实现系统托盘。
创建托盘方法
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager, Runtime,
};
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
let next_photo_i = MenuItem::with_id(app, "switch", "Switch wallpaper", true, None::<&str>)?;
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(
app,
&[
&next_photo_i,
&PredefinedMenuItem::separator(app)?,
&quit_i,
],
)?;
let _ = TrayIconBuilder::with_id("tray")
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.menu_on_left_click(false)
.on_menu_event(move |app, event| match event.id.as_ref() {
"switch" => {
// TODO: 切换壁纸
}
"quit" => {
app.exit(0);
}
_ => {}
})
.build(app);
Ok(())
}
初始化时调用创建托盘方法
tauri::Builder::default()
.setup(move |app| {
#[cfg(all(desktop))]
{
let handle = app.handle();
tray::create_tray(&handle)?;
}
// mac的docker栏隐藏需要隐藏icon
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
});
窗口固定
通过托盘图标的点击事件实现显示或隐藏窗口,并使用 positioner 实现MacOS 平台下,窗口固定在顶部, Windows 平台下,窗口固定在底部。
初始化时,将插件实例传递给 plugin 方法
tauri::Builder::default().plugin(tauri_plugin_positioner::init())
在托盘的点击事件中调用 move_window 方法
use tauri::{
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager, Runtime,
};
use tauri_plugin_positioner::{Position, WindowExt};
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
let _ = TrayIconBuilder::with_id("tray")
// ...
.on_tray_icon_event(|app, event| {
tauri_plugin_positioner::on_tray_event(app.app_handle(), &event);
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = app.app_handle();
if let Some(window) = app.get_webview_window("main") {
#[cfg(target_os = "windows")]
let _ = window.move_window(Position::Center);
#[cfg(not(target_os = "windows"))]
let _ = window.move_window(Position::TrayBottomCenter);
if window.is_visible().unwrap() {
window.hide().unwrap();
} else {
window.show().unwrap();
window.set_focus().unwrap();
}
}
}
})
.build(app);
Ok(())
}
开机自启动
省去每次开机后都要重新打开的麻烦,使用 autostart 实现。
use tauri_plugin_autostart::MacosLauncher;
let mut builder = tauri::Builder::default().plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, None))
设置相关权限
{
"permissions": [
"autostart:allow-enable",
"autostart:allow-disable",
"autostart:allow-is-enabled"
]
}
前端启用或禁用自启动
import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart';
await enable();
console.log(`registered for autostart? ${await isEnabled()}`);
disable();
实现更换壁纸功能
使用 Unsplash 提供的接口获取图片,然后使用 wallpaper 设置壁纸,并在 on_menu_event 的点击事件中调用。
// 下载到指定目录
pub async fn download(&mut self, url: &str, file_name: &str, path: PathBuf) -> Result<String, String> {
let response = http::get(url.to_string());
let data = match response.await {
Ok(data) => data,
Err(_) => return Err("Could not fetch image".to_string())
};
let path = path.join(format!("{}.jpg", file_name));
let mut file = File::create(path.clone()).unwrap();
let content = data.bytes().await.unwrap();
copy(&mut content.as_ref(), &mut file).unwrap();
Ok(path.display().to_string())
}
pub async fn set_wallpaper(&mut self, url: &str, file_name: &str) -> Result<(), String> {
// 获取系统缓存目录
let cache_dir = match dirs::cache_dir() {
Some(path) => path,
None => {
return Err("Could not find download directory".to_string())
}
};
let path = cache_dir.join(Self::CACHE_DIR);
// 如果目录不存在,先创建
let _ = std::fs::create_dir_all(cache_dir.join(Self::CACHE_DIR));
let path = Self::download(self, url, file_name, path.clone()).await.unwrap();
// 指定本地文件作为壁纸
let _ = wallpaper::set_from_path(&path);
Ok(())
}
pub async fn switch_wallpaper(&mut self) -> Result<(), String> {
let res = http::get("<https://api.unsplash.com/photos/random>".to_string()).await.unwrap();
let data = match res.json::<Random>().await {
Ok(data) => data,
Err(_) => return Err("json parse fail".to_string())
};
Self::set_wallpaper(self, &data.urls.raw, &data.id).await.unwrap();
Ok(())
}
总结
这篇文章介绍了 Tauri 的基本概念,以及如何使用 Tauri 实现一些常见的功能,由于篇幅有限,只简单介绍一些功能,更多的功能将在后续文章中介绍。