Rocket使用小結

renyuneyun 2018年08月03日(週五) 2 mins

在今年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提供一些實現以應對常見的應答情況:

  • 應答包裝器 可以包含其他 Responder ,並且執行自己的修改
    • status 可以覆蓋應答中的狀態代碼
    • content 可以覆蓋應答中的 Content-Type 字段
    • Error (我沒嘗試過,所以直接鏈接至官方文檔)
  • 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條結果)。


Related posts:

您可以在Hypothesis上的該群組內進行評論,或使用下面的Disqus評論。