在今年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(); }
(代碼片段來自 getting-started )
這點粗看很像Flask中使用 @app.route()
進行路由設定,僅有這兩點不同:
- Rocket不使用全局的app對象
- 路由可以定義但不掛載
我最初也以爲Rocket和Flask的設計極爲相似,且兩者都只打算做web框架而不涉及其他;然而,越到後面越是發現兩者不同之處的巨大。相對而言,我個人更喜歡Rocket的設計:更加函數導向(亦強調使用局部變量)。
- 在Rocket中,函數是處理路由的全部,不需要使用如flask中魔法一般的全局
request
對象; - 函數的參數和屬性中的設定共同決定了路由是否匹配,手動類型的優勢在這裏有所體現;
- 各種(預定義或自定義的) 請求哨衛 可以被添加到函數參數表中參與決定路由的匹配性;
- 使用 整流器 在請求到達前或應答發送時對請求或應答進行調整;
- 使用 狀態 做狀態存儲,以便的確需要“全局”變量的情況。
路由匹配
Rocket通過在屬性上設定不同的HTTP方法以及URL段來做匹配。 具體細節見 文檔#methods ,這裏僅做摘要:
- 常見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) } }
(代碼片段來自 文檔#methods )
自定義類型也可用在路由匹配中,只要其實現了 FromParam
trait即可。
請求及應答
在不考慮整流器的情況下,用戶的請求將直接進入相應的路由函數中,然後經過函數的處理,最後函數的返回值將作爲應答。路由匹配的過程即是請求處理器的選擇過程。
Rocket不要求路由函數的返回值是一個HTTP應答 Response
,而是通過 Responder
機制方便編程:路由函數的返回值需要是一個實現了 Responder
trait的類型,而Rocket負責調用 Responder
的相關函數將路由函數的返回值轉換爲HTTP應答。這樣,在Rocket中我們便可以用 String
等類型作爲函數返回值。
Rocket提供一些實現以應對常見的應答情況:
String
和&str
會被作爲應答體,且 Content-Type 會被設置爲text/plain
Option
是應答包裝器, :rust`Option<T>` 的T
需要實現Responder
:- 當是
Some
時,其內容將會被作爲應答 - 當是
None
時,返回404
- 當是
Result
是應答包裝器,且其功能取決於E
是否實現Responder
:- 若
E
實現了Responder
,則該Result
會被作爲應答(無論是Ok
還是Err
) - 若
E
沒有實現Responder
,則Ok
會被作爲應答,但Err
會被記錄在終端中且返回500
- 若
( 文檔#rocker-responder 中還列出了幾個常見的對於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對整流器的功能有一些額外規定:
- 整流器 不能 終止或直接響應請求
- 整流器 不能 任意注入非請求數據至請求中
- 整流器可以阻止程序的啓動
- 整流器可以修改程序的配置
文檔中fairings部分 對整流器有更多說明,但對我來說最需要知道的還有這些:
- 整流器應當只用於“全局”適用的東西(比如做日誌)
- 更多時候,需要的其實是 請求哨衛 和 數據哨衛
- 整流器按順序執行,所以`.attach()`的順序需要注意
全局共享數據
這裏的“全局”指的是Rocket之內,在各個路由中共享數據。由於路由是由Rocket管理的,故而其參數表中沒辦法添加更多參數;而Rust又沒有全局變量(即使有也不符合美感),故而Rocket提供的 狀態 機制可謂實用非常。 官方教程state部分 <https://rocket.rs/guide/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); ... }
(代碼片段來自 文檔的state部分 )
總結
總得來說,我個人對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條結果)。
您可以在Hypothesis上的該群組內進行評論,或使用下面的Disqus評論。