Skip to content

Latest commit

 

History

History
326 lines (251 loc) · 11.3 KB

README.osc.md

File metadata and controls

326 lines (251 loc) · 11.3 KB

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

中国用户可以添加我微信(chrislearn), 拉微信讨论群或者直接加入QQ群:823441777.

🎯 功能特色

  • 基于 Hyper 1, Tokio 开发;
  • 支持 HTTP1, HTTP2 和 HTTP3;
  • 统一的中间件和句柄接口;
  • 路由可以无限嵌套,并且可以在任何路由中附加多个中间件;
  • 集成 Multipart 表单处理;
  • 支持 WebSocket, WebTransport;
  • 支持 OpenAPI;
  • 支持 Acme, 自动从 let's encrypt 获取 TLS 证书.
  • 支持 Tower Serivce 和 Layer.

⚡️ 快速开始

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

支持 ACME 自动获取证书和 HTTP3 的 Hello World

只需要几行代码就可以实现一个同时支持 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_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;
}

中间件

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
        .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 CLI是一个命令行工具,可以简化创建新的Salvo项目的过程,支持Web API、网站、数据库(包括通过SQLx、SeaORM、Diesel、Rbatis支持的SQLite、PostgreSQL、MySQL)和基本的中间件的模板。 你可以使用 salvo-cli 来创建一个新的 Salvo 项目:

安装

cargo install salvo-cli

创建一个salvo项目

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是一个开源项目, 如果想支持本项目, 可以 ☕ 请我喝杯咖啡.

Alipay        Weixin

⚠️ 开源协议

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