iT邦幫忙

2023 iThome 鐵人賽

DAY 3
0
Software Development

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

03 rust 跑起來!,建立第一支 tauri 程式

  • 分享至 

  • xImage
  •  

準備工作都完成了,接下來就可以開始建立第一個神奇的tauri程式了

Hello Tauri App

好容易把套件裝起來了,先依官網指示建立一個demo app 吧

因為我們的專案之後會有很多支程式放一起,所以前端和後端就 全部摻在一起做成撒尿牛丸就好了 要先建好基本的分層結構,以下建立我們的code base,在這邊取名為demo-app,後續這個名字代表的就是整個code base。

~$ mkdir demo-app    # 建立 demo-app資料夾
~$ cd demo-app/      # 進入 demo-app資料夾
~/demo-app$ cargo install create-tauri-app --locked    # 安裝tauri 範本工具cli
   Updating crates.io index
  Downloaded create-tauri-app v3.7.3
  Downloaded 1 crate (263.2 KB) in 7.08s
  Installing create-tauri-app v3.7.3
... 略 ...
   Compiling create-tauri-app v3.7.3
    Finished release [optimized] target(s) in 24.52s
  Installing /home/heyman/.cargo/bin/cargo-create-tauri-app
   Installed package `create-tauri-app v3.7.3` (executable `cargo-create-tauri-app`)

接著我們在資料夾中建立前端專案,透過create-tauri-app工具建立tauri 專案,執行後會出現提示訊息,依下列步驟逐項選擇即可,當然你很熟的話也可以挑戰選擇其他的選項。

~/demo-app$ cargo create-tauri-app app     # 建立 tauri app
  • 選TypeScript,前端要用rust也可以,但是感覺 很硬 就離題了,所以還是先選大家比較常見的前端語言 TypeScript
    選擇前端框架

  • pnpm 就是快,你還沒用嗎,趕快去了解吧。
    選擇package manager

  • 這裡選svelte
    選Js框架

  • TypeScript
    選擇ts或js

  • 跑完結果如下:
    cli message

現在的CLI工具都會很貼心的告訴你下一步要打的指令是什麼,不過繼續之前我們先看一下所產生的資料夾結構長什麼樣子:

~/demo-app$ tree app
app
├── index.html            # SPA 單網頁的那頁html
├── package.json          # node.js 都會看到的專案設定檔
├── public (略)           # 打包web用靜態檔案
├── README.md
├── src                   # node.js 專案的src目錄
│   ├── App.svelte        # Svetle 的首頁
│   ├── lib               # Svelte 的 component 資料夾
│   │   └── Greet.svelte  # Svelte 的 component 檔案
│   ├── main.ts           # web 程式進入點
│   ├── styles.css        # 樣式表
│   └── vite-env.d.ts
├── src-tauri             # Tauri 程式的目錄
│   ├── build.rs          # rust 建構腳本
│   ├── Cargo.toml        # Rust 專案設定檔
│   ├── icons (略)
│   ├── src
│   │   └── main.rs       # Rust 程式的進入點
│   └── tauri.conf.json   # tauri 啟動設定檔
├── svelte.config.js      # Svelte 設定檔
├── tsconfig.json         # TypeScript 編譯設定檔 
├── tsconfig.node.json
└── vite.config.ts        # Vite 設定檔

用node.js開發過前端的捧友們應該覺得很熟悉,除了多出來的src-tauri資料夾。沒錯,整個專案如果忽略tauri的資料夾,直接用nodejs也可以跑的很開心。

接著我們不免俗地先執行pnpm還原相依套件,最後再執行我們的程式

~/demo-app$ cd app                 # 進入剛剛產生的目錄
~/demo-app/app$ pnpm i             # 安裝 svelte 前端套件
~/demo-app/app$ cargo tauri dev    # 開發 tauri app

如果有人還是喜歡用 yarn 或 npm的話,自己調整指令即可

rust的編譯時間比較久,等待的同時,我們來看一下package.json檔放了什麼內容:

{   
    // 略
    "scripts": {
        "dev": "vite",
        "build": "vite build",
        "preview": "vite preview",
        "check": "svelte-check --tsconfig ./tsconfig.json",
        "tauri": "tauri"
  }
}

所以除了tauri那行是新的,其他看起來就是在開發前端,是的,我們再接著往下看。

等執行完跳出下面的畫面就表示成功了,下面的三個logo圖示分別為ViteTauriSvelte,三個願望一次滿足 (?):

tauri 起始畫面

  • Vite 是替代webpack的前端SPA打包工具的一個比較推薦的方案
  • Tauri 就是本系列要介紹基於rust建立的桌面應用程式
  • Svelte 是一款不使用Virtual DOM 做成的前端SPA應用

終於把UI弄出來了,有操作畫面還是比較直觀一點,我們立馬試一下互動,輸入名字後按 Greet看看:

tauri greeting

看起來好像沒什麼(?),不過等等,開發過前端框架的眼尖朋友們,是不是有注意到,在剛剛打 cargo 開發指令時是不是好像有出現一個熟悉的畫面,這是dev server跑起來的感覺啊:

nodejs cli message

那我們試著用Chrome開啟上面的連結http://localhost:1420/看看會怎樣

溫馨小提示:按住Ctrl鍵再用滑鼠點命令模式中的網址就可以直接跳轉了

竟然也可以開耶,那按一下Greet會怎樣?

tauri app in browser

竟然沒反應!趕快看一下F12(Crhome or Edge)是怎麼回事:

  • Safari 要先開啟開發人員模式,才能按Option + ⌘ + C。
  • Firefox 按 Ctrl + Shift + I

devtools in chrome

什麼是 __TAURI_IPC__ is not a function,我們來看一下程式碼app/src/lib/Greet.svelte

<!-- app/src/lib/Greet.svelte -->
<script lang="ts">
  import { invoke } from "@tauri-apps/api/tauri"

  let name = "";
  let greetMsg = ""

  async function greet(){
    greetMsg = await invoke("greet", { name }) // <= 這裡有個奇怪的function 
  }
</script>

<div>
  <form class="row" on:submit|preventDefault={greet}>
    <input id="greet-input" placeholder="Enter a name..." bind:value={name} />
    <button type="submit">Greet</button>
  </form>
  <p>{greetMsg}</p>
</div>

原來Greet的按鈕是在第8行呼叫 invoke 方法,該方法是從 tauri api 引入而來的,那我們再到看一下tauri裡的rust程式碼 app/src-tauri/src/main.rs

// app/src-tauri/src/main.rs
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

聰明的你應該注意到了,第2行有一個fn greet,沒錯,它就是rustfunction(後面可能會依情境稱為函數函式、方法或fn),原來剛剛在前端呼叫的invoke("greet", { name })是呼叫 rust 裡定義的fn,並傳遞JSON參數name給rust同名參數name使用。

如果你現在的心情像下圖一下,先暫停一下,再回看兩次,應該就懂了

梗圖-你搞的我好亂啊

如果有不知道 { name }{ name: name } 的同學去看一下object-shorthand,這個shorthand 不是showhand唷,是港片看太多逆(?)

溫馨提醒:這個物件shorthand的語法在rust中也可以使用唷 ^.<

原來是直接呼叫tauri裡的rust代碼,所以我們剛剛直接在瀏覽器裡點Greet,當然就呼叫不到tauri了,不過如果我們的JavaScript直接呼叫後端api的話,是不是表示在這裡寫的前端也可以獨立運作了呢。心動不如馬上行動,我們來實驗一下:

先在網路找一個現成的example api,依它提供的規格,我們在Greet.svelte裡加上以下片段:

<script>
    // 略
  let data = [];
  const callMyApi = async () => {
    const response = await fetch("https://dummy.restapiexample.com/api/v1/employees");
    const body = await response.json();
    data = body.data;
    console.log(data);    // 我剛剛先偷看資料的長相,才把下面each裡的程式碼調整對
  }
</script>
<div>
  <!-- 略 -->
  <button on:click={callMyApi}>Call my API</button>
  <ul>
    {#each data as item}
      <li>{item.employee_name}</li>
    {/each}
  </ul>
</div>

修改後按存檔,畫面會自動hot reload,這時候畫面會新增一個 Call my API的按鈕,我們按下去看看:

呼叫example api結果

果真跟我們剛剛預期的一樣,不過,實際佈署的話呢?我們還是試一下,古時候沒有完善的DevOps工具時,常常發生的可怕事件之一就是:我開發跑明明都沒問題啊,怎麼佈版上去就死了,懂的就懂(?)。

我們利用剛剛 package.json 裡設好的 build 指令建置:

~/demo-app/app$ pnpm build   # 前端框架的打包工具

pnpm build result

vite打包完成的檔案不出意外的放在 dist 資料夾下,我們把它丟到http server跑起來看看,但想到要分別安裝linux, mac, windows的http server就累了,考量有些windows的朋友可能不想裝IIS (先承認你朋友就是你) ,我們用更簡單的方式來起http server,感謝現在node豐富的生態系,我們直接找看看npm裡有沒有現成的http static server,很幸運地,第一個看起來就是我們要的:

npm search http server result

通常在搜尋完先不要急著點第一個,建議再多看幾個,比較一下活躍度、星星數、最後更新、以及簡單說明之類的,再去嘗試。

比較後看起來第一個似乎比較新,就用它試試看好了。進去看一下http-server的說明,照著安裝:

~/demo-app/app$ pnpm install --global http-server  # 安裝 http-server的npm至電腦全域環境
Packages: +40
++++++++++++++++++++++++++++++++++++++++
Packages are hard linked from the content-addressable store to the virtual store.
  Content-addressable store is at: /home/yesido/.local/share/pnpm/store/v3
  Virtual store is at:             .pnpm
Progress: resolved 40, reused 20, downloaded 20, added 40, done

/home/yesido/.local/share/pnpm/global/5:
+ http-server 14.1.1

Done in 2.9s

再照著http-server的說明跑起來:

~/demo-app/app$ http-server dist/  # 把我們剛剛pnpm build的結果資料夾 dist作為靜態網頁資料夾

Starting up http-server, serving dist/

http-server version: 14.1.1

http-server settings: 
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none

Available on:
  http://127.0.0.1:8080
  http://192.168.0.25:8080
Hit CTRL-C to stop the server

看起來沒問題,我們用Browser開看看:

Get my API 功能取得資料並顯示,與剛剛開發時測試的功能一樣。所以表示剛剛我們想刻一套前端,同時讓web 瀏覽器用也可以同時讓桌面應用程式使用的想法是可行的(理論上啦,實際看我們後面會踩到什麼雷(?)。

Rust workspace 工作空間

我們打算下一篇來實作rust code,在此之前還有一點時間,我們先做一下苦工(暖個身?),一般在專案中,會採分層方式來管理程式碼,避免變成大泥球(?),你終究要變大泥球的,那為什麼不一開始就變,如果還沒聽過分層的可以收藏一下這個視頻,有時間一定要去好好聽一下。

維護workspace的步驟我不會用指令(>.<)。所以檔案都是手動改的,如果有好心的大大知道怎麼做比較快,歡迎補充一下

如何設定 rust 的工作空間(workspace),首先依官方說明,我們在一開始的 demo-app 底下新增一個檔案Cargo.toml,內容如下:

[workspace]

members = [
    "app/src-tauri",
]

記得去加上.gitignore檔。

我們再開幾個專案分層試一下,順便看看rust怎麼組織不同的專案檔:

~/demo-app/app$ cd ..                # 回上一層 (如果你在子專案裡面目錄)
~/demo-app$ cargo new core --lib     # 新增core lib 專案
~/demo-app$ cargo new service --lib  # 新增service lib 專案
~/demo-app$ cargo new web            # 新增web bin 專案

以上指令我們新增一個名稱 core 的專案,拿來放核心的業務邏輯(或有人叫商業邏輯Business Logic),或領域核心邏輯層Domain Layer,再加一個service 應用層的專案,由於這兩個都是Library(程式庫),僅供呼叫使用,不是執行檔(bin),所以加--lib參數,以下我們看一下檔案結構的比較。

~/demo-app$ tree core/ service/ web/
core/
├── Cargo.toml
└── src
    └── lib.rs
service/
├── Cargo.toml
└── src
    └── lib.rs
web/
├── Cargo.toml
└── src
    └── main.rs

可以看到lib專案只有lib.rsbin專案有一個main.rs,剛才細心看tauri說明的同學,就知道剛剛有說main.rs是rust程式的進入點。可想而知,web專案可以單獨執行,coreservice只能提供讓專案使用呼叫。這裡可以把這幾個專案的單位稱為crate

剛剛執行的時候應該有一些提示訊息,我們回過頭來看一下它說什麼,它提到workspace沒有我們剛剛加入的專案。

this may be fixable by adding `core` to the `workspace.members` array of the manifest located at: /home/whoami/demo-app/Cargo.toml
Alternatively, to keep it out of the workspace, add the package to the `workspace.exclude` array, or add an empty `[workspace]` table to the package's manifest.

所以我們還要手動去編輯剛剛維護workspace的 Cargo.toml,手動加入以下專案成員如下:

[workspace]

members = [
    "app/src-tauri",
    "core",
    "service",
    "web",
]

註:這裡的member是相對的資料夾路徑,Cargo會再去該資料夾裡找該子專案的Cargo.toml檔,依該檔案的個別設定執行該專案。

加完後我們可以試run看看:

~/demo-app$ cargo run -p core
error: a bin target must be available for `cargo run`
~/demo-app$ cargo run -p service
error: a bin target must be available for `cargo run`
~/demo-app$ cargo run -p web
   Compiling web v0.1.0 (/home/whoami/demo-app/web)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/web`
Hello, world!

上面的 -p 參數是指定我們要執行哪一個 package,不是指目錄,只是這裡我用的名稱剛好一樣,這個專案名稱的參數寫在Cargo.toml檔案中。

接下來測試一下我們想要的依賴關係 webservicecore,是否可以正確的依循,修改以下檔案(僅列出部分內容):

// core/src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
    println!("add fn called in core");
    left + right              // 相當於 return left + right;
}

Rust的function最後一行的expression就是return值,最後一行不需寫return也不能加上;結尾,如果要提早回傳是可以使用關鍵字return的(跟ruby好像唷)。

# service/Cargo.toml
[dependencies]
core = { path = "../core" }
// service/src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
    println!("add fn called in service");
    core::add(left, right)        // 記得不能加分號結尾
}
# web/Cargo.toml
[dependencies]
core = { path = "../core" }
service = { path = "../service" }
// web/src/main.rs
fn main() {
    println!("Hello, world!");
    println!("call core fn from server: {}",core::add(2, 2));
    println!("call service fn from server: {}",service::add(2, 2));
}

以上修改完我們執行一下web程式看有沒有正確:

~/demo-app$ cargo run -p web
   Compiling core v0.1.0 (/home/whoami/demo-app/core)
   Compiling service v0.1.0 (/home/whoami/demo-app/service)
   Compiling web v0.1.0 (/home/whoami/demo-app/web)
    Finished dev [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/web`
Hello, world!
add fn called in core
call core fn from server: 4
add fn called in service
add fn called in core
call service fn from server: 4

It works!!,沒看到錯誤真是一件值得開心的事 XDD。好了,怕大家累了,先在此稍作歇息,下一回合我們再開始用rust來寫核心邏輯吧。

疑難排解

Linux下跑Cargo一直報錯

我在linux環境(debian+kde)一直跳錯,處理方式原則上就是看log訊息,訊息顯示缺什麼,就去安裝它,以下是手動安裝的一些項目供參考

sudo apt-get install libglib2.0-dev
sudo apt-get install libgtk-4-dev
sudo apt-get install librust-gdk-dev
sudo apt-get install libsoup2.4
sudo apt-get install libgssapi
sudo apt-get install krb5*
sudo apt-get install libgssapi-krb5-2
sudo apt-get install libgssapi-perl
sudo apt-get install gir1.2-javascriptcoregtk-4.0
sudo apt-get install libjavascriptcoregtk-4.0
sudo apt-get install libwebkit2gtk-4.0-dev
sudo apt-get install libglobus-gssapi-gsi-dev
sudo apt-get install libprotobuf-dev
sudo apt-get install protobuf-compiler

pnpm install --global 報錯

pnpm error message

看起來是因為pnpm未設定全域環境的原因,照著提示的指令

pnpm setup

pnpm setup result

再依提示執行就可以了

source /home/you/.bashrc # 把you代成你的帳號

參考資料

備註:本系列所使用的代碼部分由 副駕駛 共同完成。

程式原始碼同步放置於 https://github.com/kenstt/demo-app


上一篇
02 開發環境準備 rust & node.js
下一篇
04 今晚,我想來點... rust 入門語法
系列文
前端? 後端? 摻在一起做成全端就好了30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言