likes
comments
collection
share

Tauri开发二: 调用后端Rust接口

作者站长头像
站长
· 阅读数 20

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.rsmain.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/apinpm install @tauri-apps/api 我们创建一个文件src/lib/Greet.sveltesrc是前端代码的目录), 添加下面的代码:

<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,查看功能是否可用 Tauri开发二: 调用后端Rust接口

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 CrateBinay 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

  1. 切换命令行到src-tauri: cd src-tauri
  2. 运行rust test的命令为cargo test
  3. 运行结果: 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,包括下面的Module tauri_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.tomltauri = { 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

安装完成后,我们就直接可以使用断点调试: Tauri开发二: 调用后端Rust接口

5. 后续

后续我们会使用上面的流程,来开发一款类似于Adobe Lightroom的照片管理软件,来实现我们特殊的需求,因为Adobe Lightroom更加倾向于摄影爱好者,我们的软件会更加倾向于普通用户,比如以下功能:

  • 照片重复导入
  • 相似照片的归类显示
  • 支持制作抖音视频

同时在过程中会探索Tauri的各种能力: fs events window ...

To Be Continued....