SalvoRust 编写的 Web 后端框架

联合创作 · 2023-09-20 01:35

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

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

功能特色

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

 快速开始

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

Hello World

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"));
}

#[tokio::main]
async fn main() {
    let acceptor = TcpListener::new("127.0.0.1:5800").bind().await;
    let router =  Router::new().get(hello);
    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 支持

无需对项目做大的改动,即可实现对 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;
}

更多示例

您可以从 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

 

开源协议

Salvo 项目采用 MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)

浏览 21
点赞
评论
收藏
分享

手机扫一扫分享

编辑 分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

编辑 分享
举报