使用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. 测试

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

bash 复制代码
cargo new todolist

然后,我们添加依赖,这里我们使用cargo add 添加

bash 复制代码
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

这样的话,就不用添加版本了。

这里我们建一个简单的数据库

sql 复制代码
create database todolist;

create table todo (
	id serial  primary key,
	description varchar(512) not null,
	completed bool not null
);

然后我们正式进入我们的代码部分:

定义postgresql连接,这里我使用了clip库,从命令行传入数据连接参数

rust 复制代码
// 定义传入参数模型
#[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。

rust 复制代码
#[derive(Clone)]
struct MyAppState {
    dbpool: Pool<PostgresConnectionManager<NoTls>>,
}

接下来,我们定义初始化日志模块

rust 复制代码
tracing_subscriber::fmt::init();

一行代码就能搞定。

然后我们定义几个model,注意这里面实现的trait,serde提供的SerializeDeserialize,还有Debug, Clone

rust 复制代码
#[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,第一元素为状态码,第二个为参数。

rust 复制代码
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服务
rust 复制代码
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也可以编写出来。

下面是整体代码

rust 复制代码
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,
            }),
        ),
    }
}

依赖的版本为

toml 复制代码
[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"
相关推荐
uhakadotcom10 分钟前
静态代码检测技术入门:Python 的 Tree-sitter 技术详解与示例教程
后端·面试·github
幂简集成explinks16 分钟前
e签宝签署API更新实战:新增 signType 与 FDA 合规参数配置
后端·设计模式·开源
River41620 分钟前
Javer 学 c++(十三):引用篇
c++·后端
RoyLin28 分钟前
TypeScript设计模式:迭代器模式
javascript·后端·node.js
爱海贼的无处不在1 小时前
一个需求竟然要开14个会:程序员的日常到底有多“会”?
后端·程序员
IT_陈寒2 小时前
Java 性能优化:5个被低估的JVM参数让你的应用吞吐量提升50%
前端·人工智能·后端
南囝coding2 小时前
《独立开发者精选工具》第 018 期
前端·后端
绝无仅有2 小时前
数据库MySQL 面试之死锁与排查经验总结
后端·面试·github
用户384958730693 小时前
Spring Boot 集成 Redis 的完整流程
后端