前言
本章主要讲述笔者个人在编写 Tauri 应用中遇到的认为值得记录的 App 基础知识,欢迎补充与指正
Rust 相关
String 与 str 转换
可参考 Rust String &str 常用操作
1 2 3 4 5 6 7 8 9
| let message:&str = "Hello, world!"; let message_String: String = String::from(message); let message_String: String = message.to_string(); let message_String: String = message.to_owned();
let message_str: &str = &message_String; let message_str: &str = message.as_str();
|
数组与 Vec
可参考 使用 Vector 储存列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| let rust_arr: [u8;5] = [1, 2, 3, 4, 5]; let index_of_third = rust_arr[2]; let part_of_arr = &rust_arr[0..2];
let rust_vec: Vec<u8> = Vec::new(); let rust_vec = vec![1, 2, 3, 4, 5]; let part_of_vec = &vec[2]; let part_of_vec = rust_vec.get(2);
rust_vec.push(1);
for (index, value) in rust_vec.iter().enumerate() { ... }
for item in &mut rust_vec { ... }
rust_vec.retain(|value| -> bool { ... return false; return true; })
|
结构体打印
1 2 3 4 5 6
| #[derive(Debug)] pub struct TodoList { pub list: HashMap<String, String> }
println!("{:?}", TodoList);
|
Clone or Copy
可参考 复制值的 Clone
和 Copy
。
当变量的值需要多处使用而引用不方便时可以使用 Clone。
Copy 以笔者目前的浅薄见识来说并不推荐使用,见 What is the difference between Copy and Clone? 及 Rust 当自定义结构体含 String 类型时 无法 derive Copy 应该怎么解决?。
1 2 3 4 5 6 7 8 9 10
| #[derive(Debug, Clone)] pub struct TodoList { pub list: HashMap<String, String> }
let todo_list: TodoList = TodoList { list: Hashmap::new() }; let todo_list_clone: TodoList = todo_list.clone();
pritnln!("{:?}", todo_list); pritnln!("{:?}", todo_list_clone);
|
文件读写
可参考 Rust std::fs::OpenOptions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| use std::fs::{ File, OpenOptions, read_to_string };
let file_path: &str = "xxxxxx"; let mut target_file: File = OpenOptions::new().read(true).write(true).create(true).open(file_path).unwrap();
let file_size: u64 = target_file.metadata().unwrap().len();
let content: &str = "yyyyyyyyyyy";
let write_size: usize = target_file.write(content).unwrap();
let mut target_file: File = OpenOptions::new().append(true).open(file_path).unwrap();
let file_content: String = read_to_string(file_path).unwrap();
|
调用 shell
可参考 std::process::Command
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| pub fn open_folder_window(path: &str) { debug!("try to open {} window", path); if consts::OS == "linux" { Command::new("xdg-open") .arg(path) .spawn() .unwrap(); } else if consts::OS == "macos" { Command::new("open") .arg(path) .spawn() .unwrap(); } else if consts::OS == "windows" { Command::new("open") .arg(path) .spawn() .unwrap(); } }
|
第三方包管理
下述方法不唯一,可自行决定合适的方式, 注意,模块名称最好是 snake case 而不是 camel case。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
|
pub struct Person { pub name: String, pub age: u8, }
mod my_types;
use my_types::Person;
fn main() { let jack: Person = Person { name: "Jack".to_string(), age: 18 }; println!("Hello, {}", jack.name); }
pub mod explores; pub mod collector;
pub fn greet() { println!("Hello from explorer"); }
pub fn greet() { println!("Hello from explorer"); }
mod modules;
use modules::explorer::greet as ex_greet; use modules::collector::greet as co_greet;
fn main() { ex_greet(); co_greet(); }
use super::collector::greet as co_greet;
pub fn greet() { co_greet(); println!("Hello from explorer"); }
|
本地修改第三方库
有时候第三方库总会有些问题,fork 后修改再拉取也很麻烦,能不能直接修改它的本地版本然后直接用呢?当然是可以的,这里我们以 Windows 为例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| cd C:\Users\(Username)\.cargo
// 如果你的依赖是这样引入的 [dependencies.tauri-plugin-highlander] git = "https://github.com/ioneyed/tauri-plugin-highlander" branch = "main"
// 那你可以在这里找到对应的本地源文件并修改 ls ./git/checkouts > tauri-plugin-highlander-3a9ec0026e0d0fde
// 其他的引入方式根据你设置的来源找到本地源文件,比如这里有官方源和清华源等,进入对应目录修改文件即可 ls ./git/registry/src > mirrors.ustc.edu.cn-61ef6e0cd06fb9b8 > mirrors.tuna.tsinghua.edu.cn-df7c3c540f42cdbd
|
日志功能
可参考 crate log4rs 及 Advanced logging in rust with log4rs: rotation, retention, patterns, multiple loggers。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| use log4rs::{ append::{ console::{ ConsoleAppender, Target }, rolling_file::{ RollingFileAppender, policy::compound::{ CompoundPolicy, roll::fixed_window::FixedWindowRoller, trigger::size::SizeTrigger, } } }, config::{ Appender, Config, Root }, encode::pattern::PatternEncoder, filter::threshold::ThresholdFilter, }; use log::{ debug, LevelFilter }; use path_absolutize::*; use std::path::Path;
pub fn initialize_logger(log_file_path: &str) { let global_log_level = LevelFilter::Debug; let stdout_log_level = LevelFilter::Debug; let logfile_log_level = LevelFilter::Info; let strong_level_log_level = LevelFilter::Warn;
let log_pattern = "{d(%Y-%m-%d %H:%M:%S)} | {({l}):5.5} | {m}{n}";
let trigger_size = byte_unit::n_mb_bytes!(6) as u64; let trigger = SizeTrigger::new(trigger_size);
let roller_pattern = "logs/log - {}.log"; let roller_count = 3; let roller_base = 1; let roller = FixedWindowRoller::builder() .base(roller_base) .build(roller_pattern, roller_count).unwrap();
let strong_level_roller = FixedWindowRoller::builder() .base(roller_base) .build(roller_pattern, roller_count).unwrap();
let log_file_compound_policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller)); let strong_level_compound_policy = CompoundPolicy::new(Box::new(trigger), Box::new(strong_level_roller));
let log_file = RollingFileAppender::builder() .encoder(Box::new(PatternEncoder::new(log_pattern))) .build(log_file_path, Box::new(log_file_compound_policy)) .unwrap();
let absolute_path = Path::new(log_file_path).absolutize().unwrap(); let parent_path = absolute_path.parent().unwrap(); let strong_level_path = parent_path.join("strong_level.log").into_os_string().into_string().unwrap(); let strong_level_log_file = RollingFileAppender::builder() .encoder(Box::new(PatternEncoder::new(log_pattern))) .build(strong_level_path, Box::new(strong_level_compound_policy)) .unwrap();
let stdout = ConsoleAppender::builder() .encoder(Box::new(PatternEncoder::new(log_pattern))) .target(Target::Stdout) .build(); let config = Config::builder() .appender( Appender::builder() .filter(Box::new(ThresholdFilter::new(logfile_log_level))) .build("log_file", Box::new(log_file))) .appender( Appender::builder() .filter(Box::new(ThresholdFilter::new(strong_level_log_level))) .build("strong_level", Box::new(strong_level_log_file)) ) .appender( Appender::builder() .filter(Box::new(ThresholdFilter::new(stdout_log_level))) .build("stdout", Box::new(stdout)), ) .build( Root::builder() .appender("stdout") .appender("log_file") .appender("strong_level") .build(global_log_level)) .unwrap();
let _log_handler = log4rs::init_config(config).unwrap();
debug!("Programe logger initialize success"); }
|
时间
可参考 Crate: chrono
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| [dependencies] ... chrono = "0.4"
use chrono::{Utc, Local, DateTime};
let current_utc_time = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(); let current_local_time = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let tommorrow_date = NaiveDate::parse_from_str(start_date, "%Y-%m-%d").unwrap() + Duration::days(1);
fn get_days_from_month(year: i32, month: u32) -> i64 { NaiveDate::from_ymd( match month { 12 => year + 1, _ => year, }, match month { 12 => 1, _ => month + 1, }, 1 ) .signed_duration_since(NaiveDate::from_ymd(year, month, 1)) .num_days() }
|
Tauri 相关
绝对路径
如果你的 app 是打算做 Macos 端或 Linux 端的,请一定保持所有涉及到的路径为已知的、绝对的路径,否则程序将无法启动,具体 issue 见 [bug] Appimage seems have permission problem。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| use std::env::current_dir; use tauri::api::path::home_dir;
pub fn get_app_home_dir() -> PathBuf { #[cfg(target_os = "windows")] if let Err(e) = current_dir() { error!("Failed to get app home dir. Errmsg: {}", e); std::process::exit(-1); } else { return current_dir().unwrap(); }
#[cfg(not(target_os = "windows"))] match home_dir() { None => { error!("Failed to get app home dir"); std::process::exit(-1); }, Some(path) => { return path.join(APP_DIR); } } }
pub fn get_app_log_dir() -> PathBuf { get_app_home_dir().join("logs") }
|
系统托盘
可参考 Tauri: System Tray
1 2 3 4 5 6 7 8 9
| { "tauri": { "systemTray": { "iconPath": "icons/icon.png", "iconAsTemplate": true } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| pub fn create_system_tray() -> SystemTray { let quit = CustomMenuItem::new("quit".to_string(), "Quit Program"); let hide = CustomMenuItem::new("hide".to_string(), "Close to tray"); let tray_menu = SystemTrayMenu::new() .add_item(hide) .add_native_item(SystemTrayMenuItem::Separator) .add_item(quit); SystemTray::new().with_menu(tray_menu) }
pub fn handle_system_tray_event(app: &AppHandle<Wry>, event: SystemTrayEvent) { match event { SystemTrayEvent::DoubleClick { position: _ , size: _, .. } => { let window = app.get_window("main").unwrap(); window.unminimize().unwrap(); window.show().unwrap(); window.set_focus().unwrap(); }, SystemTrayEvent::MenuItemClick { id, ..} => { match id.as_str() { "quit" => { app.get_window("main").unwrap().hide().unwrap(); app.emit_all("close", {}).unwrap(); debug!("System tray try to close"); } "hide" => { app.get_window("main").unwrap().hide().unwrap(); app.emit_all("hide", {}).unwrap(); debug!("System tray try to hide/show"); } _ => {} } } _ => {} } }
#[command] pub fn change_system_tray_lang(lang: &str, app_handle: tauri::AppHandle) -> bool { let window = app_handle.get_window("main").unwrap(); let hide_item = app_handle.tray_handle().get_item("hide"); let quit_item = app_handle.tray_handle().get_item("quit");
if &lang == &"zh-CN" { let hide_title = if !window_visiable {"最小化至托盘"} else {"显示主界面"}; let quit_title = r"退出程序"; hide_item.set_title(format!("{}", hide_title)).unwrap(); quit_item.set_title(format!("{}", quit_title)).unwrap(); } else if &lang == &"en-US" { let hide_title = if !window_visiable {"Close to tray"} else {"Show MainPage"}; let quit_title = r"Quit Program";
hide_item.set_title(format!("{}", hide_title)).unwrap(); quit_item.set_title(format!("{}", quit_title)).unwrap(); }
debug!("Change sysyem tray to lang {}", lang); true }
fn main() { tauri::Builder::default() .system_tray(create_system_tray()) .on_system_tray_event(handle_system_tray_event) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
|
去除顶部状态栏
可参考 Tauri: Window Customization
1 2 3 4 5 6 7 8 9 10
| { "tauri": { "windows": { ... "decorations": false } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| // TitleBar.vue <template> <div data-tauri-drag-region class="titlebar"> <div class="titlebar-left" id="titlebar-left"> <div class="titlebar-icon" id="titlebar-icon"> <el-icon class="app-icon"> <AppIcon /> </el-icon> </div> <div class="titlebar-title" id="titlebar-title"> {{t('general.AppTitle')}} </div> </div> <div class="titlebar-right" id="titlebar-right"> <div class="titlebar-button" id="titlebar-minimize"> <el-icon><SemiSelect /></el-icon> </div> <div class="titlebar-button" id="titlebar-maximize"> <el-icon><BorderOutlined /></el-icon> </div> <div class="titlebar-button" id="titlebar-close"> <el-icon><CloseBold /></el-icon> </div> </div> </div> </template>
<script lang="ts" setup> import { onMounted } from 'vue'; import { appWindow } from '@tauri-apps/api/window'; import { AppstoreOutlined, BorderOutlined } from '@vicons/antd'; import { SemiSelect, FullScreen, CloseBold } from '@element-plus/icons-vue'; import AppIcon from '~icons/icons/favicon';
onMounted(() => { (document.getElementById('titlebar-minimize') as HTMLElement).addEventListener('click', () => appWindow.minimize()); (document.getElementById('titlebar-maximize') as HTMLElement).addEventListener('click', () => appWindow.toggleMaximize()); (document.getElementById('titlebar-close') as HTMLElement).addEventListener('click', () => appWindow.close()); }) </script>
<style lang="less" scoped> @import "../assets/style/theme/default-vars.less"; .titlebar { height: 30px; color: var(--el-text-color-regular); background-color: var(--el-bg-color); user-select: none; display: flex; flex-direction: row; justify-content: space-between; position: fixed; top: 0; left: 0; right: 0; z-index: 999;
.titlebar-left { display: flex; flex-direction: row;
.titlebar-icon { width: 30px; height: 30px; padding-top: 5px; padding-left: 5px;
.app-icon { font-size: 20px; } }
.titlebar-title { padding-top: 6px; } }
.titlebar-right { display: flex; flex-direction: row; .titlebar-button { display: inline-flex; justify-content: center; align-items: center; width: 30px; height: 30px; } .titlebar-button:hover { background: var(--el-border-color); }
#titlebar-close:hover { background: red; } } } </style>
|
Cargo 管理
Cargo 指令详见 Cargo Commands
1 2 3 4 5 6 7 8 9
| { ... "scripts": { ... "tauri": "tauri", "cargo": "cd src-tauri && cargo" }, ... }
|
1 2 3 4
| $ pnpm cargo add xxx
$ pnpm cargo remove xxx
|
添加窗口阴影
可参考 window-shadows,注意:暂不支持 Linux
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| use window_shadows::set_shadow;
pub fn initialize_window_shadow(window: &TauriWindow, is_shadow_enable: bool) { if let Err(e) = set_shadow(window, is_shadow_enable) { error!("Failed to add native window shadow, errMsg: {:?}", e); } }
fn main() { tauri::Builder::default() .setup(|app| { let main_window = app.get_window("main").unwrap(); initialize_window_shadow(&main_window, true);
Ok(()) }) .invoke_handler(tauri::generate_handler![ add_todo, delete_todo, start_heart_rate ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
|
限制多实例运行
可参考 Tauri Plugin Highlander 或 Single Instance
⚠️ tauri-plugin-highlander 目前无法通过 Github Actions 下 Ubuntu 和 MacOS 环境的编译,已提交相关 pr 修复,此处使用修复后的分支
❌ single_instance 目前在纯 rust 环境下表现良好,在 tauri 环境下观察到所有实例均为 single,暂未查明原因
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
[dependencies.tauri-plugin-highlander] git = "https://github.com/Hellager/tauri-plugin-highlander" branch = "fix-platform-build-error"
use tauri_plugin_highlander::*; use tauri::Wry;
pub fn initialize_plugin_highlander(event_name: &str) -> Highlander<Wry> { HighlanderBuilder::default().event(event_name.to_string()).build() }
fn main() { let plugin_highlander = initialize_plugin_highlander("another_instance"); tauri::Builder::default() .plugin(plugin_highlander) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
single-instance = "0.3"
pub fn check_instance(name: &str) { let instance = SingleInstance::new(name).unwrap(); debug!("Check instance {} whether single {}", name, instance.is_single()); if !instance.is_single() { debug!("{} is not single", name); std::process::exit(0); } else { debug!("{} is single", name); } }
|
拦截 app 请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| pub fn handle_app_event(_app_handle: &AppHandle<Wry>, event: RunEvent) { match event { RunEvent::Exit => {} RunEvent::ExitRequested { .. } => {} RunEvent::WindowEvent { label, event, .. } => { match event { WindowEvent::CloseRequested { api, .. } => { if label == "main" { let _ = _app_handle.get_window("main").unwrap().hide(); _app_handle.emit_all("close", {}).unwrap(); debug!("Rust try to close"); api.prevent_close() } } _ => {} } } RunEvent::Ready => {} RunEvent::Resumed => {} RunEvent::MainEventsCleared => {} _ => {} } }
fn main() { let _app = tauri::Builder::default() .build(tauri::generate_context!()) .expect("error while running tauri application"); _app.run(handle_app_event) }
|
加载页
可参考 Tauri: Splashscreen
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| { "tauri": { "windows": [ {...} { "width": 500, "height": 300, "center": true, "decorations": false, "url": "splashscreen.html", "label": "splashscreen", "title": "Loading", "transparent": true } ] } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| // splashscreen.html 示例 <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> body { background-color:transparent; } canvas { display: block; left: 50%; margin: -125px 0 0 -125px; position: absolute; top: 50%; }
.container { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
#content { margin-top: 280px; font-size: 2vw; font-weight: bold; color: #FFFFFF; }
</style> </head> <body> <div class="container"> <canvas id="canvas" width="250" height="250"></canvas> <span id="content">Loading...</span> </div>
<script> var canvasLoader = function(){ var self = this; window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)}}(); self.init = function(){ self.canvas = document.getElementById('canvas'); self.ctx = self.canvas.getContext('2d'); self.ctx.lineWidth = .5; self.ctx.strokeStyle = 'rgba(255,255,255,.75)'; self.count = 75; self.rotation = 270*(Math.PI/180); self.speed = 6; self.canvasLoop(); }; self.updateLoader = function(){ self.rotation += self.speed/100; }; self.renderLoader = function(){ self.ctx.save(); self.ctx.globalCompositeOperation = 'source-over'; self.ctx.translate(125, 125); self.ctx.rotate(self.rotation); var i = self.count; while(i--){ self.ctx.beginPath(); self.ctx.arc(0, 0, i+(Math.random()*35), Math.random(), Math.PI/3+(Math.random()/12), false); self.ctx.stroke(); } self.ctx.restore(); }; self.canvasLoop = function(){ requestAnimFrame(self.canvasLoop, self.canvas); self.ctx.globalCompositeOperation = 'destination-out'; self.ctx.fillStyle = 'rgba(255,255,255,.03)'; self.ctx.fillRect(0,0,250,250); self.updateLoader(); self.renderLoader(); }; }; var loader = new canvasLoader(); loader.init(); </script> </body> </html>
|
Ubuntu 22.04 编译
需安装 libfuse2, 详见 [bug] Failed to build Appimage on Ubuntu 22.04
1
| $ sudo apt install libfuse2
|
手动退出程序
⚠️ 两种退出方式都不会触发 app 的 close 事件,而是直接退出
1 2 3 4 5 6 7 8 9 10
| use std::process;
process::exit(0)
RUST <script lang="ts" setup> import { appWindow } from '@tauri-apps/api/window';
appWindow.close(); </scriptscript>
|