185

0

Salvo

一个功能强大且简单的 Rust Web 服务端框架

Salvo 是一个极其简单且功能强大的 Rust Web 后端框架. 仅仅需要基础 Rust 知识即可开发后端服务.

Note: salvo's main branch is currently preparing breaking changes. For the most recently released code, look to the 0.37.x branch.

🎯 功能特色

  • 基于 Hyper, Tokio 开发;
  • 支持 Http1, Http2 和 Http3;
  • 统一的中间件和句柄接口;
  • 路由支持无限层次嵌套;
  • 每一个路由都可以拥有一个或者多个中间件;
  • 集成 Multipart 表单处理;
  • 支持 WebSocket;
  • 支持 Acme, 自动从 let's encrypt 获取 TLS 证书.

⚡️ 快速开始

你可以查看实例代码, 或者访问官网.

创建一个全新的项目:

cargo new hello_salvo --bin

添加依赖项到 Cargo.toml

[dependencies]
salvo = "*"
tokio = { version = "1", features = ["macros"] }

main.rs 中创建一个简单的函数句柄, 命名为 hello, 这个函数只是简单地打印文本 "Hello World".

use salvo::prelude::*;

#[handler]
async fn hello(_req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    res.render(Text::Plain("Hello World"));
}

中间件

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)
    .post(list_articles)
    .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)
            .post(list_articles)
            .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.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
        } else {
            res.render("Ok");
        }
    } else {
        res.set_status_code(StatusCode::BAD_REQUEST);
    }
}

提取请求数据

可以轻松地从多个不同数据源获取数据, 并且组装为你想要的类型. 可以先定义一个自定义的类型, 比如:

#[derive(Serialize, Deserialize, Extractible, Debug)]
/// 默认从 body 中获取数据字段值
#[extract(default_source(from = "body"))]
struct GoodMan<'a> {
    /// 其中, id 号从请求路径参数中获取, 并且自动解析数据为 i64 类型.
    #[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));
}

数据类型的定义有相当大的灵活性, 甚至可以根据需要解析为嵌套的结构:

#[derive(Serialize, Deserialize, Extractible, Debug)]
#[extract(default_source(from = "body", format = "json"))]
struct GoodMan<'a> {
    #[extract(source(from = "param"))]
    id: i64,
    #[extract(source(from = "query"))]
    username: &'a str,
    first_name: String,
    last_name: String,
    lovers: Vec<String>,
    /// 这个 nested 字段完全是从 Request 重新解析.
    #[extract(source(from = "request"))]
    nested: Nested<'a>,
}

#[derive(Serialize, Deserialize, Extractible, Debug)]
#[extract(default_source(from = "body", format = "json"))]
struct Nested<'a> {
    #[extract(source(from = "param"))]
    id: i64,
    #[extract(source(from = "query"))]
    username: &'a str,
    first_name: String,
    last_name: String,
    #[extract(rename = "lovers")]
    #[serde(default)]
    pets: Vec<String>,
}

查看完整源码

更多示例

您可以从 examples 文件夹下查看更多示例代码, 您可以通过以下命令运行这些示例:

cargo run --bin example-basic-auth

您可以使用任何你想运行的示例名称替代这里的 basic-auth.

中国用户可以添加我微信(chrislearn), 拉微信讨论群.

🚀 性能

Benchmark 测试结果可以从这里查看:

https://web-frameworks-benchmark.netlify.app/result?l=rust

https://www.techempower.com/benchmarks/#section=data-r21 techempower

🎇 部署

你可以通过 shuttle.rs 部署你的 Salvo 项目, 这非常简单, 具体参见 shuttle's [官方文档)(https://docs.shuttle.rs/guide/salvo-examples.html).

🩸 贡献

非常欢迎大家为项目贡献力量,可以通过以下方法为项目作出贡献:

  • 在 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是一个开源项目, 如果想支持本项目, 可以 ☕ 在这里买一杯咖啡.

Alipay Weixin

⚠️ 开源协议

Salvo 项目采用以下开源协议: