使用rust编写一个web服务不如使用java提供的spring boot一样简单,需要手工去添加依赖,目前rust web生态已趋近成熟,可以尝试进行web开发。
本次开发的服务使用的依赖有
- axum:一个专注于生态和模块化的web应用开发框架
- serde:rust中数据的泛用性序列化/反序列化库
- tokio:异步运行时库
- tower:为server/client提供模块化和可重用的库
- tower-http:专为HTTP协议提供的模块化和可重用的库
- tracing:日志库
- tracing-subscriber:给tracing日志库提供工具和组合消费者的方法,这个可以提供给axum使用
- bb8:连接池,基于tokio
- bb8_postgres:连接池,专为postgres提供
做一个简单的Web应用,有以下几个步骤
- 设置db schema
- 编写对应schema的rust struct
- 规划router,加入http endpoints
- 规划handlers
- 规划前后端的数据交互格式
- 写代码
- 测试
我们一步一步来,首先我们先创建一个应用
cargo new todolist
然后,我们添加依赖,这里我们使用cargo add 添加
cargo add axum serde tokio tower tower-http tracing tracing-subscriber bb8 bb8-postgres clap --features serde/derive,tokio/rt-multi-thread,tower-http/fs,tower-http/trace,clap/derive
这样的话,就不用添加版本了。
这里我们建一个简单的数据库
create database todolist;
create table todo (
id serial primary key,
description varchar(512) not null,
completed bool not null
);
然后我们正式进入我们的代码部分:
定义postgresql连接,这里我使用了clip库,从命令行传入数据连接参数
// 定义传入参数模型
#[derive(Parser, Debug)]
#[command(version, about, long_about=None)]
struct Args {
#[arg(short='H', long)]
host: String,
#[arg(short, long)]
user: String,
#[arg(short, long)]
password: String,
#[arg(short, long)]
dbname: String,
}
// 主体部分,建立postgreSQL的数据库连接
let args = Args::parse();
let connection_str = format!("host={} user={} password={} dbname={}", args.host,args.user,args.password,args.dbname);
let manager = PostgresConnectionManager::new_from_stringlike(connection_str, NoTls).unwrap();
let pool = Pool::builder().build(manager).await.unwrap();
这里,我使用了axum中的AppState来管理全局所要使用的变量,在axum中使用Router::new()
提供的with_state
,值得注意的是,这里的struct必须实现Clone trait。
#[derive(Clone)]
struct MyAppState {
dbpool: Pool<PostgresConnectionManager<NoTls>>,
}
接下来,我们定义初始化日志模块
tracing_subscriber::fmt::init();
一行代码就能搞定。
然后我们定义几个model,注意这里面实现的trait,serde提供的Serialize
,Deserialize
,还有Debug
, Clone
。
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Todo {
id: i32,
description: String,
completed: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct CreateTodo {
description: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct UpdateTodo {
id: i32,
description: Option<String>,
completed: Option<bool>
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct ResultWrapper<T> {
code: u32,
message: String,
data: Option<T>,
}
接下来我们定义handler,分别是获取todo数据列表,新建数据列表和删除数据列表
这里要求返回的结果必须实现IntoResponse,否则无法在axum的Route中注册,可以使用axum提供的Json Struct包括数据和结果,这样就能将数据正常转换为Respone。
State则在axum中进行注册,可以直接在参数列表中传入,这里bb8提供的Pool,不用考虑所有权,不使用clone,直接进行get使用。
返回的结果为一个tuple,第一元素为状态码,第二个为参数。
async fn todo_list(State(app_state): State<MyAppState>) -> impl IntoResponse {
match app_state.dbpool.get().await {
Ok(db) => match db.query("SELECT * FROM todo", &[]).await {
Ok(rows) => {
let data: Vec<Todo> = rows.into_iter().map(|i| {
Todo {
id: i.get(0),
description: i.get(1),
completed: i.get(2)
}
}).collect();
(StatusCode::OK, Json(ResultWrapper{
code: 0, message: "ok".to_string(), data: Some(data)})