使用Rust编写一个web todolist应用

使用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应用,有以下几个步骤

  1. 设置db schema
  2. 编写对应schema的rust struct
  3. 规划router,加入http endpoints
  4. 规划handlers
  5. 规划前后端的数据交互格式
  6. 写代码
  7. 测试

我们一步一步来,首先我们先创建一个应用

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提供的SerializeDeserialize,还有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)}))
            }, Err(e) => {
                (StatusCode::INTERNAL_SERVER_ERROR, Json(ResultWrapper{code: 500, message: e.to_string(), data: None}))
            }
        }, Err(e) => {
            (StatusCode::INTERNAL_SERVER_ERROR, Json(ResultWrapper{code: 500, message: e.to_string(), data: None}))
        }
    }
}

async fn todo_delete(State(pool): State<MyAppState>, Json(id): Json<i32>) -> impl IntoResponse {
    match pool.dbpool.get().await {
        Ok(db) => match db.execute("DELETE FROM todo WHERE id = $1", &[&id]).await {
            Ok(r) => {
                tracing::info!("todo list id {} had been deleted", id);
                (
                    StatusCode::OK,
                    Json(ResultWrapper {
                        code: 0,
                        message: "ok".to_string(),
                        data: Some(r),
                    }),
                )
            }
            Err(e) => (
                StatusCode::BAD_REQUEST,
                Json(ResultWrapper {
                    code: 500,
                    message: e.to_string(),
                    data: None,
                }),
            ),
        },
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ResultWrapper {
                code: 500,
                message: e.to_string(),
                data: None,
            }),
        ),
    }
}

async fn todo_create(
    State(pool): State<MyAppState>,
    Json(input): Json<CreateTodo>,
) -> impl IntoResponse {
    match pool.dbpool.get().await {
        Ok(db) => {
            match db
                .query(
                    "INSERT INTO todo (description, completed) VALUES ($1, FALSE) RETURNING id",
                    &[&input.description],
                )
                .await
            {
                Ok(rows) => {
                    if let Some(row) = rows.get(0) {
                        let id: i32 = row.get(0);
                        (
                            StatusCode::OK,
                            Json(ResultWrapper {
                                code: 0,
                                message: "ok".to_string(),
                                data: Some(id),
                            }),
                        )
                    } else {
                        (
                            StatusCode::INTERNAL_SERVER_ERROR,
                            Json(ResultWrapper {
                                code: 400,
                                message: "no return data".to_string(),
                                data: None,
                            }),
                        )
                    }
                }
                Err(e) => (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    Json(ResultWrapper {
                        code: 500,
                        message: e.to_string(),
                        data: None,
                    }),
                ),
            }
        }
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ResultWrapper {
                code: 500,
                message: e.to_string(),
                data: None,
            }),
        ),
    }
}

我们看一下,axum的主体部分,即路由注册和端口注册启动这个环节

这里面有:

  • 注册路由
  • 添加app_state
  • 增加日志部分
  • 启动web server服务
let app = Router::new()
   .route("/", post(todo_create))
   .route("/", delete(todo_delete))
   .route("/", get(todo_list))
   .with_state(my_state)
   .layer(TraceLayer::new_for_http());

let listener = tokio::net::TcpListener::bind("127.0.0.1:8889")
   .await
   .unwrap();
axum::serve(listener, app).await.unwrap();

这样,整体一个简单的todolist webserver就已完成,这里面还有一个update部分没有编写,不过仿照上面的handler也可以编写出来。

下面是整体代码

use axum::{
    extract::State,
    http::StatusCode,
    response::IntoResponse,
    routing::{delete, get, post},
    Json, Router,
};
use bb8::Pool;
use bb8_postgres::{tokio_postgres::NoTls, PostgresConnectionManager};
use clap::Parser;
use serde::{Deserialize, Serialize};
use tower_http::trace::TraceLayer;

#[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,
}

#[derive(Clone)]
struct MyAppState {
    dbpool: Pool<PostgresConnectionManager<NoTls>>,
}

#[tokio::main]
async fn main() {
    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();
    let my_state = MyAppState { dbpool: pool };

    tracing_subscriber::fmt::init();

    let app = Router::new()
        .route("/", post(todo_create))
        .route("/", delete(todo_delete))
        .route("/", get(todo_list))
        .with_state(my_state)
        .layer(TraceLayer::new_for_http());

    let listener = tokio::net::TcpListener::bind("127.0.0.1:8889")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

#[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>,
}

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),
                    }),
                )
            }
            Err(e) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ResultWrapper {
                    code: 500,
                    message: e.to_string(),
                    data: None,
                }),
            ),
        },
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ResultWrapper {
                code: 500,
                message: e.to_string(),
                data: None,
            }),
        ),
    }
}

async fn todo_delete(State(pool): State<MyAppState>, Json(id): Json<i32>) -> impl IntoResponse {
    match pool.dbpool.get().await {
        Ok(db) => match db.execute("DELETE FROM todo WHERE id = $1", &[&id]).await {
            Ok(r) => {
                tracing::info!("todo list id {} had been deleted", id);
                (
                    StatusCode::OK,
                    Json(ResultWrapper {
                        code: 0,
                        message: "ok".to_string(),
                        data: Some(r),
                    }),
                )
            }
            Err(e) => (
                StatusCode::BAD_REQUEST,
                Json(ResultWrapper {
                    code: 500,
                    message: e.to_string(),
                    data: None,
                }),
            ),
        },
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ResultWrapper {
                code: 500,
                message: e.to_string(),
                data: None,
            }),
        ),
    }
}

async fn todo_create(
    State(pool): State<MyAppState>,
    Json(input): Json<CreateTodo>,
) -> impl IntoResponse {
    match pool.dbpool.get().await {
        Ok(db) => {
            match db
                .query(
                    "INSERT INTO todo (description, completed) VALUES ($1, FALSE) RETURNING id",
                    &[&input.description],
                )
                .await
            {
                Ok(rows) => {
                    if let Some(row) = rows.get(0) {
                        let id: i32 = row.get(0);
                        (
                            StatusCode::OK,
                            Json(ResultWrapper {
                                code: 0,
                                message: "ok".to_string(),
                                data: Some(id),
                            }),
                        )
                    } else {
                        (
                            StatusCode::INTERNAL_SERVER_ERROR,
                            Json(ResultWrapper {
                                code: 400,
                                message: "no return data".to_string(),
                                data: None,
                            }),
                        )
                    }
                }
                Err(e) => (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    Json(ResultWrapper {
                        code: 500,
                        message: e.to_string(),
                        data: None,
                    }),
                ),
            }
        }
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ResultWrapper {
                code: 500,
                message: e.to_string(),
                data: None,
            }),
        ),
    }
}

依赖的版本为

[dependencies]
axum = "0.7.5"
bb8 = "0.8.5"
bb8-postgres = "0.8.1"
clap = { version = "4.5.13", features = ["derive"] }
serde = { version = "1.0.204", features = ["derive"] }
tokio = { version = "1.39.2", features = ["rt-multi-thread"] }
tower = "0.4.13"
tower-http = { version = "0.5.2", features = ["fs", "trace"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值