Rocket.rs 初探 - 動態路由
HTTP Method
Rocket.rs 支援了大部分的 HTTP Request Method 如:get
,put
,post
等:
#[get("/")]
#[put("/")]
#[post("/")]
#[delete("/")]
#[head("/")]
#[patch("/")]
#[options("/")]
最基礎的動態路由
動態路由以<param>
為基礎,直接以下方的程式碼來舉例:
#[get("/users/<id>")]
fn get_user(id: usize) -> String {
return format!("Hello user{}", id);
}
fn main() {
rocket::ignite().mount("/", routes![get_user]).launch();
}
這樣當我們訪問/users/1
時就會得到Hello user1
的回覆內容。
Forwarding 路由
當一個請求進入之後,Rocket.rs 會開始匹配路由,當發現找到第一個路由不符合規則,則會將請求轉發到下一個路由中,以下面的程式碼來舉例:
#[get("/users/<id>")]
fn get_user_u8(id: u8) -> String {
return format!("Hello user{}, message from get_user_u8", id);
}
#[get("/users/<id>", rank = 2)]
fn get_user_u16(id: u16) -> String {
return format!("Hello user{}, message from get_user_u16", id);
}
fn main() {
rocket::ignite().mount("/", routes![
get_user_u8,
get_user_u16
]).launch();
}
這樣當我們訪問/users/1
時就會得到Hello user1, message from get_user_u8
, 而當我們訪問/users/257
時就會得到Hello user257, message from get_user_u16
這是因為257這個值並沒有辦法轉成型態u8
, 因此就會將請求 forward 到get_user_u16()
中。
對於這種路由,我們通常需要定義一個 fallback 用的路由,用於接受那些沒有被前面所有的路由所匹配的請求:
#[get("/users/<id>")]
fn get_user_u8(id: u8) -> String {
return format!("Hello user{}, message from get_user_u8", id);
}
#[get("/users/<id>", rank = 2)]
fn get_user_u16(id: u16) -> String {
return format!("Hello user{}, message from get_user_u16", id);
}
// 收納不匹配所有路由的請求
#[get("/users/<id>", rank = 3)]
fn get_user_string(id: String) -> String {
return format!("Hello user{}, message from get_user_string", id);
}
fn main() {
rocket::ignite().mount("/", routes![
get_user_u8,
get_user_u16,
get_user_string
]).launch();
}
另外,注意到每一個路由定義都有rank=n
的標注,這個 rank 代表的就是路由的優先級別,愈小的 rank 就會愈優先匹配。
預設 ranking
請求 | ranking | 範例 |
---|---|---|
純靜態路由 | -6 | /hello?world=ture |
靜態路由 + 動態 Query String | -5 | /hello?<world> |
靜態路由 | -4 | /hello |
動態路由 + 靜態 Query String | -3 | /<hello>?world=true |
動態路由 + 動態 Query String | -2 | /<hello>?<world> |
純動態路由 | -1 | /<hello> |
Query String
必要 Query String
Query String 也是透過<param>
的方式來撰寫:
#[get("/users?<id>")]
fn get_user(id: usize) -> String {
return format!("Hello, user{}!", id)
}
fn main() {
rocket::ignite().mount("/", routes![get_user]).launch();
}
這樣當我們訪問/users?id=1
時就會收到Hello, user1!
。
可選 Query String
在某些狀況下,比如說商品列表,搜尋的 Query String 通常為可選, 換句話說就是當沒有search
時則顯示全部,有的話則顯示符合搜尋條件的結果,我們可以寫這樣:
#[get("/products?<search>")]
fn get_products(search: Option<String>) -> String {
return match search {
None => format!("No search query string"),
Some(search) => format!("You search project: {}", search)
}
}
fn main() {
rocket::ignite().mount("/", routes![get_products]).launch();
}
將參數的型態定義為Option<string>
就可以將search
這個 Query String 的參數定義為可選, 上面的程式碼執行後,訪問/products
則會返回No search query string
, 而訪問/products?search=123
則會得到You search project: 123
。
將 Query String 包裝成 struct
我們也可以將 Query String 包成 struct 的型態方便處理,在這裡就直接拿官方文件的東西來舉例:
use rocket::request::Form;
#[derive(FromForm)]
struct User {
name: String,
account: usize,
}
#[get("/item?<id>&<user..>")]
fn item(id: usize, user: Form<User>) -> String {
return format!("Id: {}, name: {}, account: {}", id, user.name, user.account);
}
fn main() {
rocket::ignite().mount("/", routes![get_products, item]).launch();
}
這樣當我們訪問/item?id=1&name=floatflower&account=123
時,就可以得到Id: 1, name: floatflower, account: 123
。
Forwarding
當然在這裡使用場景中,forwarding 依然適用,我們把上面的程式碼改寫一下變成這樣:
use rocket::request::Form;
#[derive(FromForm)]
struct User {
name: String,
account: usize,
}
// 增加 User1 的 struct
#[derive(FromForm)]
struct User1 {
name: String,
account: String,
}
#[get("/item?<id>&<user..>")]
fn item(id: usize, user: Form<User>) -> String {
return format!("Id: {}, name: {}, account: {}, item", id, user.name, user.account);
}
// 當 account 的值不能與 usize 匹配時,就會 forward 到這個路由。
#[get("/item?<id>&<user..>", rank = 2)]
fn item1(id: usize, user: Form<User1>) -> String {
return format!("Id: {}, name: {}, account: {}, item1", id, user.name, user.account);
}
fn main() {
rocket::ignite().mount("/", routes![get_products, item, item1]).launch();
}
到這裡就是動態路由的匹配筆記,最後要注意的是,在路由衝突的時候,需要定義 ranking, 在對應相同的路由時,通常一個 ranking 愈小的,包含的 URL 的集合會愈小,我們透過改寫上方的範例來舉例:
#[get("/users/<id>", rank = 2)]
fn get_user_u8(id: u8) -> String {
return format!("Hello user{}, message from get_user_u8", id);
}
#[get("/users/<id>", rank = 1)]
fn get_user_u16(id: u16) -> String {
return format!("Hello user{}, message from get_user_u16", id);
}
#[get("/users/<id>", rank = 3)]
fn get_user_string(id: String) -> String {
return format!("Hello user{}, message from get_user_string", id);
}
fn main() {
rocket::ignite().mount("/", routes![
get_user_u8,
get_user_u16,
get_user_string
]).launch();
}
如果今天是將u16
的匹配放在u8
,那麼因為u16
已經包含所有u8
的值了, 因此當get_user_u16()
不匹配時,get_user_u8()
也必然不匹配,這樣也就沒有後者存在的必要了, 因此務必注意 ranking 所包含的 URL 集合, 假設今天訪問的路由是/users/65537
那麼必然會跳過get_user_u16()
但是也必然會跳過get_user_u8()
, 這也就是為什麼上面的 Default Ranking 是這樣排列的了。