Tauri开发二: 调用后端Rust接口
Tauri开发二: 调用后端Rust接口
代码位置: gitee.com/dassio/lear…
2023-07-23 update:集成测试重构 2023-07-24 update:集成测试VS Code单步调试
在Tauri开发一: Tauri开发工具介绍我们介绍了如何创建项目 但是项目运行起来后,前端和后端是完全分离的,互相之间完全没有交互 此章节我们主要看前端是如何调用后端的
1. Rust工程结构和Crate概念
在介绍调用后端接口之前,我们先介绍Rust几个组织代码的概念:
- Package
- Crate
- Module
而他们之间的关系为: Package Crate 和 Module 的关系是: Package 包含 Crate , Crate 包含 Module
同时Rust规定了他们三个之间的规则:
- 一个rust工程是一个Package
- 一个rust源码文件默认是一个Moudule:
lib.rs
和main.rs
除外,他们默认是一个Crate - 一个Package只能包含一个library Crate, 但是可以包含多个 binary Crate
2. 创建后端接口
tauri的接口就是Rust的函数,只需要在函数上面添加宏#[tauri::command]
即可
2.1 在main.rs
中创建接口
在main.rs
中增加以下代码:
#[tauri::command]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
我们需要告诉tauri这个接口,在main
函数中增加.invoke_handler(tauri::generate_handler![greet])
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
2.2 调用后端接口
在上一篇文正中我们讲过,tauri前端的主要组成就是 @tauri-apps/api
, 他负责和后端的IPC通信
安装 @tauri-apps/api
:npm install @tauri-apps/api
我们创建一个文件src/lib/Greet.svelte
(src是前端代码的目录), 添加下面的代码:
<script>
import { invoke } from '@tauri-apps/api/tauri'
let name = ''
let greetMsg = ''
async function greet() {
greetMsg = await invoke('greet', { name })
}
</script>
<div>
<input id="greet-input" placeholder="Enter a name..." bind:value="{name}" />
<button on:click="{greet}">Greet</button>
<p>{greetMsg}</p>
</div>
然后在src/routes/+page.svelte
里面调用:
<script>
import Greet from '../lib/Greet.svelte'
</script>
<h1>Welcome to SvelteKit</h1>
<Greet />
运行cargo tauri dev
,查看功能是否可用
2.3 拆分代码
我们当前的rust工程,只有一个main.rs
源码文件,这是一个crate,但是crate的名字不是main,main.rs
对应的crate名字就是package的名字,而package的名字是在Cargo.toml
定义的,Tauri默认的是app
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
#[tauri::command]
宏只能在binary crate里面使用,也就是main.rs
文件里面,但是如果我们的后端接口非常多的时候,我们的main.rs
就会变得非常臃肿,不方便维护。
记得我们前面说过,crate的下一级是module,因此我们可以把接口相关的内容都移动到一个tauri_commands
Module里面
创建一个文件src-tauri\src\tauri_commands.rs
,将commands的代码移动到里面:
一个rust源文件,默认就是一个Module
#[tauri::command]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
同时在main.rs
里面增加 mod tauri_commands
并将.invoke_handler(tauri::generate_handler![greet])
替换为.invoke_handler(tauri::generate_handler![tauri_commands::greet])
正常情况下,到这里我们的介绍应该完成了。 但是作为一个程序员,实际工作中我们的工作量不止这么点,我们有很多的精力花在了修改转测之后的bug上,原因就是因为我们没有做足够的测试,或者说 我们没有吧用户的行为测试,作为我们编码的最终目标。而这正好是TDD解决的问题。
3. TDD开发方式
正常的前后端分离的业务开发中,前后端都是分开测试的,测试使用的工具也不一样。我们当前的后端相当于只有一个API,我们现在就用TDD的方式来继续完善我们的代码
3.1 后端TDD
3.1.1 Rust集成测试介绍
Rust支持单元测试,但是我一直认为,单元测试(unit test)的功能,应该通过用户行为出发,真正的TDD应该只测试用户行为,因此我们这里只使用集成测试(integration test)
Rust的集成测试有一个非常重要的限制: 集成测试只能够测试library Crate
前面我们说过Package包含Crate, 但是Crate也分为两种: Library Crate
和 Binay Crate
.
Binay Crate
就是我们前面遇到的main.rs
,这个Crate的名字和Package名字一致,都是在Cargo.toml
里面定义的
而lib.rs
文件对应的就是Library Crate
, 而且 一个工程下面只能有一个Library Crate
如果我们创建工程的时候使用命令:
cargo new my_project --lib
, cargo就会给我们创建一个目录,在src
目录下面不是main.rs
, 而是lib.rs
。
因此当前我们有两个主要的限制:
- 集成测试只能够测试
Library Crate
- 一个工程下面只能有一个
Library Crate
这导致我们需要创建一个lib.rs
作为Library Crate
, 然后我们测试lib.rs
里面的代码
而此时的main.rs
或者所属的Crate仅仅是一个薄薄的封装层,负责调用lib.rs
对应的crate业务代码
如果你写过spring代码,
main.rs
对应的Crate有点类似于Controller,lib.rs
对应的Crate有点类似于业务Service
我们不测试
main.rs
,只测试lib.rs
的方式,有点类似于 Spring TestContext Framework 而直接测试main.rs
有点类似于 Spring WebTestClient
3.1.2 增加业务层(lib.rs
对应的Crate)
创建文件 src-tauri\src\lib.rs
, 增加如下代码:
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
同时更新我们tauri_commands.rs
为简单的调用:
lib.rs
对应的Crate名称是和main.rs
一致的,都叫做app
, 因此我们调用的时候使用app::greet()
#[tauri::command]
pub fn greet(name: &str) -> String {
app::greet(name)
}
运行一下业务,保证当前正常
3.1.3 TDD开发循环
3.1.3.1 搭建测试环境
集成测试默认的存在路径为src-tauri\tests
,我们新建目录,并创建文件src-tauri\tests\test_greeting.rs
- 切换命令行到
src-tauri
:cd src-tauri
- 运行rust test的命令为
cargo test
- 运行结果: cargo 默认运行三种类型的测试, 我们可以看到第二个测试用例为零
- 单元测试(
Running unittests src\main.rs
) - 集成测试(
Running tests\test_greeting.rs
) - Doc-Tests(
Doc-tests app
)
- 单元测试(
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src\main.rs (target\debug\deps\app-7826712535976156.exe)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests\test_greeting.rs (target\debug\deps\test_greeting-437d74c127a81ecb.exe)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests app
3.1.3.2 编写第一个测试用例
TDD的时候,我们写的一个用例经常是空输入的用例,这个地方对应的就是输入的name
是空字符串
此时我们希望程序返回什么呢,当前的返回是Hello,
,但是这个测试肯定会给你提单,我们应该更人性化一点,比如放回Please input your name!
由于每个测试的rust源码文件,也是一个Crate,因此我们需要引用我们的
Libraay Crate
,才能调用里面的代码 上面提到我们的lib.rs
对应的Crate的名字是在Cargo.toml
里面定义的,而Tauri默认的是app
use app;
#[test]
fn greet_empty_name_correct() {
assert_eq!(app::greet(""), "Please input your name!");
}
运行cargo test
: 可以看到用例失败了,这和我们TDD开发模式是一样的,下一步就是修改代码,让用例通过
Running tests\test_greeting.rs (target\debug\deps\test_greeting-437d74c127a81ecb.exe)
running 1 test
test greet_empty_name_correct ... FAILED
修改lib.rs
代码为:
pub fn greet(name: &str) -> String {
if name.len() == 0 {
return "Please input your name!".to_string();
}
format!("Hello, {}!", name)
}
再次运行cargo test
, 没有失败的用例
3.1.3.3 编写第二个测试用例
我们再从使用者的角度考虑,或者从测试的角度考虑,如果输入的都是空格呢,我们应该希望和什么都不输入的时候,返回的结果一样,因此我们再增加两个测试用例:
#[test]
fn greet_single_space_name_correct() {
assert_eq!(app::greet(" "), "Please input your name!");
}
#[test]
fn greet_multi_spaces_name_correct() {
assert_eq!(app::greet(" "), "Please input your name!");
}
再次运行用例,结果为:新增的两个用例失败
failures:
greet_multi_spaces_name_correct
greet_single_space_name_correct
test result: FAILED. 1 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--test test_greeting`
再次修改代码,并运行用例,测试用例全部通过
pub fn greet(name: &str) -> String {
if name.len() == 0 || name.trim().len() == 0 {
return "Please input your name!".to_string();
}
format!("Hello, {}!", name)
}
上面的的过程我们可以一直继续下去,尽量覆盖用户的常用场景:
- 超长输入
- 特殊字符
- ........
这个就是TDD的开发流程,这样开发,有如下好处:
- 能让我们专注于用户的需求,而不是中间为了各种架构而跑偏
- 保证老的功能一直是可用的,我们总是在前进,没有因为完成了一个新的功能,把前面的都搞坏,搞来搞去,挫折感大增
2023-07-23更新
4. 代码重构
随着对Tauri和Rust的学习,发现前面的代码有两个问题:
- 我们的集成测试,并不是真正的集成测试,我们还是测试的某个函数
- 系统运行的时候,我们获取不到一些参数:用户的配置文件夹,还有其他的一些运行时的数据
- Rust推崇的是
Binary Crate
应该是薄薄的一个调用层,而我们的main.rs
,包括下面的Moduletauri_commands.rs
太厚重了
因此我们对工程做一些修改:
- 将大部分的代码移动到
lib.rs
对应的Library Crate
里面 - 我们使用IPC来调用后端的接口,这时系统已经完全拉起,更能贴近用户的行为
4.1 将代码移出main.rs
在lib.rs
中创建create_app
方法:
pub fn create_app<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::App<R> {
builder
.build(tauri::generate_context!())
.expect("error while running tauri application")
}
将main.rs
里面的调用此方法,直接调用run
方法,此时main.rs
只有调用,没有各种初始化的业务逻辑
app::create_app(tauri::Builder::default())
.run(|_app_handle, event| match event {
tauri::RunEvent::ExitRequested { api, .. } => {
api.prevent_exit();
}
_ => {}
});
将#[tauri::command]
直接放到lib.rs
的方法上面,同时在创建APP的函数里面注册Commands
pub fn create_app<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::App<R> {
builder
.invoke_handler(tauri::generate_handler![greet]) // 新增加
.build(tauri::generate_context!())
.expect("error while running tauri application")
}
#[tauri::command] // 直接将函数标记为Tauri Command,且去掉 pub
fn greet(name: &str) -> String {
if name.len() == 0 || name.trim().len() == 0{
return "please input your name".to_string();
}
format!("Hello, {}!", name)
}
如上,我们新增加了.invoke_handler(tauri::generate_handler![greet])
调用,并将greet
函数直接标记为Tauri的后端接口(Command), 同时设置为私有
主要需要将
gree
函数变为私有,否则编译不通过,issue见:github.com/tauri-apps/…
此时,tauri_command.rs
可以直接删除了,同时将main.rs
里面的mod tauri_commands
删除
4.2 通过IPC调用后端接口
前面我们的集成测试,只是测试了lib.rs
的函数,没有将整个应用拉起来,这时有几个问题:
- 应用的初始化我们无法测试
- 应用运行时的各种中间状态参数我们无法获取
我们希望想Spring的Context测试一样,能将整个应用拉起来,尽量贴合用户的行为
这时我们需要借助到一个Module tauri::test
, 在Cargo.toml
的tauri = { version = "1.4.0", features = [] }
feature中增加test变成: tauri = { version = "1.4.0", features = ["test"] }
测试代码示例:
use app::create_app;
use serde_json::json;
use tauri::Manager;
use tauri::test::{assert_ipc_response, mock_builder};
#[test]
fn greet_name_correct() {
let app = create_app(mock_builder());
let window = app.get_window("main").unwrap();
assert_ipc_response(
&window,
tauri::InvokePayload {
cmd: "greet".to_string(),
tauri_module: None,
callback: tauri::api::ipc::CallbackFn(0),
error: tauri::api::ipc::CallbackFn(1),
inner: json!({
"name" : "danny"
}),
},
Ok("Hello, danny!"),
);
}
运行测试, 四个测试用例都通过了:
cd src-tauri
cargo test
...
Running tests\greeting_test.rs (target\debug\deps\greeting_test-3eba86ba933bb885.exe)
running 4 tests
test greet_empty_name_correct ... ok
test greet_single_space_name_correct ... ok
test greet_name_correct ... ok
test greet_spaces_name_correct ... ok
...
4.3 单步调试
有了上面的集成测试以后,我们就可以方便的调试代码 VS Code安装Rust Analyser 和 CodeLLDB, CodeLLDB可能会出现下载失败的情况,请直接下载安装包:CodeLLDB VS Code安装包, 然后使用命令行安装:
code --install-extension codelldb-x86_64-windows.vsix
安装完成后,我们就直接可以使用断点调试:
5. 后续
后续我们会使用上面的流程,来开发一款类似于Adobe Lightroom的照片管理软件,来实现我们特殊的需求,因为Adobe Lightroom更加倾向于摄影爱好者,我们的软件会更加倾向于普通用户,比如以下功能:
- 照片重复导入
- 相似照片的归类显示
- 支持制作抖音视频
同时在过程中会探索Tauri的各种能力: fs events window ...
To Be Continued....
转载自:https://juejin.cn/post/7257776570449002533