iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0
Software Development

前端? 後端? 摻在一起做成全端就好了系列 第 13

13 Tauri 該你上場了 - rust 桌面應用

  • 分享至 

  • xImage
  •  

咦,到現在都還沒讓主角Tauri上場,(Tauri:我不只是路過的啊,往前站了不只一點點)。

在Rust中使用http client

我們先把tauri當成前端來使用,所以試著用rust寫一個http Client,tauri文件裡有提到reqwest,也算rust裡滿有名的http client之一,既然tauri都使用它了,我們要寫在tauri裡面,也跟著搭便車使用吧,直接在Cargo.toml把 reqwest加進來:

@@ Cargo.toml @@
 [workspace.dependencies]
 dotenvy = { version = "0.15" }
 rand = { version = "0.8" }
+reqwest = { version = "0.11" }

@@ app/src-tauri/Cargo.toml @@
 [dependencies]
+reqwest = { workspace = true }
 tauri = { version = "1.4", features = ["shell-open"] }
+serde = { workspace = true }
-serde = { version = "1.0", features = ["derive"] }
+serde_json = { workspace = true }
-serde_json = "1.0"

看到tauri裡我們當初安裝的template也預設有裝serdeserde_json,順便把它改成跟workspace同版本。

加好了來試一下吧,我們直接看reqwest的範例,reqwest需要tokio,我們workspace已經有了,補到tauri的Cargo.toml裡就好,另外範例的reqwest有開json功能,我們也開起來:

@@ Cargo.toml @@
 rand = { version = "0.8" }
+reqwest = { version = "0.11", features = ["json"] }
-reqwest = { version = "0.11" }

@@ app/src-tauri/Cargo.toml @@
 serde_json = { workspace = true }
+tokio = { workspace = true }

我們照樣照句,在main裡照著旁邊的範例寫起來:

// app/src-tauri/src/main.rs
use std::collections::HashMap;

#[tauri::command]
async fn http_test() -> Result<(), Box<dyn std::error::Error>> {
    let resp = reqwest::get("https://httpbin.org/ip")
        .await?
        .json::<HashMap<String, String>>()
        .await?;
    println!("{:#?}", resp);

    Ok(())
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            greet,
            http_test,    // 這裡要把我們設的command加進來,前端才能呼叫
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

rust提醒tauri command限制

又開始要解任務了,這裡說放到tauri裡的command不認我們fn的簽章,看一下官方文件的說明,command回傳的型別要實作serde::Serialize這個trait,並且他也提供範例,我們直接照著做一個Error:

// app/src-tauri/src/main.rs
#[derive(Debug, thiserror::Error)]
enum Error {
    #[error(transparent)]
    Io(#[from] std::io::Error),
}

// we must manually implement serde::Serialize
impl serde::Serialize for Error {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: serde::ser::Serializer,
    {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

#[tauri::command]
async fn http_test() -> Result<(), Error> { // 改成上面的Error

記得要在tauri裡裝thisError

 tauri = { version = "1.4", features = ["shell-open"] }
+thiserror = { workspace = true }
 serde = { workspace = true }

rust提示未轉換reqwest的error

這個之前有發生過,就是我們在做Error mapping的去套From這個trait,不過reqwest提供的範例好像可以照樣造句,我們試試看:

 @@ app/src-tauri/src/main.rs @@
 enum Error {
     #[error(transparent)]
     Io(#[from] std::io::Error),
+    #[error(transparent)]
+    Http(#[from] reqwest::Error),
 }

編譯通過了,我們來看一下,從前端測試一下,不過前端可能要先做一個按鈕去觸發,我們一樣在about裡poc,之前放的Greet還留著舊的呼叫tauri範例,就照著複刻一組:

<!-- app/src/lib/Greet.svelte -->
<script lang="ts">
  import { invoke } from '@tauri-apps/api/tauri';
  // ...略
  const test_api = async () => {
    const res = await invoke('http_test');
    console.log(res);
  }
</script>

<button
  class="bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded"
  on:click={test_api}> 測試tauri reqwest
</button>

加呼叫tauri reqweust的畫面

按下去看看:

測試呼叫tauri後的終端畫面
有印出東西來,看起來work了。

依然是 依然是 CR和UD

開始重組我們的tauri結構,先把error移走,然後新增api client檔案:

app/src-tauri/src/
├── error.rs           
├── main.rs
└── tic_tac_toe
    ├── mod.rs
    └── rest_api.rs
// app/src-tauri/src/main.rs
mod error;
// app/src-tauri/src/error.rs
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error(transparent)]
    Http(#[from] reqwest::Error),
}

impl serde::Serialize for Error {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: serde::ser::Serializer,
    {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

BJ4,再來刪掉剛剛的test並建立CRUD的API:

@@ app/src-tauri/src/main.rs @@
+mod tic_tac_toe;

-use std::collections::HashMap;
-use error::Error;

-#[tauri::command]
-async fn http_test() -> Result<(), Error> {
-    let resp = reqwest::get("https://httpbin.org/ip")
-        .await?
-        .json::<HashMap<String, String>>()
-        .await?;
-    println!("{:#?}", resp);

-    Ok(())
-}
...
             greet,
-            http_test,
         ])

api 要引用核心的Game結構體,所以引入core的引用,有了上次撞名的經驗,這次直接先取用不同名字:

@@ app/src-tauri/Cargo.toml @@
[dependencies]
+my-core = { path = "../../core", package = "core" }

之後可能會加其他東西,先把rest_api獨立拆出一個rs檔案

// app/src-tauri/src/tic_tac_toe/mod.rs
pub mod rest_api;

先照著剛剛的api改一下,先實作兩個get_gamenew_game,get照剛剛範例使用reqwest::get方法即可,但post要用reqwest::Client,我們看以下怎麼寫的:

// app/src-tauri/src/tic_tac_toe/rest_api.rs
use crate::error::Error;
use my_core::tic_tac_toe::Game;        

#[tauri::command]                // 要給 WebView 呼叫,要加這個
pub async fn get_game(id: usize) -> Result<Game, Error> {
    let url = format!("http://localhost:3030/tic_tac_toe/{}", id);
    let game = reqwest::get(url) // url 需要串網址放id,使用format組字串
        .await?                  // 這裡會得到 reqwest::Response 的結構體
        .json::<Game>()          // 把Response用json反序列化為Game結構體
        .await?;                 // 解析Json是Future<完畢取得結果
    Ok(game)                     // 以上?會把Error外拋,若沒有,這裡會回傳game結構體
}

#[tauri::command]
pub async fn new_game() -> Result<(isize, Game), Error> {
    let url = format!("http://localhost:3030/tic_tac_toe");
    let client = reqwest::Client::new();   // 建立 client
    let game = client.post(url)            // 設定post及網址
        .send()                            // 送出request要求
        .await?                            // await取得 Result<Response, Error>
        .json::<(isize, Game)>()           // 維持原API的輸出讓前端API介面一致
        .await?;                           // Result<(isize, Game)>, 
                    
    Ok(game)  
}

在 rust 裡非同步的使用, await 其實是接在 Future trait後面,而async會把回傳值用impl Future<T...>包起來,await會等待Future得到結果才會繼續往下處理,在等待的過程(透過tokio runtime的協調,不會造成整個程式hang住)。

這裡先專注在把後端(前端的後端)寫對後,再來處理前端(前端的前端),所以還是要到前端呼叫一下,先在main補上tauri command的註冊:

// app/src-tauri/src/main.rs
use tic_tac_toe::rest_api::{get_game, new_game};

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            greet,
            get_game, new_game, // 這裡加入我們剛剛寫的api client
        ])
// app/src/lib/Greet.svelte
<script lang="ts">
  const get_game = async (id) => {
    const res = await invoke('get_game', {id});
    console.log(res);
  }
  const new_game = async () => {
    const res = await invoke('new_game');
    console.log(res);
  }
</script>

<button
  class="bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded"
  on:click={() => get_game(1)}>
  Get Game
</button>
<button
  class="bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded"
  on:click={() => new_game()}>
  NEW Game
</button>

接著到我們Tauri裡的about頁面,開啟開發人員視窗,按一下這兩個按鈕

畫面補上 get 和 new兩個按鈕

開啟開發人員工具,檢視是否有正確輸入呼叫API後的資料

觀看devtool觸發按鈕的反應

看起來是ok的,記得小步完成時,git 先 stage起來或進行commit後再繼續往下。

Rust reqwest 錯誤處理

我們把剛剛get_game的數字調成一個不存在的數,結果就理所當然的報錯。

@@ app/src/lib/Greet.svelte @@
+  on:click={() => get_game(1111)}>
-  on:click={() => get_game(1)}>

devtool裡console報錯畫面

官方有說明前端呼叫Tauri的錯誤要怎麼處理,rust 的 Error回傳到前端會是Promise的reject,記得之前建議要去看的鐵道模型嗎:

鐵道開發示意圖

圖片來源:https://fsharpforfunandprofit.com/posts/recipe-part2/

原圖有點小,我放大一下:
白話版鐵道開發示意圖

我知道很多朋友沒有時間去看,在這裡簡單扼要說明一下:原圖中的三個function,上面的通道是正確的路徑,下面的通道是錯誤的路徑,所以每個function接到前手的輸入後,如果是錯誤,就直接把錯誤交給下一棒(以我們的案例可能直接傳或做error mapping後再傳),如果正確輸入就依處理後的結果,如果正確就繼續走上面的通道,如果執行出現錯誤就會轉到下面的通道。

所以在Rust中使用雙軌制(?,就不像其他語言處理例外需要額外的成本,可以看一下C#Java的一些討論。

我們在tauri裡的錯誤要配合後端web api傳遞,api回傳的資料是JSON,已經是序列化後的資料,所以我們在這裡序要再進行序列化為rust的物件,我們加上一個錯誤類別:

// app/src-tauri/src/error.rs
#[derive(serde::Serialize, serde::Deserialize)]  // 要能轉json
pub struct ErrorResponse {                       // 配合後端api的錯誤訊息建立
    message: String,
    details: Option<String>,
}

impl From<reqwest::Error> for ErrorResponse {    // 加這個才能用語法糖?來簡化寫法
    fn from(value: reqwest::Error) -> Self {
        ErrorResponse {
            message: "主機連線失敗".into(),
            details: Some(value.to_string()),
        }
    }
}

修改tauri command 的錯誤處理:

// app/src-tauri/src/tic_tac_toe/rest_api.rs
use crate::error::{Error, ErrorResponse};

#[tauri::command]
pub async fn get_game(id: usize) -> Result<Game, ErrorResponse> {
    let url = format!("http://localhost:3030/tic_tac_toe/{}", id);
    let game = reqwest::get(url).await?;      // Response
    if game.status().is_success() {           // 2xx的代碼代表成功
        let game = game.json::<Game>().await?;
        Ok(game)       // 成功回傳 game 物件
    } else {
        let error = game.json::<ErrorResponse>().await?;
        Err(error)     // 失敗則解析後端api回傳的body訊息再傳給前端js
    }
}

調整typescript處理呼叫API的錯誤訊息:

// app/src/lib/Greet.svelte
const get_game = async (id) => {
    try {
      const res = await invoke('get_game', {id});
      console.log(res);    // await執行成功,印出正確結果
    } catch (e) {
      console.log(e);      // 執行失敗,接到error印出
    }
  }  

以上把前後端對應的錯誤處理一併寫好,再試試看:
加入錯誤處理後,前端呼叫API的錯誤正確打印

完成 http client for CRUD

現在正確和錯誤都可以正確解析了,繼續加api:

// app/src-tauri/src/tic_tac_toe/rest_api.rs
#[tauri::command]
pub async fn play_game(id: usize, num: usize) -> Result<Game, ErrorResponse> {
    let url = format!("http://localhost:3030/tic_tac_toe/{id}/{num}");
    let client = reqwest::Client::new();
    let game = client.put(url).send().await?;
    if game.status().is_success() {
        let game = game.json::<Game>().await?;
        Ok(game)
    } else {
        let error = game.json::<ErrorResponse>().await?;
        Err(error)
    }
}

寫完後發現後半段的if區塊都是重覆的代碼,能不能縮減呢,先抽fn如下:

async fn unwrap_game(game: reqwest::Response) -> Result<Game, ErrorResponse>
{
    if game.status().is_success() {
        let game = game.json::<Game>().await?;
        Ok(game)    // 這裡沒分號,表回傳值
    } else {
        let error = game.json::<ErrorResponse>().await?;
        Err(error)  // 這裡沒分號,表回傳值
    }               // 這裡沒分號,整個 if 的結果就是這一整個fn的回傳值
}

不過這個fn只有play_gameget_game可以使用,new_game的類別是(isize, Game)不一樣,無法使用,但是又跟這很像,能不能有方法一起用呢,有的,就是泛型,我們使用泛型來改寫這個fn,泛型的語法是用<T>

// <T> 要加在 function 名稱後,隨後可使用在fn的參數類、回傳類,或fn裡面使用的到的類
async fn unwrap_game<T>(game: reqwest::Response) -> Result<T, ErrorResponse>
{
    if game.status().is_success() {
        let game = game.json::<T>().await?;    // 這裡使用 T
        Ok(game)
    } else {
        let error = game.json::<ErrorResponse>().await?;
        Err(error)
    }
}

rust警告json泛型T需要實作Deserialize

這裡出現了錯誤,因為我們在內文中使用json::<T>,所以這裡要求我們設的T類別必需要符合json方法裡的T限制,我們可以用where條件來寫泛型的限制:

async fn unwrap_game<T>(game: reqwest::Response) -> Result<T, ErrorResponse>
where T: serde::de::DeserializeOwned

調整完最後的結果如下:

// app/src-tauri/src/tic_tac_toe/rest_api.rs
use crate::error::ErrorResponse;
use my_core::tic_tac_toe::Game;

#[tauri::command]
pub async fn new_game() -> Result<(isize, Game), ErrorResponse> {
    let url = format!("http://localhost:3030/tic_tac_toe");
    let client = reqwest::Client::new();
    let game = client.post(url).send().await?;
    unwrap_game(game).await
}

#[tauri::command]
pub async fn get_game(id: usize) -> Result<Game, ErrorResponse> {
    let url = format!("http://localhost:3030/tic_tac_toe/{}", id);
    let game = reqwest::get(url).await?;
    unwrap_game(game).await
}

#[tauri::command]
pub async fn play_game(id: usize, num: usize) -> Result<Game, ErrorResponse> {
    let url = format!("http://localhost:3030/tic_tac_toe/{id}/{num}");
    let client = reqwest::Client::new();
    let game = client.put(url).send().await?;
    unwrap_game(game).await
}

#[tauri::command]
pub async fn delete_game(id: usize) -> Result<(), ErrorResponse> {
    let url = format!("http://localhost:3030/tic_tac_toe/{id}");
    let client = reqwest::Client::new();
    client.delete(url).send().await?.text().await?;
    Ok(())
}

async fn unwrap_game<T>(game: reqwest::Response) -> Result<T, ErrorResponse>
    where T: serde::de::DeserializeOwned
{
    if game.status().is_success() {
        let game = game.json::<T>().await?;
        Ok(game)
    } else {
        let error = game.json::<ErrorResponse>().await?;
        Err(error)
    }
}
// app/src-tauri/src/main.rs
use tic_tac_toe::rest_api::{get_game, new_game, play_game, delete_game};
    // ...略
    .invoke_handler(tauri::generate_handler![
        // ...
        get_game, new_game, play_game, delete_game,

泛型方法的呼叫是mymethod::<T>(params),但上面我們在呼叫使用的時候,都寫一樣也沒特別寫明T的類別,這是因為rust會自動判斷回傳的類別,去推斷T要放什麼,而幫我們放上去。要注意編譯器推斷和動態產生是兩件不同的事情,rust其實是在編譯的時候,依情境自動幫我們補上缺漏的程式碼如下:

unwrap_game::<(isize, Game)>(game).await
unwrap_game::<Game>(game).await

前端 Svelte 改呼叫 tauri api

API好了,那我們來改寫前端,接接看剛剛寫好的tauri api,首先在api裡追加呼叫tauri的部分:

// app/src/api/tic_tac_toe.ts
import { invoke } from "@tauri-apps/api/tauri";

const getGameTauri = async (id: number): Promise<GameSet> => {
  try {
    const game = await invoke('get_game', { id });   // { id:id } 縮寫
    return [id, game as Game];                       // 組 GameSet
  } catch (e) {                                      // 補捉rust的Err(e)
    return Promise.reject(e);
  }
};

const newGameTauri = async (): Promise<GameSet> => {
  try {
    const gameSet = await invoke('new_game');  // 無參數
    return gameSet as GameSet;
  } catch (e) {
    return Promise.reject(e);
  }
};

const playGameTauri = async (id: number, num: number): Promise<GameSet> => {
  try {
    const game = await invoke('play_game', { id, num }); // 兩個參數
    return [id, game as Game];
  } catch (e) {
    return Promise.reject(e);
  }
};

const deleteGameTauri = async (id: number): Promise<void> => {
  try {
    await invoke('delete_game', { id });
  } catch (e) {
    return Promise.reject(e);
  }
};

export const ticTacToeApiTauri: TicTacToeApi = { // 實現與http同樣介面
  deleteGame: deleteGameTauri,
  getGame: getGameTauri,
  newGame: newGameTauri,
  play: playGameTauri,
};

與之前寫http api呼叫很像,只是這裡統一改成呼叫tauri的api,透過tauri提供的invoke呼叫我們在rust裡寫好的程式,呼叫的名稱是和rust裡的fn同名,如果有參數,則使用json物件的方式傳遞,如{ name : 'hello', age: 3 },要注意的是傳遞的參數必需要能被轉型成該rust的類別,不然會發生恐慌。

這邊我們套用相同的interface,如此就可以透過不同的執行環境進行抽換,比如瀏覽器端就使用http呼叫,tauri app就呼叫tauri api。在api統一接口的部分我們再定義一個tauri的api:

@@ app/src/api/index.ts @@
+import { ticTacToeApiTauri } from './tic_tac_toe';
-import { ticTacToeApi } from './tic_tac_toe';
...
 export const api: Api = {
+  ticTacToe: ticTacToeApiTauri,
-  ticTacToe: ticTacToeApi,
 };

定義好了之後到我們的遊戲頁面:

這裡import api改成我們剛剛寫好的tauriApi,然後在本檔案裡別成alias成原本的名稱api,所以下面的代碼都不需要動,就完成抽換了(有沒有感受到interface的威力了,請愛用interface),實測如下,可正常操作遊戲,新遊戲與刪除。
正確顯示tauri app的棋局畫面

什麼,有人不相信已經換成呼叫tauri的command了,那麼可以試著開啟瀏覽器開發工具,看看網路是不是都沒有http request/response了,空空如也,這下總該相信了吧。

不過當我們要執行「跳至第N筆」的時候就報錯了:
tauri的報錯畫面未正確顯示

出現一個undefined,怎麼會這樣呢,打開console看看:
devtools的console畫面

怎麼也跟著空空如也,什麼訊息都沒有是要怎麼偵錯啊 (╯°Д°)╯ ┻━┻ (╯°O°),換個招式(有人說程式設計師要十八般武藝樣樣精通,等練到了一定程度,還可以通靈喔 ^.< ),我們在goto裡加上console.log試試看:

@@ app/src/routes/tic_tac_toe/+page.svelte @@
const goto = async (id: number) => {
  error = null;
  try {
    gameSet = await api.ticTacToe.getGame(id);
  } catch (e) {
+  console.log(e);
    error = e as ErrorResponse;
}

補上console.log後的console畫面

這次有訊息了,而且很清楚,原來是在呼叫tauri裡的get_game時,id參數只能接收usize,但我們傳遞的是string "2"。(怎麼這麼笨不會自己轉,學學javascript好嗎)。就說rust是相對比較嚴格且安全的語言,所以我們只能配合修改。如果是初學者的話,說明一下這裡接收到網頁端的輸入,基本上格式都是字串,如果想要數字的話要進行轉型。

畫面中會出現undefined是因為我們把收到的錯誤直接指派給error = e as ErrorResponse;,但它不是我們在rust包好的ErrorResponse,所以沒有messagedetails欄位,才造成剛剛畫面上無法正確顯示。那我們是不是要再把這個錯誤訊息包起來給用戶端呢?不過在這邊我不太想包這個錯誤,為什麼呢?因為這個錯誤訊息本來就不是要給玩家看的呀,是給我們這些苦力程式設計師看的,這麼嚴重的問題怎麼可以上線(?),所以我們的工作就是確保把這類的問題修好。

i.e. panic 主要給開發人員看的,要給user看的Error會另外包一個Error型別處理。

有沒有體會到rust的好了,型別錯錯直接告訴,不會等程式在跑的時候時不時就噴undefined。

其實還有一種方法,就是把這個inputtype設定成"number",但整個會變的很醜有點不太好接受(明明現在畫面也沒多好看(?)。所以我們用另一種方式,把svelte的雙向綁定改成兩個單向綁定。(有點像在vue裡要自己實作v-model也是拆成@update:

加上type為number會出現上下的小三角
加上type為number的input box

@@ app/src/routes/tic_tac_toe/+page.svelte @@
+value={id} on:input={(e) => {id = Number(e.target.value)}}
-bind:value={id}
  • value={id}:單向綁定id這個變數,當id的值發生變化時,會自動更新我們input元件的value,這裡是往input元件內的綁定。
  • on:input:當觸發input元件的輸入事件時,會呼叫其指定的function,這裡的e是輸入的事件InputEvent,我們直接在這裡寫Inline程式,把id設定為修改事件的value,這裡是從input元件往外的綁定。

input box出現undefined

測試可以正常跳動了,只是一開始undefined怪怪的,我們先賦予它一個初始值:

@@ app/src/routes/tic_tac_toe/+page.svelte @@
+let id: number = 1;
-let id: number;

在tauri裡面我們試著跑CRUD,基本上都沒問題了,我們用瀏覽器開一下:

遊戲畫面出現window.TAURI_IPC is not a function

什麼是window.__TAURI_IPC__ is not a functionIPC是inter-process-commuication,這個__TAURI_IPC__主要是tauri拿來呼叫rust程式使用的一個接口,使用類似JSON-RPC的格式調用呼叫與回應。

既然是呼叫rust程式的,那麼在瀏覽器裡面當然呼叫不到,所以自然而然地報錯。我們可以透過不同的環境來選不同的api,假設今天跑在瀏覽器,就用我們一開始做的ky呼叫http api,如果跑在tauri,就呼叫tauri IPC,具體設法如下:

export const httpApi: Api = {    // 把之前 api rename 成 httpApi 區別 tauri
  ticTacToe: ticTacToeApi,
};

export const tauriApi: Api = {    // 這部分沒有動
  ticTacToe: ticTacToeApiTauri,
};

export const api: Api = window.__TAURI_IPC__ ? tauriApi : httpApi;

最後輸入的api,會依執行環境,判斷如果存在Tauri IPC就呼叫tauri,不然就呼叫ky的rest api。好了,瀏覽器跟tauri兩個版本目前都可以使用了。

有沒有在次感受到抽介面的好處!

此系列文章會儘可能同時兼用不同的技術達到相類似的效果,讓大家在現實的案例中可以有更多的選擇:比如我們用js 呼叫http api最簡單,如果要構建前後端分離可逕行採用;如果今天我們不想把js暴露給前端,那麼可以使用tauri包成桌面版應用。tauri發佈時可以設定要不要關閉devtool,所以對用戶來說就不能按F12偷看js代碼了。

那包在tauri裡我們也可以用http呼叫啊,為什麼透過tauri的rust還多繞一層?其實像一開始我們http api遇到的CORS policy,因為後端也是我們寫所以可以在後端的回應追加處理,如果遇到後端是別人寫那麼直接用rust http client就不用走CORS。或者是有些後端如果採用https可能會有憑證信任的問題,但在tauri裡我們的httpClient是用rust寫的,可以依我們的需求設定憑證的規則,相對比較彈性,甚至我們之後也可以使用gRPC等不同的後端通訊技術。

使用tauri另外還有個優勢,可以參考邦友寫的瀏覽器的安全模型,裡面有一句重點:「瀏覽器不給你的,你拿不到,拿不到就是拿不到」,所以今天我們使用tauri開發的桌面版應用,可以把tauri當作是前端的後端,既然是後端,那麼後端可以做的事理論上它都可以做(?)。而就畫面顯示的部分(前端的前端),仍然應用現存既有的前端框量,方便快速建構使用者介面(UI)。

不是說要把後端塞進前端嗎,這樣講好像也太混(?),好啦這裡只是先預告一下,希望後面可以補完 XDDD。

本系列專案源始碼放置於 https://github.com/kenstt/demo-app


上一篇
12 讓前端再好一點點 let Svelte co-work with rust
下一篇
14 幫 tauri 整理一下儀容
系列文
前端? 後端? 摻在一起做成全端就好了30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言