欲練此功

為什麼要寫這個框架

當初,作為初學者,我發現自己很笨拙,無法學會使用 actix-web、Rocket 等現有框架。當我想把以前用 Go 寫的 Web 服務改用 Rust 實現時,乍看之下,似乎每個框架都比 Go 的現有框架複雜。Rust 的學習曲線已經夠陡峭了,何必還要把 Web 框架搞得那麼複雜?

當 Tokio 推出 Axum 框架時,我開心地以為以後不必再維護自己的 Web 框架了。然而事實是,Axum 看似簡單,實際使用時卻需要大量類型體操和泛型定義,必須對 Rust 語言有深刻理解並寫出大量晦澀難懂的模板代碼,才能完成一個簡單的中間件。

因此我決定繼續維護這個特別(順手、功能豐富且適合初學者)的 Web 框架。

Salvo(賽風) 是否適合你

Salvo 雖然簡單,但功能足夠全面強大,基本可以視為 Rust 界最強的框架。然而,如此強大的系統,學習和使用卻非常簡單,絕對不會讓你感到任何「揮刀自宮」的痛苦。

  • 它適合剛開始學習 Rust 的初學者。CRUD 是極其平常且常用的功能,如果用 Salvo 做類似工作,你會發現它和你用過的其他語言 Web 框架一樣簡單(例如:Express、Koa、gin、flask...),甚至在某些方面更抽象簡潔;

  • 它適合希望將 Rust 用於生產環境,提供穩健快速伺服器的人。雖然 Salvo 尚未發布 1.0 版本,但其核心功能已經過幾年迭代,足夠穩定且問題修復及時;

  • 它適合毛髮已不再茂密但仍每天不停掉髮的你。

如何做到足夠簡單

許多底層實現 Hyper 都已提供,因此一般需求基於 Hyper 實現應該沒錯。Salvo 也是如此。核心功能是一個強大且靈活的路由系統,以及許多常用功能,如 Acme、OpenAPI、JWT Auth 等。

Salvo 統一了 Handler 和 Middleware。Middleware 就是 Handler,通過路由的 hoop 添加到 Router 上。本質上,Middleware 和 Handler 都是處理 Request 請求,並可能向 Response 寫入數據。而 Handler 接收的參數是 Request、Depot、Response 三個,其中 Depot 用於儲存請求處理過程中的臨時數據。

為方便書寫,用不到時可省略某些參數,也可無視參數傳入順序。

use salvo::prelude::*;

#[handler]
async fn hello_world(_req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    res.render("Hello world");
}
#[handler]
async fn hello_world(res: &mut Response) {
    res.render("Hello world");
}

此外,路由系統提供的 API 極其簡單,但功能強大。正常使用需求下,基本上只需關注 Router 一個類型即可。 另外,如果結構體實現了相關 trait,Salvo 可以自動生成 OpenAPI 文檔及提取參數,自動處理不同錯誤並返回友好提示。這使得編寫 handler 就像編寫普通函數一樣簡單直觀。後續教程會詳細講解這些功能,以下是範例:

#[endpoint(tags("消息日誌"))]
pub async fn create_message_log_handler(
    input: JsonBody<CreateOrUpdateMessageLog>,
    depot: &mut Depot,
) -> APPResult<Json<MessageLog>> {
    let db = utils::get_db(depot)?;
    let log = create_message_log(&input, db).await?;
    Ok(Json(log))
}

此範例中,JsonBody<CreateOrUpdateMessageLog> 會自動從請求體解析 JSON 數據並轉換為 CreateOrUpdateMessageLog 類型(也支援多數據源、嵌套類型),同時 #[endpoint] 宏會自動為此接口生成 OpenAPI 文檔,簡化參數提取和錯誤處理的代碼。

路由系統

我認為路由系統與其他框架不同。Router 可以寫成平面,也可以寫成樹狀。這裡區分業務邏輯樹與訪問目錄樹。業務邏輯樹根據業務需求劃分 router 結構,形成 router 樹,它不一定與訪問目錄樹一致。

通常我們這樣寫路由:

Router::new().path("articles").get(list_articles).post(create_article);
Router::new()
    .path("articles/{id}")
    .get(show_article)
    .patch(edit_article)
    .delete(delete_article);

查看文章和文章列表通常不需用戶登錄,但創建、編輯、刪除文章等需要登錄認證權限。Salvo 支援嵌套路由系統,可很好滿足這種需求。我們可將不需登錄的路由寫在一起:

Router::new()
    .path("articles")
    .get(list_articles)
    .push(Router::new().path("{id}").get(show_article));

然後將需要登錄的路由寫在一起,並使用相應中間件驗證用戶是否登錄:

Router::new()
    .path("articles")
    .hoop(auth_check)
    .post(list_articles)
    .push(Router::new().path("{id}").patch(edit_article).delete(delete_article));

雖然這兩個路由都有相同的 path("articles"),但它們仍可同時添加到同一個父路由,因此最終路由長這樣:

Router::new()
    .push(
        Router::new()
            .path("articles")
            .get(list_articles)
            .push(Router::new().path("{id}").get(show_article)),
    )
    .push(
        Router::new()
            .path("articles")
            .hoop(auth_check)
            .post(list_articles)
            .push(Router::new().path("{id}").patch(edit_article).delete(delete_article)),
    );

{id} 匹配路徑中的一個片段。通常文章 id 只是數字,這時我們可用正則表達式限制 id 的匹配規則:r"{id:/\d+/}"