Salvo(賽風) 是一個極其簡單且功能強大的 Rust Web 後端框架. 僅僅需要基礎 Rust 知識即可開發後端服務.
中国用户可以添加我微信(chrislearn), 拉微信讨论群.
- 基於 Hyper, Tokio 開發;
- 統一的中間件和句柄接口;
- 支持 HTTP1, HTTP2 和 HTTP3;
- 路由可以無限嵌套,並且可以在任何路由是附加多個中間件;
- 集成 Multipart 錶單處理;
- 支持 WebSocket, WebTransport;
- 支持 OpenAPI;
- 支持 Acme, 自動從 let's encrypt 獲取 TLS 證書.
- 支持 Tower serivce 和 layer.
Salvo CLI是一個命令行工具,可以簡化創建新的Salvo項目的過程,支援Web API、網站、資料庫(包括透過SQLx、SeaORM、Diesel、Rbatis支援的SQLite、PostgreSQL、MySQL)和基本的中介軟體的模板。 你可以使用 salvo-cli 来來創建一個新的 Salvo 項目:
cargo install salvo-cli
salvo new project_name
輕輕鬆鬆實現一個支持 ACME 自動獲取證書的,支持 HTTP3 的服務器.
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 mut router = Router::new().get(hello);
let listener = TcpListener::new("0.0.0.0:443")
.acme()
.cache_path("temp/letsencrypt")
.add_domain("test.salvo.rs")
.http01_challege(&mut router).quinn("0.0.0.0:443");
let acceptor = listener.join(TcpListener::new("0.0.0.0:80")).bind().await;
Server::new(acceptor).serve(router).await;
}
Salvo 中的中間件其實就是 Handler, 冇有其他任何特別之處. 所以書寫中間件並不需要像其他某些框架需要掌握泛型關聯類型等知識. 隻要你會寫函數就會寫中間件, 就是這麼簡單!!!
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)
這就是一個簡單的中間件, 它嚮 Response
的頭部添加了 Header
, 查看完整源碼.
正常情況下我們是這樣寫路由的:
Router::with_path("articles").get(list_articles).post(create_article);
Router::with_path("articles/<id>")
.get(show_article)
.patch(edit_article)
.delete(delete_article);
往往查看文章和文章列錶是不需要用戶登錄的, 但是創建, 編輯, 刪除文章等需要用戶登錄認證權限才可以. Salvo 中支持嵌套的路由係統可以很好地滿足這種需求. 我們可以把不需要用戶登錄的路由寫到一起:
Router::with_path("articles")
.get(list_articles)
.push(Router::with_path("<id>").get(show_article));
然後把需要用戶登錄的路由寫到一起, 並且使用相應的中間件驗證用戶是否登錄:
Router::with_path("articles")
.hoop(auth_check)
.push(Router::with_path("<id>").patch(edit_article).delete(delete_article));
雖然這兩個路由都有這同樣的 path("articles")
, 然而它們依然可以被同時添加到同一個父路由, 所以最後的路由長成了這個樣子:
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>
匹配了路徑中的一個片段, 正常情況下文章的 id
隻是一個數字, 這是我們可以使用正則錶達式限製 id
的匹配規則, r"<id:/\d+/>"
.
還可以通過 <**>
, <*+>
或者 <*?>
匹配所有剩餘的路徑片段. 為了代碼易讀性性強些, 也可以添加適合的名字, 讓路徑語義更清晰, 比如: <**file_path>
.
有些用於匹配路徑的正則錶達式需要經常被使用, 可以將它事先註冊, 比如 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(),
);
這樣在需要路徑匹配時就變得更簡潔:
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);
}
}
可以輕鬆地從多個不同數據源獲取數據, 並且組裝為你想要的類型. 可以先定義一個自定義的類型, 比如:
#[derive(Serialize, Deserialize, Extractible, Debug)]
/// 默認從 body 中獲取數據字段值
#[salvo(extract(default_source(from = "body")))]
struct GoodMan<'a> {
/// 其中, id 號從請求路徑參數中獲取, 並且自動解析數據為 i64 類型.
#[salvo(extract(source(from = "param")))]
id: i64,
/// 可以使用引用類型, 避免內存複製.
username: &'a str,
first_name: String,
last_name: String,
}
然後在 Handler
中可以這樣獲取數據:
#[handler]
async fn edit(req: &mut Request) {
let good_man: GoodMan<'_> = req.extract().await.unwrap();
}
甚至於可以直接把類型作為參數傳入函數, 像這樣:
#[handler]
async fn edit<'a>(good_man: GoodMan<'a>) {
res.render(Json(good_man));
}
查看完整源碼
無需對項目做大的改動,即可實現對 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
.unshift(doc.into_router("/api-doc/openapi.json"))
.unshift(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 文件夾下查看更多示例代碼, 您可以通過以下命令運行這些示例:
cd examples
cargo run --bin example-basic-auth
您可以使用任何你想運行的示例名稱替代這裏的 basic-auth
.
Benchmark 測試結果可以從這裏查看:
https://web-frameworks-benchmark.netlify.app/result?l=rust
https://www.techempower.com/benchmarks/#section=data-r21
非常歡迎大家為項目貢獻力量,可以通過以下方法為項目作出貢獻:
- 在 issue 中提交功能需求和 bug report;
- 在 issues 或者 require feedback 下留下自己的意見;
- 通過 pull requests 提交代碼;
- 在博客或者技術平臺發錶 Salvo 相關的技術文章。
All pull requests are code reviewed and tested by the CI. Note that unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Salvo by you shall be dual licensed under the MIT License, without any additional terms or conditions.
Salvo
是一個開源項目, 如果想支持本項目, 可以 ☕ 在這裏買一杯咖啡.
Salvo 項目採用以下開源協議:
-
Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
-
MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)