Rocket使用小結
2019-04-08: 本文亦在我的新博客中
(本文亦在我的wikiblog中)
在今年Increase Rust's Reach中,我參與Rust新網站的i18n及l10n。其中新網站要基於Rocket構建,所以也就(跟着官方教程)學習了一下Rocket。 既然學了,就順便記錄一點心得和體會,以方便後來者。
Rocket是一個 web框架 。我個人對web編程(尤前端)並不太感興趣(主要是感到 web技術棧 太過麻煩/複雜),所以涉及不太多,之前也只用過Python那邊的Flask以及(一小段時間)Django以及Go自帶的http服務器,故而本文不怎麼會涉及和其他web框架的對比。
本文不打算成爲通常意義上的Rocket教程,而只是打算給有興趣者一個快速的(對rocket的)觀感。其中也會有一些個人的經驗教訓等。
Rocket概覽
類似我之前用過的框架,Rocket也將函數作爲不同的路由的處理器。Rocket在每個函數之前使用形如#[get("/myroute")]
的 屬性 作爲標記,之後在Rocket入口對象/結構體上對所需要的路由(函數)進行mount
即可。
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
fn main() {
rocket::ignite()
.mount("/", routes![index])
.launch();
}
(代碼片段來自這裏。)
這點粗看很像Flask中使用@app.route()
進行路由設定,僅有這兩點不同:
- Rocket不使用全局的app對象
- 路由可以定義但不掛載
我最初也以爲Rocket和Flask的設計極爲相似,且兩者都只打算做web框架而不涉及其他;然而,越到後面越是發現兩者不同之處的巨大。相對而言,我個人更喜歡Rocket的設計:更加函數導向(亦強調使用局部變量)。
-
在Rocket中,函數是處理路由的全部,不需要使用如flask中魔法一般的全局
request
對象; - 函數的參數和屬性中的設定共同決定了路由是否匹配,手動類型的優勢在這裏有所體現;
- 各種(預定義或自定義的) 請求哨衛 可以被添加到函數參數表中參與決定路由的匹配性;
- 使用 整流器 在請求到達前或應答發送時對請求或應答進行調整;
-
使用
State
做狀態存儲,以便的確需要“全局”變量的情況。
路由匹配
Rocket通過在屬性上設定不同的HTTP方法以及URL段來做匹配。 具體細節見官方文檔,這裏僅做摘要:
- 常見HTTP方法均被支持,只是每次只能設定一個方法
- 在未定義時,HEAD請求會被自動轉到相應的GET請求上(不過會刪除應答體)
-
表單首個字段爲
_method
時,POST請求會被自動重譯爲相應的請求- 該設定是爲了方便瀏覽器,畢竟瀏覽器通常只有GET和POST
在路由的URL上,可以設定將部分(或全部) 節 注入到函數的對應參數中。Rocket會自動進行類型轉換,且僅匹配轉換成功的路由。
#[get("/hello/<name>/<age>/<cool>")]
fn hello(name: String, age: u8, cool: bool) -> String {
if cool {
format!("You're a cool {} year old, {}!", age, name)
} else {
format!("{}, we need to talk about your coolness.", name)
}
}
(代碼片段來自這裏。)
自定義類型也可用在路由匹配中,只要其實現了FromParam
trait即可。
請求及應答
在不考慮整流器的情況下,用戶的請求將直接進入相應的路由函數中,然後經過函數的處理,最後函數的返回值將作爲應答。路由匹配的過程即是請求處理器的選擇過程。
Rocket不要求路由函數的返回值是一個HTTP應答(Response
),而是通過Responder
機制方便編程:路由函數的返回值需要是一個實現了Responder
trait的類型,而Rocket負責調用Responder
的相關函數將路由函數的返回值轉換爲HTTP應答。這樣,在Rocket中我們便可以用String
等類型作爲函數返回值。
Rocket提供一些實現以應對常見的應答情況:
-
應答包裝器 可以包含其他
Responder
,並且執行自己的修改 -
String
和&str
會被作爲應答體,且Content-Type會被設置爲text/plain
-
Option
是應答包裝器,Option<T>
的T
需要實現Responder
:-
當是
Some
時,其內容將會被作爲應答 -
當是
None
時,返回404
-
當是
-
Result
是應答包裝器,且其功能取決於E
是否實現Responder
:-
若
E
實現了Responder
,則該Result
會被作爲應答(無論是Ok
還是Err
) -
若
E
沒有實現Responder
,則Ok
會被作爲應答,但Err
會被記錄在終端中且返回500
-
若
(官方文檔中還列出了幾個常見的對於HTTP很有意義的Responder
實現,包括下面所說的Template
。)
網頁模板
作爲一個web框架,提供對網頁模板的支持幾乎是理所應當。Rocket本身提供了Template
機制,而在rocket_contrib
crate中提供了一些特定模板支持。
Template
被實現爲一個Responder
,這樣讓響應函數返回Template
類型即可:
#[get("/")]
fn index() -> Template {
let context = /* object-like value */;
Template::render("index", &context)
}
Rocket不限制使用何種模板,但官方文檔提到了.hbs
Handlebars和.tera
Tera。而Rocket的Template
機制之所以有效,還需要整流器的幫助——所以需要在Rocket實例上.attach(Template::fairing());
以便可以正確使用模板。
整流器
依Rocket文檔所說,整流器的功能類似於中間件,可以介入請求和應答過程以進行額外操作。由於我沒有學過相關課程,也沒有太多瞭解相關知識,所以無法給出個人對此的看法,只能照搬官方文檔的說法。
在類似其他框架的中間件之外,Rocket對整流器的功能有一些額外規定:
- 整流器不能終止或直接響應請求
- 整流器不能任意注入非請求數據至請求中
- 整流器可以阻止程序的啓動
- 整流器可以修改程序的配置
官方文檔對整流器有更多說明,但對我來說最需要知道的還有這些:
- 整流器應當只用於“全局”適用的東西(比如做日誌)
- 更多時候,需要的其實是 請求哨衛 和 數據哨衛
-
整流器按順序執行,所以
.attach()
的順序需要注意
全局共享數據
這裏的“全局”指的是Rocket之內,在各個路由中共享數據。由於路由是由Rocket管理的,故而其參數表中沒辦法添加更多參數;而Rust又沒有全局變量(即使有也不符合美感),故而Rocket提供的 狀態 機制可謂實用非常。Rocket官方教程中也教導使用狀態來管理數據庫連接。
使用上,狀態同樣通過 請求哨衛 機制,作爲路由函數的參數。任何類型的數據均可作爲State
,且不需要額外實現任何東西。唯一的要求就是在Rocket實例載入時要求管理該狀態。
像這樣要求Rocket去管理某個狀態:
struct HitCount {
count: AtomicUsize
}
rocket::ignite()
.manage(HitCount { count: AtomicUsize::new(0) });
像這樣要求在某路由函數上使用某狀態:
#[get("/count")]
fn count(hit_count: State<HitCount>) -> String {
let current_count = hit_count.count.load(Ordering::Relaxed);
format!("Number of visits: {}", current_count)
}
#[get("/state")]
fn state(hit_count: State<HitCount>, config: State<Config>) -> T { ... }
需要注意的是,Rocket對每種數據類型管理一個狀態,而不是每個數據。
另外,在自定義的 請求哨衛 中,也可以使用狀態:
fn from_request(req: &'a Request<'r>) -> request::Outcome<T, ()> {
let hit_count_state = req.guard::<State<HitCount>>()?;
let current_count = hit_count_state.count.load(Ordering::Relaxed);
...
}
(代碼片段來自這裏。)
總結
總得來說,我個人對Rocket的設計較爲欣賞/膜拜,尤其是其對Rust各項機制的有效利用。
之前用其他框架時總有覺得彆扭的地方,但它們均不存在於Rocket中,讓我寫起來覺得比較順手:
-
Django(2013年底或2014年初)
- 框架內耦合性太強,但框架的手又過長
-
什麼都想讓框架承包,初學者用起來束手束腳
- 當然,也可以說是我還沒有體會到Django的好處。但我實在是對封閉花園式的東西感到反感,所以不見得可以體會到Django的妙處
- 而且當年對Py3的支持還不怎麼樣,但我又恰恰想用Py3
-
Go(2015年)
- Go的自帶http庫直接將請求和應答對象作爲參數傳入,手動解析很難受
- Go的html模板是語言提供的,靈活度上讓我懷疑
- 當時(2015年)查過其他的go語言web框架,比較看好的有beego以及另一個想不起來名稱的。但其教程寫得並不如人意(不如我意),又由於當年需求十分簡單,所以直接裸上語言庫
-
Flask(2016年)
- Flask要使用全局的app對象和db對象,設計上很詭異
-
Flask要使用魔法一般的request對象,總讓人覺得不安心
- 且request對象暴露太多內容,類似Go用http庫的感覺(和使用Android的Context對象的感覺很像)
然而,我對Rocket的部分trait和/或類型設計仍有疑惑,還在尋找解決方案的過程中。 另外,我暫時還沒有在Rust中使用過數據庫連接,所以無法對其聯合使用後的手感做出評價。 但綜合來看,Rocket的設計可以說是我所用過的所有框架中最符合我心意的框架;而且它的設計理念可以說符合了我對web框架所構想的所有主要要求。如果不是因爲Rust仍算小衆,Rocket的用戶量和教程量應當早就超過現有的數量了吧(2018-08-02 Google搜“Rust Rocket 教程”,得到11,200條結果)。