Salvo(赛风)Rust Web框架
Salvo 是国产开源、基于 Tokio + Hyper 的异步 Rust Web 框架,中文文档完善,主打低样板代码、简洁API、全功能内置,对标 Axum / Actix‑web,目前版本 0.x(尚未1.0),核心已经稳定可用。
- GitHub:https://github.com/salvo‑rs/salvo
- 官网中文文档:https://salvo.rs/zh‑hans/guide/
- 底层:Tokio 异步运行时 + Hyper HTTP库,支持 HTTP/1、HTTP/2、HTTP/3(QUIC) 完整协议栈。
核心设计理念:Handler 就是 Middleware(中间件),没有区分两种不同类型,写普通函数就可以写中间件,大幅降低学习成本。
Salvo核心概念
1. Handler 处理器
用 #[handler] 宏标记异步函数,就是接口处理器。 参数可以按需省略,不需要全部写 Request, Depot, Response;
Request:请求对象,读取参数、body、headerDepot:请求上下文存储,用于中间件传递数据(比如登录用户信息)Response:响应对象,返回json、文本、状态码
use salvo::prelude::*;
#[handler]
async fn hello(res: &mut Response) {
res.render("Hello Salvo");
}
#[tokio::main]
async fn main() {
let router = Router::new().get(hello);
let acceptor = TcpListener::new("127.0.0.1:7878").bind().await;
Server::new(acceptor).serve(router).await;
}2. Router 树状路由(核心特色)
Salvo 路由是树状嵌套路由,支持无限层级嵌套;.hoop() 挂载中间件,可以作用于整个子路由分支,实现局部鉴权。
let router = Router::new()
// 公开接口
.push(Router::with_path("public").get(public_handler))
// 需要鉴权的接口组,hoop 只作用于此分支
.push(
Router::with_path("api")
.hoop(auth_middleware) // 该路由下全部接口都会经过鉴权
.push(Router::with_path("user").get(get_user).post(create_user))
.push(Router::with_path("article/{id}").get(get_article))
);{id}路径参数;.push()嵌套子路由;.hoop(mid):挂载中间件,可以作用到任意子路由,粒度非常灵活。
3. 中间件 hoop
中间件本质就是一个 #[handler] 函数。
- 可以写在根路由(全局生效),也可以写在子路由(局部生效);
- 通过
FlowCtrl控制流程:调用FlowCtrl::Next()继续往下执行,FlowCtrl::Break()直接终止请求,返回响应。
#[handler]
async fn log_mid(req: &mut Request, depot: &mut Depot, res: &mut Response) -> FlowCtrl {
println!("请求路径: {}", req.uri().path());
FlowCtrl::Next() // 继续执行后续handler
}4. 类型安全参数提取 Extractible
通过 derive 宏 Extractible,自动从请求的路径参数、query、body、header提取数据,编译期校验,不用手动解析json/query字符串。
#[derive(Debug, Deserialize, Extractible)]
#[salvo(extract(default_source(from = "body")))]
struct UserReq {
#[salvo(extract(source(from = "param")))]
id: u64, // 从url路径{id}取
name: String, // 从body json取
}
#[handler]
async fn update_user(req: UserReq) -> Json<UserReq> {
Json(req)
}5. OpenAPI 自动文档(一等公民支持)
把 #[handler] 换成 #[endpoint],框架自动生成 OpenAPI3 规范,内置Swagger UI,几乎零配置,不用手动写schema描述接口。
#[endpoint]
async fn hello() -> &'static str {
"hello"
}6. Depot 请求上下文
Depot 是每个请求独立的存储,中间件可以把数据放入Depot,后续handler直接读取。 例如鉴权中间件把登录用户id存入depot,业务handler直接拿。
// 中间件存数据
depot.insert("user_id", 100u64);
// handler读取
let uid = depot.get::<u64>("user_id").unwrap();Salvo内置主要功能
- HTTP/1、HTTP/2、HTTP/3(QUIC) 完整支持,内置ACME自动HTTPS证书申请(Let’s Encrypt),不用额外工具管理证书。
- WebSocket、SSE 长连接原生支持。
- 内置JSON、表单、multipart文件上传、静态文件服务。
- 内置CORS、JWT、Session、压缩等常用中间件。
- 模板渲染(Tera)。
salvo‑cli脚手架工具,一键生成项目模板,支持SQLx/SeaORM数据库模板,JWT、CORS预设。- 错误处理:支持返回Result,自动处理错误响应。
快速启动
cargo new salvo-demo
cd salvo-demo
cargo add salvo tokio --features salvo/oapi,tokio/macrosSalvo vs Axum vs Actix‑web
| 特性 | Salvo | Axum | Actix‑web |
|---|---|---|---|
| 底层 | Tokio+Hyper | Tokio+Tower | 自己的运行时 |
| 中间件 | Handler即中间件,hoop,嵌套路由局部生效 | Tower中间件,泛型复杂 | Wrap,需要实现Trait |
| OpenAPI | 内置#[endpoint]自动生成 | 需要第三方库 | 需要第三方库 |
| HTTP3 | ✅内置 | 需要额外配置 | ✅支持 |
| 中文文档 | ⭐⭐⭐⭐⭐(官方中文) | ⭐⭐ | ⭐⭐ |
| 路由嵌套 | 树状嵌套,支持局部中间件 | 嵌套路由,中间件全局/路由级 | 嵌套路由 |
| 版本 | 0.x(未1.0) | 稳定 | 稳定 |
| 生态 | 国内社区活跃,国内项目多 | 国际主流,生态庞大 | 老牌高性能,生态成熟 |
Salvo优缺点
✅ 优点
- API简洁,样板代码少,学习曲线平缓,对Rust新手友好。
- 中文官方文档,国内开发者友好。
- 树状路由+局部中间件,大型项目组织结构清晰。
- OpenAPI文档原生,不用额外插件。
- HTTP3、ACME自动HTTPS内置,开箱即用。
- 性能属于Rust Web第一梯队,benchmark表现优秀。
❌ 缺点
- 还没有发布1.0版本,版本迭代可能会有breaking change。
- 国际社区规模不如Axum,第三方生态库数量少于Axum。
- 部分高级场景需要自己找第三方crates。
适合与不适合场景
✅适合
- 后端API服务、RESTful接口
- 需要自动OpenAPI文档的业务服务
- 中小型Rust后端,希望快速开发,不想写大量模板代码
- 需要HTTP3、自动HTTPS证书的服务
- 国内开发者,偏好中文文档
❌不适合
- 追求绝对稳定、要求1.0正式版本的大型企业核心业务;
- 重度依赖大量第三方生态库的项目(优先Axum)。
Salvo常见配套生态
- ORM:SeaORM、SQLx、Diesel
- 认证:salvo内置JWT,也可使用其他库
- 数据库:PostgreSQL / MySQL / SQLite / MongoDB
- 部署:编译为单二进制文件,直接部署,或者docker。
Salvo 完整可运行示例
包含:嵌套路由、鉴权中间件、JSON接口、OpenAPI文档、请求参数提取、静态文件,复制即可编译运行。
Cargo.toml
[package]
name = "salvo-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
salvo = { version = "0.76", features = ["oapi", "websocket", "multipart", "static-files", "jwt"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"src/main.rs
use salvo::prelude::*;
use serde::{Deserialize, Serialize};
// -------------------------- 数据模型 --------------------------
#[derive(Debug, Serialize, Deserialize, Extractible)]
#[salvo(extract(default_source(from = "body")))]
pub struct CreateUser {
pub name: String,
pub age: u8,
}
#[derive(Debug, Serialize)]
pub struct User {
pub id: u64,
pub name: String,
pub age: u8,
}
// -------------------------- 中间件:简单鉴权 --------------------------
#[handler]
async fn auth_mid(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) -> FlowCtrl {
// 模拟校验token
let token = req.headers().get("Authorization").and_then(|v| v.to_str().ok());
match token {
Some(t) if t == "Bearer demo-token-123" => {
// 把用户信息存入Depot,后续handler读取
depot.insert("current_user", User {
id: 1,
name: "admin".into(),
age: 25,
});
FlowCtrl::Next()
}
_ => {
res.status_code(StatusCode::UNAUTHORIZED);
res.render(Json(serde_json::json!({"msg":"未授权"})));
FlowCtrl::Break()
}
}
}
// -------------------------- 接口处理器 --------------------------
/// 公开接口,不需要鉴权
#[endpoint]
async fn hello() -> &'static str {
"Hello Salvo!"
}
/// 获取用户详情,路径参数 {id}
#[endpoint]
async fn get_user(#[salvo(extract(source(from = "param")))] id: u64) -> Json<User> {
Json(User {
id,
name: format!("user_{id}"),
age: 20,
})
}
/// 创建用户,接收JSON body
#[endpoint]
async fn create_user(body: CreateUser) -> Json<User> {
Json(User {
id: 100,
name: body.name,
age: body.age,
})
}
/// 读取Depot中鉴权后的用户信息
#[endpoint]
async fn current_user(depot: &mut Depot) -> Json<User> {
let user = depot.get::<User>("current_user").unwrap();
Json(user.clone())
}
#[tokio::main]
async fn main() {
// 路由树
let router = Router::new()
// 公开路由
.push(Router::with_path("/").get(hello))
// 受保护API组,全部经过 auth_mid 鉴权
.push(
Router::with_path("/api")
.hoop(auth_mid)
.push(Router::with_path("user/{id}").get(get_user))
.push(Router::with_path("user").post(create_user))
.push(Router::with_path("me").get(current_user))
)
// OpenAPI文档路由
.push(OpenApi::new("salvo-demo-api", ApiDoc::new()).swagger_ui());
let acceptor = TcpListener::new("127.0.0.1:7878").bind().await;
println!("服务启动:http://127.0.0.1:7878");
println!("Swagger文档:http://127.0.0.1:7878/swagger-ui");
Server::new(acceptor).serve(router).await;
}运行
cargo run访问地址
- 主页:
http://127.0.0.1:7878 - Swagger OpenAPI文档:
http://127.0.0.1:7878/swagger-ui
测试受保护接口需要在请求头带上:
Authorization: Bearer demo-token-123
关键知识点小结
#[endpoint]:自动生成OpenAPI文档;普通接口用#[handler]即可。hoop():中间件,可以挂载到任意子路由,只作用于该分支,粒度非常灵活。Depot:每个请求独立存储,中间件与handler之间传递数据。Extractible:自动提取路径参数、query、json body,编译期校验。- FlowCtrl:
Next()继续执行;Break()直接终止请求返回响应。
测试接口示例(curl)
# 公开接口
curl http://127.0.0.1:7878
# 受保护接口,带上token
curl -H "Authorization: Bearer demo-token-123" http://127.0.0.1:7878/api/me
# 创建用户 POST json
curl -X POST -H "Authorization: Bearer demo-token-123" -H "Content-Type: application/json" -d '{"name":"test","age":22}' http://127.0.0.1:7878/api/userSalvo 扩展示例
Salvo 扩展示例:SeaORM + WebSocket + 静态文件 + JWT 基于上面的项目继续扩展,包含:
- SeaORM ORM 数据库操作(SQLite)
- WebSocket 长连接示例
- 静态文件服务
- 真实 JWT 签发&校验鉴权
Cargo.toml
[package]
name = "salvo-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
salvo = { version = "0.76", features = ["oapi", "websocket", "multipart", "static-files", "jwt"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sea-orm = { version = "1.0", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"] }
sea-orm-migration = "1.0"
jsonwebtoken = "9.0"
chrono = { version = "0.4", features = ["serde"] }src/main.rs
use chrono::{Duration, Utc};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use salvo::prelude::*;
use sea_orm::{Database, DatabaseConnection, EntityTrait, IntoActiveModel, Set};
use serde::{Deserialize, Serialize};
// -------------------------- SeaORM 实体定义 --------------------------
mod entity {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub name: String,
pub age: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
use entity::{Entity as UserEntity, Model as UserModel};
// -------------------------- JWT 配置 --------------------------
const JWT_SECRET: &[u8] = b"my-secret-key-123456";
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: i64,
exp: i64,
}
// 生成JWT Token
fn generate_jwt(user_id: i64) -> String {
let expiration = Utc::now() + Duration::hours(24);
let claims = Claims {
sub: user_id,
exp: expiration.timestamp(),
};
let header = Header::new(Algorithm::HS256);
encode(&header, &claims, &EncodingKey::from_secret(JWT_SECRET)).unwrap()
}
// -------------------------- 请求模型 --------------------------
#[derive(Debug, Deserialize, Extractible)]
#[salvo(extract(default_source(from = "body")))]
pub struct LoginReq {
pub name: String,
}
#[derive(Debug, Serialize)]
pub struct LoginResp {
pub token: String,
}
// -------------------------- JWT鉴权中间件 --------------------------
#[handler]
async fn jwt_auth(depot: &mut Depot, req: &mut Request, res: &mut Response, ctrl: &mut FlowCtrl) -> FlowCtrl {
let auth_header = match req.headers().get("Authorization") {
Some(h) => h.to_str().ok(),
None => None,
};
let token_str = match auth_header {
Some(s) if s.starts_with("Bearer ") => &s[7..],
_ => {
res.status_code(StatusCode::UNAUTHORIZED);
res.render(Json(serde_json::json!({"msg":"需要Bearer token"})));
return FlowCtrl::Break();
}
};
// 校验jwt
let token_data = match jsonwebtoken::decode::<Claims>(
token_str,
&jsonwebtoken::DecodingKey::from_secret(JWT_SECRET),
&jsonwebtoken::Validation::new(Algorithm::HS256),
) {
Ok(d) => d,
Err(_) => {
res.status_code(StatusCode::UNAUTHORIZED);
res.render(Json(serde_json::json!({"msg":"token无效"})));
return FlowCtrl::Break();
}
};
depot.insert("user_id", token_data.claims.sub);
FlowCtrl::Next()
}
// -------------------------- WebSocket处理器 --------------------------
#[handler]
async fn ws_echo(req: &mut Request, res: &mut Response) {
let ws = match WebSocket::upgrade(req, res).await {
Ok(ws) => ws,
Err(_) => return,
};
let (mut tx, mut rx) = ws.split();
while let Some(msg) = rx.recv().await {
if let Ok(msg) = msg {
if msg.is_text() || msg.is_binary() {
tx.send(msg).await.ok();
}
} else {
break;
}
}
}
// -------------------------- API接口 --------------------------
/// 登录接口,返回JWT token
#[endpoint]
async fn login(body: LoginReq, db: &State<DatabaseConnection>) -> Json<LoginResp> {
// 模拟查询用户
let user = UserEntity::find()
.filter(entity::Column::Name.eq(body.name))
.one(db)
.await
.unwrap();
let user = match user {
Some(u) => u,
None => {
// 不存在则新建用户
let new_user = entity::ActiveModel {
name: Set(body.name),
age: Set(18),
..Default::default()
};
UserEntity::insert(new_user).exec(db).await.unwrap().last_insert_id
}
};
let token = generate_jwt(user.id);
Json(LoginResp { token })
}
/// 获取当前登录用户信息(需要jwt鉴权)
#[endpoint]
async fn get_me(depot: &mut Depot, db: &State<DatabaseConnection>) -> Json<UserModel> {
let uid = depot.get::<i64>("user_id").unwrap();
let user = UserEntity::find_by_id(*uid).one(db).await.unwrap().unwrap();
Json(user)
}
#[tokio::main]
async fn main() {
// 1. 初始化数据库 SQLite
let db = Database::connect("sqlite://./demo.db?mode=rwc").await.unwrap();
// 执行迁移(简单示例,实际项目使用 sea‑orm‑migration)
sea_orm_migration::create_table!(
db,
users,
id integer primary key autoincrement,
name text not null,
age integer not null
)
.await
.unwrap();
// 2. 路由
let router = Router::new()
// 静态文件:访问 /static/* 读取 ./static 目录
.push(StaticFiles::new("./static").with_path("static"))
// WebSocket
.push(Router::with_path("ws").get(ws_echo))
// 公开接口:登录
.push(Router::with_path("login").post(login))
// 需要JWT鉴权的API分组
.push(
Router::with_path("api")
.hoop(jwt_auth)
.push(Router::with_path("me").get(get_me))
)
// OpenAPI文档
.push(OpenApi::new("salvo‑demo‑full", ApiDoc::new()).swagger_ui());
// 注入数据库连接到State全局状态
let router = router.with_state(db);
let acceptor = TcpListener::new("127.0.0.1:7878").bind().await;
println!("服务启动: http://127.0.0.1:7878");
println!("Swagger: http://127.0.0.1:7878/swagger-ui");
println!("WebSocket: ws://127.0.0.1:7878/ws");
Server::new(acceptor).serve(router).await;
}运行步骤
- 创建静态资源文件夹
static,里面放任意html/css文件,访问http://127.0.0.1:7878/static/xxx.html - 执行
cargo run,会自动生成demo.dbsqlite数据库。
测试curl示例
# 登录,获取token
curl -X POST -H "Content-Type: application/json" -d '{"name":"alice"}' http://127.0.0.1:7878/login
# 使用token访问受保护接口
curl -H "Authorization: Bearer 这里替换成返回的token" http://127.0.0.1:7878/api/meWebSocket测试
可以使用在线ws测试工具连接:ws://127.0.0.1:7878/ws,发送消息会原样回显。
重点知识点
State<T>:全局共享状态,把数据库连接注入路由,所有handler都可以获取。- JWT鉴权中间件:
hoop(jwt_auth)挂载到/api分支,该分支全部接口强制校验token。 - WebSocket:
WebSocket::upgrade升级http连接,收发消息。 - StaticFiles:内置静态文件服务,可托管前端静态资源。
- SeaORM:ORM操作SQLite,简单建表,CRUD。
生产环境注意事项
- JWT密钥不要硬编码,使用环境变量读取。
- 数据库迁移建议使用
sea‑orm‑migration独立迁移文件,不要在main函数直接建表。 - 生产建议开启HTTPS,Salvo内置ACME自动证书。
- 错误处理:示例为简单演示,真实业务需要完善错误捕获。