在之前工作(使用Tauri + Leptos开发带系统托盘桌面应用-CSDN博客)的基础上,继续尝试对本地Sqlite数据库进行读、写、删除操作,开发环境还是VS Code+Rust-analyzer。
最终程序界面如下:
主要参考文章:Building a todo app in Tauri with SQLite and sqlx
1. 创建项目
还是用create-tauri-app来创建一个新项目,并安装sqlx-cli。
cargo create-tauri-app
cargo install sqlx-cli
create-tauri-app升级到了4.5.9版本,Leptos也从0.6版本更新到了0.7版本。
src-tauri/Cargo.toml文件内容如下:
[package]
name = "acid-index"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "acid_index_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
#tauri-plugin-sql = {version = "2", features = ["sqlite"] } # or "postgres", or "mysql"
sqlx = { version = "0.8.2", features = ["sqlite", "runtime-tokio"] }
tokio = { version ="1", features = ["full"] }
futures = "0.3.31"
log = "0.4.22"
#for win7
#tauri-plugin-notification = { version = "2", features = [ "windows7-compat" ] }
VS Code的rust-analyzer插件会自动完成相关依赖包的安装
2. Sqlite数据库操作
使用sqlx实现对Sqlite数据库的操作,数据库文件名为:db.sqlite,默认在appDataDir文件夹下面,这个找了很久,Win10系统下为:C:\Users\<user_name>\AppData\Roaming\<identifier>\ 文件夹,其中"username"为用户名,"identifier"为tauri.conf.json中定义的"identifier",此处为com.acid-index.app。
本例中新建一个数据库,包含一个users表格。数据库迁移文件在src-tauri/migrates/文件夹下面,使用如下命令生成:
cd src-tauri
sqlx migrate add create_users_table
然后修改 src-tauri/migrates/文件夹下面的sql文件,内容如下:
-- Add migration script here
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
email TEXT
);
还是通过#[tauri::command]新建invoke handle函数实现对数据库的操作,用于前端leptos调用。主要程序在src-tauri\src\lib.rs中,具体如下:
use futures::TryStreamExt;
use sqlx::{migrate::MigrateDatabase, prelude::FromRow, sqlite::SqlitePoolOptions, Pool, Sqlite};
use tauri::{App, Manager};
type Db = Pool<Sqlite>;
struct DbState {
db: Db,
}
async fn setup_db(app: &App) -> Db {
let mut path = app.path().app_data_dir().expect("获取程序数据文件夹路径失败!");
match std::fs::create_dir_all(path.clone()) {
Ok(_) => {}
Err(err) => {
panic!("创建文件夹错误:{}", err);
}
};
//C:\Users\<user_name>\AppData\Roaming\com.mynewapp.app\db.sqlite
path.push("db.sqlite");
Sqlite::create_database(
format!("sqlite:{}", path.to_str().expect("文件夹路径不能为空!")).as_str(),
)
.await
.expect("创建数据库失败!");
let db = SqlitePoolOptions::new()
.connect(path.to_str().unwrap())
.await
.unwrap();
//创建迁移文件位于./migrations/文件夹下
//cd src-tauri
//sqlx migrate add create_users_table
sqlx::migrate!("./migrations/").run(&db).await.unwrap();
db
}
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("你好, {}!Rust向你问候了!", name)
}
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, FromRow)]
struct User {
id: u16,
username: String,
email: String,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
struct UserId {
id: u16,
}
#[tauri::command]
async fn get_db_value(state: tauri::State<'_, DbState>, window: tauri::Window) -> Result<String, String> {
let db = &state.db;
let query_result:Vec<User> = sqlx::query_as::<_, User>( //查询数据以特定的格式输出
"SELECT * FROM users"
)
.fetch(db)
.try_collect()
.await.unwrap();
let mut div_content = String::new();
for user in query_result.iter(){
div_content += &format!(r#"<p>ID:{},姓名:{},邮箱:{}</p>"#, user.id, user.username, user.email);
}
// 获取当前窗口
let current_window = window.get_webview_window("main").unwrap();
let script = &format!("document.getElementById('db-item').innerHTML = '{}';",div_content);
current_window.eval(script).unwrap();
Ok(String::from("数据库读取成功!"))
}
//use tauri::Error;
#[tauri::command]
async fn insert_db_item(state: tauri::State<'_, DbState>, username: &str, email: Option<&str>) -> Result<String, String> {
let db = &state.db;
email.unwrap_or("not set yet"); //email类型为Option<&str>,其结果为None或者Some(&str)
sqlx::query("INSERT INTO users (username, email) VALUES (?1, ?2)")
.bind(username)
.bind(email)
.execute(db)
.await
.map_err(|e| format!("数据库插入项目错误: {}", e))?;
Ok(String::from("插入数据成功!"))
}
#[tauri::command]
async fn del_last_user(state: tauri::State<'_, DbState>) -> Result<String, String> {
let db = &state.db;
let last_id:UserId = sqlx::query_as::<_,UserId>("SELECT id FROM users ORDER BY id DESC LIMIT 1")
.fetch_one(db)
.await
.unwrap();
sqlx::query("DELETE FROM users WHERE id = ?1")
.bind(last_id.id)
.execute(db)
.await
.map_err(|e| format!("could not delete last user: {}", e))?;
Ok(String::from("最后一条数据删除成功!"))
}
mod tray; //导入tray.rs模块
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet, get_db_value, insert_db_item, update_user, del_last_user])
.setup(|app| {
#[cfg(all(desktop))]
{
let handle = app.handle();
tray::create_tray(handle)?; //设置app系统托盘
}
tauri::async_runtime::block_on(async move {
let db = setup_db(&app).await; //setup_db(&app:&mut App)返回读写的数据库对象
app.manage(DbState { db }); //通过app.manage(DbState{db})把数据库对象传递给state:tauri::State<'_, DbState>
});
Ok(())
})
.run(tauri::generate_context!())
.expect("运行Tauri程序的时候出错!");
}
setup_db函数新建了数据库,设置迁移(如数据库文件存在将直接使用),并将数据库返回给了全局状态:tauri::Builder::default().manager(state),这样注册的invoke handle函数就可以直接调用数据库。
对Leptos不熟悉,get_db_value函数对前端界面元素的操作也是在src-tauri\src\lib.rs中通过调用javascript脚本实现的。其实更好的办法是将数据库查询的结果通过invoke传递给前端,前端Leptos再对接收到的JsValue数据进行处理。
具体修改get_db_value函数如下:
#[tauri::command]
async fn get_db_value(state: tauri::State<'_, DbState>) -> Result<Vec<User>, String> {
let db = &state.db;
let query_result:Vec<User> = sqlx::query_as::<_, User>( //查询到的数据以特定的格式输出
"SELECT * FROM users"
)
.fetch(db)
.try_collect()
.await.unwrap();
Ok(query_result) //查询结果传递给前端
}
前端需要定义接收数据的格式,然后使用serde_wasm_bindgen::from_value()将invoke得到的JsValue数据格式转换成rust数据格式以供使用(因为前端是Leptos)。
#[derive(Serialize, Deserialize)]
struct User {
id: u16,
username: String,
email: String,
}
#[component]
pub fn App(initial_value:i32) -> impl IntoView {
......
let get_db_item = move |ev: SubmitEvent| {
ev.prevent_default(); //类似javascript中的Event.preventDefault()
spawn_local(async move {
let js_vec = invoke_without_args("send_db_item").await; //调用得到的数据类型为JsValue
//使用serde_wasm_bindgen::from_value()将JsValue数据格式转换成rust数据格式
let rust_vec: Vec<User> = serde_wasm_bindgen::from_value(js_vec).map_err(|_| JsValue::from("Deserialization error")).unwrap();
let mut receive_msg = String::from("读取数据库ID序列为:[");
for user in rust_vec{
receive_msg += &format!("{}, ", user.id);
}
receive_msg += "]";
/*在此处理invoke过来的数据*/
set_insert_msg.set(receive_msg);
});
};
}
3. 前端Leptos调用
主要是界面元素设计(包括style.css)和Invoke调用,对于没有参数的调用,要增加如下设置:
#[wasm_bindgen]
extern "C" {
+ #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)]
+ async fn invoke_without_args(cmd: &str) -> JsValue;
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] //Tauri API 将会存储在 window.__TAURI__ 变量中,并通过 wasm-bindgen 导入。
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}
src/app.rs文件内容如下:
use leptos::task::spawn_local;
use leptos::{ev::SubmitEvent, prelude::*};
use serde::{Deserialize, Serialize}; //#derive(serialize, Deserialize)用于请求响应传递参数的序列化
use wasm_bindgen::prelude::*;
//WASM(WebAssembly)是一种低级字节码格式,可以从C、C++、Rust等高级语言编译而来,旨在在Web端实现接近原生的执行效率
//wasm 并不是传统意义上汇编语言(Assembly),而是一种中间编译的字节码,可以在浏览器上运行非 JavaScript 编写的代码
//wasm-bindgen主要目的是实现Rust与现有JavaScript环境的无缝集成,自动生成必要的绑定和胶水代码,确保Rust函数和JavaScript之间平滑通信
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)]
async fn invoke_without_args(cmd: &str) -> JsValue;
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] //Tauri API 将会存储在 window.__TAURI__ 变量中,并通过 wasm-bindgen 导入。
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}
//derive是一个编译器指令,在类型定义(如结构体或枚举)上添加#[derive(...)]让编译器为一些特性提供基本的实现,
//...表示要为其提供基本实现的特性列表。#[derive(Serialize, Deserialize)],可以轻松地为结构体实现序列化和反序列化功能。
//要在Rust中使用序列化和反序列化,首先需要在Cargo.toml文件中引入serde库
//serde = { version = "1.0", features = ["derive"] }
//serde_json = "1.0"
//序列化后的变量作为函数invoke(cmd, args: JsValue)的参数,JsValue为序列化格式
#[derive(Serialize, Deserialize)]
struct GreetArgs<'a> {
name: &'a str,
}
#[derive(Serialize, Deserialize)]
struct InsertArgs<'a> { //生命周期标识符 'a 用于帮助编译器检查引用的有效性,避免悬垂引用和使用已被释放的内存。
username: &'a str,
email: &'a str,
}
#[derive(Serialize, Deserialize)]
struct User {
id: u16,
username: String,
email: String,
}
//Component是一个重要的概念,它通常指的是项目中的一部分代码或功能模块。
#[component]
pub fn App(initial_value:i32) -> impl IntoView { //函数返回IntoView类型,即返回view!宏,函数名App()也是主程序view!宏中的组件名(component name)。
let (name, set_name) = signal(String::new());
let (greet_msg, set_greet_msg) = signal(String::new());
let (value, set_value) = signal(initial_value);
let (username, set_username) = signal(String::new());
let (email, set_email) = signal(String::new());
let (insert_msg, set_insert_msg) = signal(String::new());
//let (fetch_msg, set_fetch_msg) = signal(String::new());
let update_name = move |ev| { //将闭包传递给按钮组件的onclick参数,鼠标点击触发事件,更新之前创建signal中的值。
let v = event_target_value(&ev);
set_name.set(v);
};
let get_db_item = move |ev: SubmitEvent| {
ev.prevent_default(); //类似javascript中的Event.preventDefault(),处理<input>字段非常有用
spawn_local(async move { //使用Leptos的spawn_local创建一个本地线程(local_thread)Future, 提供一个异步move闭包。
let new_msg = invoke_without_args("get_db_value").await.as_string().unwrap();
set_insert_msg.set(new_msg);
});
};
let greet = move |ev: SubmitEvent| {
ev.prevent_default(); //类似javascript中的Event.preventDefault(),处理<input>字段非常有用
spawn_local(async move { //使用Leptos的spawn_local创建一个本地线程(local_thread)Future, 提供一个异步move闭包。
let name = name.get_untracked(); //防止通常由Leptos signal产生的反应式绑定,因此即使值发生变化,Leptos也不会尝试更新闭包。
if name.is_empty() {
return;
}
let args = serde_wasm_bindgen::to_value(&GreetArgs { name: &name }).unwrap(); //参数序列化
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
let new_msg = invoke("greet", args).await.as_string().unwrap(); //使用invoke调用greet命令,greet类似于API
set_greet_msg.set(new_msg);
});
};
let update_user = move |ev| { //将闭包传递给按钮组件的onclick参数,更新之前创建signal中的值。
let v = event_target_value(&ev);
set_username.set(v);
};
let update_email = move |ev| { //将闭包传递给按钮组件的onclick参数,更新之前创建signal中的值。
let v = event_target_value(&ev);
set_email.set(v);
};
let insert_db_item = move|ev:SubmitEvent| {
ev.prevent_default();
spawn_local(async move {
let username = username.get_untracked();
let email = email.get_untracked();
if username.is_empty() {
return;
}
let args = serde_wasm_bindgen::to_value(&InsertArgs { username: &username, email:&email }).unwrap(); //参数序列化
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
let new_msg = invoke("insert_db_item", args).await.as_string().unwrap_or(String::from("Insert Data Failed!!!")); //使用invoke调用insert_db_item命令,greet类似于API
set_insert_msg.set(new_msg);
});
};
let del_last_item = move|ev:SubmitEvent| {
ev.prevent_default();
spawn_local(async move { //使用Leptos的spawn_local创建一个本地线程(local_thread)Future, 提供一个异步move闭包。
let new_msg = invoke_without_args("del_last_user").await.as_string().unwrap_or(String::from("Delete Data Failed!!!"));
set_insert_msg.set(new_msg);
});
};
let clear = move |_| {set_value.set(0);};
let decrement = move |_| {set_value.update(|v| *v -=1);}; //update()函数接受闭包,参数是signal的setter值set_value
let increment = move |_| {set_value.update(|v| *v +=1);};
view! { //view!宏作为App()函数的返回值返回IntoView类型
<main class="container">
<h1>"Welcome to Tauri(2.1.1) + Leptos(0.7.2)"</h1>
<div class="row">
<a href="https://tauri.app" target="_blank">
<img src="public/tauri.svg" class="logo tauri" alt="Tauri logo"/>
</a>
<a href="https://docs.rs/leptos/" target="_blank">
<img src="public/leptos.svg" class="logo leptos" alt="Leptos logo"/>
</a>
<a href="https://scybbd.com" target="_blank">
<img src="public/doughnutS.svg" class="logo scybbd" alt="YislWll's website"/>
</a>
</div>
<p>"点击Tauri、Leptos和Scybbd的logo了解更多..."</p>
<form class="row" id="greet-form" on:submit=greet>
<input
id="greet-input"
placeholder="请输入一个名字..."
on:input=update_name
/>
<button type="submit" id="greet-button">"打招呼"</button>
</form>
<p>{ move || greet_msg.get() }</p>
<div>
<button style="margin:0 10px 0 10px;" on:click = clear>"清零"</button>
<button style="margin:0 10px 0 10px;" on:click = decrement>"-1"</button>
<span style="margin:0 10px 0 10px;" class:red=move||{value.get()<0}>"数值:"{value}</span> //当值小于0时,字体变成红色
<button style="margin:0 10px 0 10px;" on:click = increment>"+1"</button>
</div>
<p></p>
<form class="row" id="insert-form" on:submit=insert_db_item>
<input
id="user-input"
placeholder="请输入姓名..."
on:input=update_user
style="margin:0 10px 0 10px;"
/>
<input
id="email-input"
placeholder="请输入邮箱(可选)..."
on:input=update_email
style="margin:0 10px 0 10px;"
/>
<button type="submit" id="insert-button" style="margin:0 10px 0 10px;" >"插入数据库"</button>
</form>
<p class="info">数据库变动信息:{ move || insert_msg.get() }</p>
<div class="form-container">
<div class="db-window" id="db-item">
<p></p>
</div>
<div class="btn-window">
<form class="row" on:submit=get_db_item>
<button type="submit" style="margin:10px 5px 10px 5px;" id="get-button" style="margin:0 10px 0 10px;" >"读取数据库"</button>
</form>
<form class="row" on:submit=del_last_item>
<button type="submit" style="margin:10px 5px 10px 5px;" id="del-button" style="margin:0 10px 0 10px;" >"删除最后项"</button>
</form>
</div>
</div>
</main>
}
}
Leptos 0.7版与0.6版有所区别,create_signal换成了signal。
至此,Tuari+Leptos应用程序通过sqlx对数据库进行操作的简单功能基本完成。