当前位置: 首页 > news >正文

Tauri2+Leptos开发桌面应用--Sqlite数据库操作

 在之前工作(使用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_tablesqlx::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类似于APIset_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类似于APIset_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_valuelet 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><inputid="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><inputid="user-input"placeholder="请输入姓名..."on:input=update_userstyle="margin:0 10px 0 10px;" /><inputid="email-input"placeholder="请输入邮箱(可选)..."on:input=update_emailstyle="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对数据库进行操作的简单功能基本完成。 


http://www.mrgr.cn/news/81824.html

相关文章:

  • 深度学习-稀疏卷积
  • 微信小程序Uniapp
  • python脚本加载ui页面:PySide6设计的页面
  • 【MySQL】聚集索引、二级索引
  • 【Python运维】自动化备份与恢复系统的实现:Python脚本实战
  • LangChain教程 - 表达式语言 (LCEL) -构建智能链
  • 进军AI大模型-环境配置
  • vue Promise使用
  • Python爬虫(一)- Requests 安装与基本使用教程
  • 二叉树相关性质及其遍历
  • 【老白学 Java】第一次绘制
  • 阿里云人工智能工程师ACA认证免费课程学习笔记
  • Maven + MyBatis
  • MoH:将多头注意力(Multi-Head Attention)作为头注意力混合(Mixture-of-Head Attention)
  • Vuex用法
  • C++进阶重点知识(一)|智能指针|右值|lambda|STL|正则表达式
  • springboot整合log4j2案例以及异常输出格式案例2
  • 【SpringMVC】REST 风格
  • Pytorch | 利用DTA针对CIFAR10上的ResNet分类器进行对抗攻击
  • ipad如何直连主机(Moonlight Sunshine)
  • 【Linux学习五】时间日期指令与查找指令
  • 将现有Web 网页封装为macOS应用
  • Shell的变量功能
  • 学习记录—正则表达式-基本语法
  • 总结TCP/IP四层模型
  • jdk动态代理和cglib动态代理对比