準備工作都完成了,接下來就可以開始建立第一個神奇的tauri程式了
好容易把套件裝起來了,先依官網指示建立一個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 就是快,你還沒用嗎,趕快去了解吧。
這裡選svelte
TypeScript
跑完結果如下:
現在的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圖示分別為Vite、Tauri及Svelte,三個願望一次滿足 (?):
終於把UI弄出來了,有操作畫面還是比較直觀一點,我們立馬試一下互動,輸入名字後按 Greet
看看:
看起來好像沒什麼(?),不過等等,開發過前端框架的眼尖朋友們,是不是有注意到,在剛剛打 cargo 開發指令時是不是好像有出現一個熟悉的畫面,這是dev server跑起來的感覺啊:
那我們試著用Chrome開啟上面的連結http://localhost:1420/看看會怎樣
溫馨小提示:按住Ctrl鍵再用滑鼠點命令模式中的網址就可以直接跳轉了
竟然也可以開耶,那按一下Greet會怎樣?
竟然沒反應!趕快看一下F12(Crhome or Edge)是怎麼回事:
- Safari 要先開啟開發人員模式,才能按Option + ⌘ + C。
- Firefox 按 Ctrl + Shift + I
什麼是 __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
,沒錯,它就是rust
的function
(後面可能會依情境稱為函數
、函式
、方法或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
的按鈕,我們按下去看看:
果真跟我們剛剛預期的一樣,不過,實際佈署的話呢?我們還是試一下,古時候沒有完善的DevOps
工具時,常常發生的可怕事件之一就是:我開發跑明明都沒問題啊,怎麼佈版上去就死了,懂的就懂(?)。
我們利用剛剛 package.json
裡設好的 build
指令建置:
~/demo-app/app$ pnpm build # 前端框架的打包工具
vite打包完成的檔案不出意外的放在 dist
資料夾下,我們把它丟到http server跑起來看看,但想到要分別安裝linux, mac, windows的http server就累了,考量有些windows的朋友可能不想裝IIS (先承認你朋友就是你) ,我們用更簡單的方式來起http server,感謝現在node豐富的生態系,我們直接找看看npm裡有沒有現成的http static server,很幸運地,第一個看起來就是我們要的:
通常在搜尋完先不要急著點第一個,建議再多看幾個,比較一下活躍度、星星數、最後更新、以及簡單說明之類的,再去嘗試。
比較後看起來第一個似乎比較新,就用它試試看好了。進去看一下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 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.rs
,bin
專案有一個main.rs
,剛才細心看tauri說明的同學,就知道剛剛有說main.rs
是rust程式的進入點。可想而知,web
專案可以單獨執行,core
及service
只能提供讓專案使用呼叫。這裡可以把這幾個專案的單位稱為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
檔案中。
接下來測試一下我們想要的依賴關係 web
→ service
→ core
,是否可以正確的依循,修改以下檔案(僅列出部分內容):
// 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環境(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未設定全域環境的原因,照著提示的指令
pnpm setup
再依提示執行就可以了
source /home/you/.bashrc # 把you代成你的帳號
備註:本系列所使用的代碼部分由 副駕駛 共同完成。
程式原始碼同步放置於 https://github.com/kenstt/demo-app