這篇文章是 2023 COSCUP Rust 議程軌之一,介紹 Rust 在後端開發的應用,以 Tide 為例。
Rust 已經連續多年成為 Stack Overflow 最受歡迎的程式語言,在 2023 年的調查也毫不意外的再度奪冠。
許多公司紛紛將 Rust 應用在產品上,例如:Amazon、Meta、Microsoft、Google 這些大公司,並且帶起一股似乎什麼都可以用 Rust 來重新改寫的風潮。
既然 Rust 這麼猛,那麼是不是也可以運用在網頁開發上呢?答案是肯定的,現在已經有許多的框架可以用來開發,想開發前端沒問題,後端框架更是如雨後春筍般的冒出來,例如:Rocket、Actix、Warp、Tide 等等。
從 JavaScript 的角度學習 Rust
身為網頁開發者,多多少少都會使用 JavaScript,所以從 JavaScript 的角度來學習 Rust 會比較容易理解。
宣告變數
let x = 5; // mutable
const y = 10; // constant
let x = 5; // immutable
let mut y = 10; // mutable
const Z: i32 = 15; // constant
JavaScript 使用 let
宣告可變變數,而 const
用於宣告常數。
Rust 默認使用 let
宣告不可變變數,需要加 mut
關鍵字來宣告可變變數。const
在 Rust 中也用於宣告常數,但需要明確指定型別,並且變數名稱為大寫。
型別
let a = "hello"; // string
let b = 5; // number
let c = 3.14; // number
let d = true; // boolean
let a: &str = "hello"; // 字串切片
let b: i32 = 5; // 數字 i32
let c: f64 = 3.14; // 浮點數 f64
let d: bool = true; // bool
JavaScript 是動態型別語言,所以它不需要在變數宣告時指定型別。 Rust 是靜態型別語言,通常會在變數宣告時指定型別,但 Rust 的型別推斷也很強大,可以在多數情況下自動判斷型別,所以以這個例子來說,Rust 的型別宣告可以省略。
函式
function calculate(x, y) {
return x + y;
}
fn calculate(x: i32, y: i32) -> i32 {
x + y
}
在 JavaScript 中,使用 function
關鍵字定義函式。
在 Rust 中,使用 fn
關鍵字定義函式,並在參數和返回值時指定型別。
補充一下說明,Rust 的函式可以省略 return
,最後一行的運算式會自動當作返回值,並且可以省略分號。
if...else
if (x > y) {
console.log("x 大於 y");
} else {
console.log("x 不大於 y");
}
if x > y {
println!("x 大於 y");
} else {
println!("x 不大於 y");
}
這兩種語言的 if...else 結構在語法上很相似,但 Rust 不需要括號來包裹條件語句。補充一下, JavaScript 的 console.log
在 Rust 中等同是 println!
。
迴圈
for (let i = 0; i < 10; i++) {
console.log(`i is ${i}`);
}
while (x < 10) {
console.log(`x is ${x}`);
x+=1;
}
for i in 0..10 {
println!("i is {}", i);
}
while x < 10 {
println!("x is {}", x);
x+=1;
}
兩種語言的迴圈語法也很相似,但在 Rust 中,for in
用於遍歷範圍或集合。補充一下,在 Rust 的 println!
中,{}
用於格式化輸出,{}
會被變數的值取代。所以這個 for
例子出來的結果會是 i is 0
到 i is 9
。
錯誤處理
function divide(x, y) {
if (y === 0) {
throw new Error("無法除以 0");
}
return x / y;
}
try {
const result = divide(10, 0);
console.log(result);
} catch (e) {
console.log(e.message);
}
fn divide(x: i32, y: i32) -> Result<i32, String> {
if y == 0 {
return Err("無法除以 0".to_string());
}
Ok(x / y)
}
match divide(10, 0) {
Ok(result) => println!("{}", result),
Err(e) => println!("{}", e),
}
JavaScript 使用 throw
和 try...catch
來處理異常。
Rust 則使用 Result
enum 來表示可能的錯誤,並使用 match
語句來處理這些結果。
modules
// calc.js
function calculate(x, y) {
return x + y;
}
export default calculate;
// index.js
import calculate from "./calc.js";
console.log(calculate(5, 10));
// calc.rs
pub fn calculate(x: i32, y: i32) -> i32 {
x + y
}
// main.rs
mod calc;
use calc::calculate;
fn main() {
println!("{}", calculate(5, 10));
}
在 JavaScript 中,您可以使用 export
和 import
來管理模組。
Rust 使用 mod
關鍵字來聲明模組,並使用 use
關鍵字來引入它。公開的函式或結構需要使用 pub
關鍵字。
如何使用 Tide
首先建立一個新專案:
$ cargo new tide_crud
並且安裝相關的套件:
[package]
name = "tide_crud"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tide = { version = "0.16.0" }
tokio = { version = "1.29.1", features = ["full"] } // 非同步處理
serde = { version = "1.0", features = ["derive"] } // 將資料轉換各種格式,例如 JSON、YAML、XML 等
安裝方法就是直接在 Cargo.toml
中 dependencies
底下加入 tide
、tokio
和 serde
這三個套件,然後只要一執行 cargo run
就會自動安裝。
Hello World
use tide::{Request, Result};
async fn hello(_req: Request<()>) -> Result<String> {
Ok("Hello, world!".to_string())
}
#[tokio::main]
async fn main() -> Result<()> {
let mut app = tide::new();
app.at("/").get(hello);
app.listen("0.0.0.0:8080").await?;
Ok(())
}
接下來只要執行 cargo run -q
,瀏覽器打開 http://localhost:8080
就可以看到 Hello, world!
。
查詢參數
use tide::{Request, Result, new};
async fn handle_name(req: Request<()>) -> Result<String> {
let name = req
.url()
.query_pairs()
.find(|(key, _)| key == "name")
.map(|(_, value)| value)
.unwrap_or("world".into());
Ok(format!("Hello, {}!", name))
}
#[tokio::main]
async fn main() -> Result<()> {
let mut app = new();
app.at("/").get(handle_name);
app.listen("0.0.0.0:8080").await?;
Ok(())
}
這個例子展示了如何從 URL 中獲取查詢參數。如果提供了 name 查詢參數,它將用於回應,否則預設為 "world"。
JSON 處理
use tide::{Request, Result, new};
#[derive(serde::Deserialize)]
struct Rust {
name: String,
version: f32,
}
async fn create(mut req: Request<()>) -> Result<String> {
let rust: Rust = req.body_json().await?;
Ok(format!("Hello {}! Your Rust version is {}.", rust.name, rust.version))
}
#[tokio::main]
async fn main() -> Result<()> {
let mut app = new();
app.at("/rust").post(create);
app.listen("0.0.0.0:8080").await?;
Ok(())
}
這裡展示了如何使用 Tide 處理 JSON 資料。當你向 /rust
路徑發送 POST 請求並提供正確的 JSON 資料時,伺服器將回傳一個格式化的訊息。
這個例子,假如在 Postman 輸入以下資料:
{
"name": "Bucky",
"version": 1.8
}
則會回傳:
Hello Bucky! Your Rust version is 1.8.
巢狀路由
use tide::{Request, Result, new};
#[tokio::main]
async fn main() -> Result<()> {
let mut app = new();
app.at("/rust").nest({
let mut api = new();
api.at("/").post(|_req: Request<()>| async move { Ok("Create") });
api.at("/:id")
.get(|_req: Request<()>| async move { Ok("Read") });
api.at("/version").get(|_req: Request<()>| async move { Ok("1.0.0") });
api
});
app.listen("0.0.0.0:8080").await?;
Ok(())
}
這個例子展示了如何使用巢狀路由來組織你的應用程式。nest 方法允許你在一個給定的路徑下建立多個路由。
status code
use tide::{new, Request, Response, Result, StatusCode};
async fn create(_req: Request<()>) -> Result {
let res = Response::builder(StatusCode::Created)
.body("Hello, world!")
.build();
Ok(res)
}
#[tokio::main]
async fn main() -> Result<()> {
let mut app = new();
app.at("/rust").post(create);
app.listen("0.0.0.0:8080").await?;
Ok(())
}
這個例子展示了如何使用 Tide 處理 status code。
如果要使用自定義的 status code,則在 Response::new()
時,參數傳入自定義的 status code 即可,例如:
let mut res = Response::new(200);
res.set_body("Hello, world!");
Ok(res)
file server
use tide::{Result, new};
#[tokio::main]
async fn main() -> Result<()> {
let mut app = new();
app.at("/file.txt").serve_file("src/hello.txt")?;
app.at("/test").serve_dir("./www")?;
app.listen("0.0.0.0:8080").await?;
Ok(())
}
這個例子展示了如何使用 Tide 來處理靜態檔案。
logger
先安裝 femme
套件:
[dependencies]
femme = "2.2.1"
use tide::{Result, new, log};
#[tokio::main]
async fn main() -> Result<()> {
femme::start();
let mut app = new();
app.with(log::LogMiddleware::new());
app.at("/").get(|_| async {
Ok("Hello, world!")
});
app.listen("0.0.0.0:8080").await?;
Ok(())
}
透過 femme 在啟動伺服器後,每當有任何請求,在終端機中,就可以看到有關這次請求的日誌資訊。
建立 CRUD API
建立 model
先新增一個 models.rs
檔案:
use std::collections::HashMap;
#[derive(serde::Deserialize, serde::Serialize)]
pub struct User {
pub id: u32,
pub name: String,
pub email: String,
}
pub struct Repository {
pub users: HashMap<u32, User>,
}
impl Repository {
pub fn new() -> Self {
Self {
users: HashMap::new(),
}
}
}
在這個 models.rs
檔案中,定義了一個名為 User
的結構體,代表一個用戶,以及一個名為 Repository
的結構體,代表用戶的儲存庫。Repository
使用 HashMap
來儲存 User
實例,其中用戶的 ID 作為 key。
建立 controller
再新增一個 controller.rs
檔案:
use crate::models::{Repository, User};
use tide::{Body, Request, Result, StatusCode};
use std::sync::Arc;
use tokio::sync::RwLock;
type State = Arc<RwLock<Repository>>;
pub async fn create(mut req: Request<State>) -> Result {
let user: User = req.body_json().await?;
let state = req.state();
let mut repo = state.write().await;
repo.users.insert(user.id, user);
Ok(StatusCode::Ok.into())
}
pub async fn read(req: Request<State>) -> Result {
let state = req.state();
let repo = &state.read().await;
Ok(Body::from_json(&repo.users)?.into())
}
在 controller.rs
檔案中,定義了幾個控制器函數,這些函數是用來處理 Web 請求的。這裡定義了 create
和 read
函數,分別用於新增和讀取。
- create:從請求的 JSON 主體中獲取用戶資料,然後添加到儲存庫中。
- read:從儲存庫中獲取所有用戶的資料,然後以 JSON 格式返回。
在 main.rs 中使用 create & read
mod models;
mod controller;
use std::sync::Arc;
use tide::Result;
use tokio::sync::RwLock;
use models::Repository;
use controller::{create, read};
#[tokio::main]
async fn main() -> Result<()> {
femme::start();
let state = Arc::new(RwLock::new(Repository::new()));
let mut app = tide::with_state(state);
app.at("/users").post(create);
app.at("/users").get(read);
app.listen("0.0.0.0:8080").await?;
Ok(())
}
在 main.rs
中,首先引入之前定義的模型和控制器,並建立了一個新的儲存庫並將其儲存在共享狀態中。然後定義了路由以處理使用者的新增與讀取操作。
update & delete
在 controller.rs
中新增以下程式碼:
pub async fn update(mut req: Request<State>) -> Result {
let id: u32 = req.param("id")?.parse()?;
let user: User = req.body_json().await?;
let state = req.state();
let mut repo = state.write().await;
if repo.users.contains_key(&id) {
repo.users.insert(id, user);
Ok(StatusCode::Ok.into())
} else {
Ok(StatusCode::NotFound.into())
}
}
pub async fn delete(req: Request<State>) -> Result {
let id: u32 = req.param("id")?.parse()?;
let state = req.state();
let mut repo = state.write().await;
if repo.users.contains_key(&id) {
repo.users.remove(&id);
Ok(StatusCode::Ok.into())
} else {
Ok(StatusCode::NotFound.into())
}
}
在 controller.rs
添加兩個新的控制器函數:
- update:更新具有指定 ID 的用戶的資料。如果用戶存在,則更新其資料;否則,返回 404 Not Found。
- delete:刪除具有指定 ID 的用戶。如果用戶存在,則從儲存庫中刪除它;否則,返回 404 Not Found。
在 main.rs 中使用 update & delete
use controller::{create, read, update, delete};
#[tokio::main]
async fn main() -> Result<()> {
femme::start();
let state = Arc::new(RwLock::new(Repository::new()));
let mut app = tide::with_state(state);
app.at("/users").post(create);
app.at("/users").get(read);
app.at("/users/:id").put(update);
app.at("/users/:id").delete(delete);
app.listen("0.0.0.0:8080").await?;
Ok(())
}
最後在 main.rs
加入新的路由以使用上面定義的 update
和 delete
控制器函數。這樣就可以建立完一個完整的 CRUD API。
心得
感謝這次 COSCUP 所有的工作人員,沒有你們的協助,讓我有這次寶貴的機會並且順利完成這次分享,也要特別感謝 nukr 大大幫我主持,沒想過會這麼多人參加,謝謝大家的支持,希望大家都有獲得一些東西回去。(感謝 Patty 一路陪我並幫我拍照)
讓我炫耀一下被 Rust 大神 Luca Palmieri 發文的推特:
最後有興趣的可以看一下當天的簡報分享