SalvoRust 編寫的 Web 后端框架
Salvo 是一個(gè)極其簡單且功能強(qiáng)大的 Rust Web 后端框架. 僅僅需要基礎(chǔ) Rust 知識即可開發(fā)后端服務(wù).
中國用戶可以添加我微信(chrislearn), 拉微信討論群.
功能特色
- 基于 Hyper, Tokio 開發(fā);
- 支持 HTTP1, HTTP2 和 HTTP3;
- 統(tǒng)一的中間件和句柄接口;
- 路由支持無限層次嵌套;
- 每一個(gè)路由都可以擁有一個(gè)或者多個(gè)中間件;
- 集成 Multipart 表單處理;
- 支持 WebSocket, WebTransport;
- 支持 OpenAPI;
- 支持 Acme, 自動從 let's encrypt 獲取 TLS 證書.
快速開始
Hello World
在 main.rs 中創(chuàng)建一個(gè)簡單的函數(shù)句柄, 命名為hello, 這個(gè)函數(shù)只是簡單地打印文本 "Hello World".
use salvo::prelude::*; #[handler] async fn hello(_req: &mut Request, _depot: &mut Depot, res: &mut Response) { res.render(Text::Plain("Hello World")); } #[tokio::main] async fn main() { let acceptor = TcpListener::new("127.0.0.1:5800").bind().await; let router = Router::new().get(hello); Server::new(acceptor).serve(router).await; }
中間件
Salvo 中的中間件其實(shí)就是 Handler, 沒有其他任何特別之處. 所以書寫中間件并不需要像其他某些框架需要掌握泛型關(guān)聯(lián)類型等知識. 只要你會寫函數(shù)就會寫中間件, 就是這么簡單!!!
use salvo::http::header::{self, HeaderValue}; use salvo::prelude::*; #[handler] async fn add_header(res: &mut Response) { res.headers_mut() .insert(header::SERVER, HeaderValue::from_static("Salvo")); }
然后將它添加到路由中:
Router::new().hoop(add_header).get(hello)
這就是一個(gè)簡單的中間件, 它向 Response 的頭部添加了 Header, 查看完整源碼.
可鏈?zhǔn)綍鴮懙臉錉盥酚上到y(tǒng)
正常情況下我們是這樣寫路由的:
Router::with_path("articles").get(list_articles).post(create_article); Router::with_path("articles/<id>") .get(show_article) .patch(edit_article) .delete(delete_article);
往往查看文章和文章列表是不需要用戶登錄的, 但是創(chuàng)建, 編輯, 刪除文章等需要用戶登錄認(rèn)證權(quán)限才可以. Salvo 中支持嵌套的路由系統(tǒng)可以很好地滿足這種需求. 我們可以把不需要用戶登錄的路由寫到一起:
Router::with_path("articles") .get(list_articles) .push(Router::with_path("<id>").get(show_article));
然后把需要用戶登錄的路由寫到一起, 并且使用相應(yīng)的中間件驗(yàn)證用戶是否登錄:
Router::with_path("articles") .hoop(auth_check) .push(Router::with_path("<id>").patch(edit_article).delete(delete_article));
雖然這兩個(gè)路由都有這同樣的 path("articles"), 然而它們依然可以被同時(shí)添加到同一個(gè)父路由, 所以最后的路由長成了這個(gè)樣子:
Router::new() .push( Router::with_path("articles") .get(list_articles) .push(Router::with_path("<id>").get(show_article)), ) .push( Router::with_path("articles") .hoop(auth_check) .push(Router::with_path("<id>").patch(edit_article).delete(delete_article)), );
<id> 匹配了路徑中的一個(gè)片段, 正常情況下文章的 id 只是一個(gè)數(shù)字, 這是我們可以使用正則表達(dá)式限制 id 的匹配規(guī)則, r"<id:/\d+/>".
還可以通過 <*> 或者 <**> 匹配所有剩余的路徑片段. 為了代碼易讀性性強(qiáng)些, 也可以添加適合的名字, 讓路徑語義更清晰, 比如: <**file_path>.
有些用于匹配路徑的正則表達(dá)式需要經(jīng)常被使用, 可以將它事先注冊, 比如 GUID:
PathFilter::register_wisp_regex( "guid", Regex::new("[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}").unwrap(), );
這樣在需要路徑匹配時(shí)就變得更簡潔:
Router::with_path("<id:guid>").get(index)
查看完整源碼
文件上傳
可以通過 Request 中的 file 異步獲取上傳的文件:
#[handler] async fn upload(req: &mut Request, res: &mut Response) { let file = req.file("file").await; if let Some(file) = file { let dest = format!("temp/{}", file.name().unwrap_or_else(|| "file".into())); if let Err(e) = std::fs::copy(&file.path, Path::new(&dest)) { res.status_code(StatusCode::INTERNAL_SERVER_ERROR); } else { res.render("Ok"); } } else { res.status_code(StatusCode::BAD_REQUEST); } }
提取請求數(shù)據(jù)
可以輕松地從多個(gè)不同數(shù)據(jù)源獲取數(shù)據(jù), 并且組裝為你想要的類型. 可以先定義一個(gè)自定義的類型, 比如:
#[derive(Serialize, Deserialize, Extractible, Debug)] /// 默認(rèn)從 body 中獲取數(shù)據(jù)字段值 #[salvo(extract(default_source(from = "body")))] struct GoodMan<'a> { /// 其中, id 號從請求路徑參數(shù)中獲取, 并且自動解析數(shù)據(jù)為 i64 類型. #[salvo(extract(source(from = "param")))] id: i64, /// 可以使用引用類型, 避免內(nèi)存復(fù)制. username: &'a str, first_name: String, last_name: String, }
然后在 Handler 中可以這樣獲取數(shù)據(jù):
#[handler] async fn edit(req: &mut Request) { let good_man: GoodMan<'_> = req.extract().await.unwrap(); }
甚至于可以直接把類型作為參數(shù)傳入函數(shù), 像這樣:
#[handler] async fn edit<'a>(good_man: GoodMan<'a>) { res.render(Json(good_man)); }
查看完整源碼
OpenAPI 支持
無需對項(xiàng)目做大的改動,即可實(shí)現(xiàn)對 OpenAPI 的完美支持。
#[derive(Serialize, Deserialize, ToSchema, Debug)] struct MyObject<T: ToSchema + std::fmt::Debug> { value: T, } #[endpoint] async fn use_string(body: JsonBody<MyObject<String>>) -> String { format!("{:?}", body) } #[endpoint] async fn use_i32(body: JsonBody<MyObject<i32>>) -> String { format!("{:?}", body) } #[endpoint] async fn use_u64(body: JsonBody<MyObject<u64>>) -> String { format!("{:?}", body) } #[tokio::main] async fn main() { tracing_subscriber::fmt().init(); let router = Router::new() .push(Router::with_path("i32").post(use_i32)) .push(Router::with_path("u64").post(use_u64)) .push(Router::with_path("string").post(use_string)); let doc = OpenApi::new("test api", "0.0.1").merge_router(&router); let router = router .push(doc.into_router("/api-doc/openapi.json")) .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui")); let acceptor = TcpListener::new("127.0.0.1:5800").bind().await; Server::new(acceptor).serve(router).await; }
更多示例
您可以從 examples 文件夾下查看更多示例代碼, 您可以通過以下命令運(yùn)行這些示例:
cd examples
cargo run --bin example-basic-auth
您可以使用任何你想運(yùn)行的示例名稱替代這里的 basic-auth.
性能
Benchmark 測試結(jié)果可以從這里查看:
https://web-frameworks-benchmark.netlify.app/result?l=rust
https://www.techempower.com/benchmarks/#section=data-r21
開源協(xié)議
Salvo 項(xiàng)目采用 MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
