180 个版本 (66 个重大更改)
新功能 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 | 2020 年 2 月 23 日 |
#1483 在 HTTP 服务器
149,385 每月下载量
在 49 个软件包 中使用 (直接使用 18 个)
645KB
14K SLoC
Salvo 是一个极其简单且强大的 Rust Web 后端框架。只需基本的 Rust 知识即可开发后端服务。
🎯 功能
- 使用 Hyper 1 和 Tokio 构建;
- HTTP1、HTTP2 和 HTTP3;
- 统一的中间件和处理器接口;
- 路由器可以无限嵌套,可以将多个中间件附加到任何路由器;
- 集成多部分表单处理;
- 支持 WebSocket、WebTransport;
- 支持 OpenAPI,自动生成 OpenAPI 数据;
- 支持 Acme,自动从 Let's Encrypt 获取 TLS 证书;
- 支持 Tower Service 和 Layer;
⚡️ 快速入门
使用 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("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) = 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许可证,版本2.0,(LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)。
-
MIT许可证(LICENSE-MIT 或 http://opensource.org/licenses/MIT)。
依赖项
~25–42MB
~892K SLoC