Salvo(赛风) 是一个极其简单且功能强大的 Rust Web 后端框架。仅仅需要基础 Rust 知识即可开发后端服务。
中国用户可以添加我微信 (chrislearn), 拉微信讨论群或者直接加入 QQ 群:823441777.
- 基于 Hyper 1, Tokio 开发;
- 支持 HTTP1, HTTP2 和 HTTP3;
- 统一的中间件和句柄接口;
- 路由可以无限嵌套,并且可以在任何路由中附加多个中间件;
- 集成 Multipart 表单处理;
- 支持 WebSocket, WebTransport;
- 支持 OpenAPI;
- 支持 Acme, 自动从 let's encrypt获取 TLS 证书。
- 支持 Tower Service 和 Layer.
只需要几行代码就可以实现一个同时支持 ACME 自动获取证书以及支持 HTTP1,HTTP2,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()
.add_domain("test.salvo.rs") // 用你自己的域名替换此域名
.http01_challenge(&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;
}
Salvo CLI 是一个命令行工具,可以简化创建新的 Salvo 项目的过程,支持 Web API、网站、数据库(包括通过 SQLx、SeaORM、Diesel、Rbatis 支持的 SQLite、PostgreSQL、MySQL)和基本的中间件的模板。 你可以使用 salvo-cli来创建一个新的 Salvo 项目:
cargo install salvo-cli
salvo new project_name
您可以从 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-r22
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)