152个版本 (59个重大更改)
新版本 0.71.1 | 2024年8月18日 |
---|---|
0.70.0 | 2024年8月13日 |
0.68.5 | 2024年7月9日 |
0.66.2 | 2024年3月6日 |
0.1.6 |
|
在HTTP服务器类别中排名 11
每月下载量 150,525
在 33 个Crate中 使用(直接使用 30 个)
2MB
19K SLoC
Salvo是一个极其简单且强大的Rust Web后端框架。只需具备基本的Rust知识,就可以开发后端服务。
🎯 特性
- 使用Hyper 1和Tokio构建;
- HTTP1、HTTP2和HTTP3;
- 统一的中间件和处理器接口;
- 路由器可以无限嵌套,可以将多个中间件附加到任何路由器;
- 集成Multipart表单处理;
- 支持WebSocket、WebTransport;
- 支持OpenAPI,自动生成OpenAPI数据;
- 支持Acme,自动从Let's Encrypt获取TLS证书;
- 支持Tower服务和层;
⚡️ 快速开始
使用ACME和HTTP3的Hello World
只需几行代码即可实现支持ACME自动获取证书并支持HTTP1、HTTP2、HTTP3协议的服务器。
use salvo::prelude::*;
#[handler]
async fn hello(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") // Replace this domain name with your own.
.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;
}
中间件
处理器和中间件之间没有区别,中间件只是处理器。 因此,如果您能编写函数,就可以编写中间件!您不需要了解关联类型、泛型类型等概念。
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
,但它们仍然可以同时添加到同一父路由,因此最终的路由看起来是这样的
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) = tokio::fs::copy(&file.path, Path::new(&dest)).await {
res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
} else {
res.render("Ok");
}
} else {
res.status_code(StatusCode::BAD_REQUEST);
}
}
从请求中提取数据
您可以从多个不同的数据源轻松获取数据,并将其组装成您想要的类型。您可以先定义一个自定义类型,例如
#[derive(Serialize, Deserialize, Extractible, Debug)]
/// Get the data field value from the body by default.
#[salvo(extract(default_source(from = "body")))]
struct GoodMan<'a> {
/// The id number is obtained from the request path parameter, and the data is automatically parsed as i64 type.
#[salvo(extract(source(from = "param")))]
id: i64,
/// Reference types can be used to avoid memory copying.
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
无需对项目进行重大更改,即可实现完美的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;
}
🛠️ Salvo CLI
Salvo CLI是一个命令行工具,用于简化新Salvo项目的创建,支持Web API、网站、数据库(包括SQLite、PostgreSQL和MySQL通过SQLx、SeaORM、Diesel、Rbatis)和基本中间件的模板。您可以使用salvo-cli创建新的Salvo项目
安装
cargo install salvo-cli
创建一个新的salvo项目
salvo new project_name
更多示例
您可以在examples文件夹中找到更多示例。您可以使用以下命令运行这些示例
cd examples
cargo run --bin example-basic-auth
您可以使用任何示例名称运行,而不是这里的basic-auth
。
🚀 性能
基准测试结果可从这里找到
https://web-frameworks-benchmark.netlify.app/result?l=rust
https://www.techempower.com/benchmarks/#section=data-r22
🩸 贡献者
☕️ 捐赠
Salvo 是一个开源项目。如果您想支持 Salvo,您可以 ☕️ 在这里给我买一杯咖啡。
⚠️ 许可证
Salvo 使用以下任何一个许可证:
-
Apache License,版本 2.0,(LICENSE-APACHE 或 http://www.apache.org/licenses/LICENSE-2.0).
-
MIT 许可证 (LICENSE-MIT 或 http://opensource.org/licenses/MIT).
依赖
~18–37MB
~690K SLoC