使用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"
相关推荐
m0_748230948 分钟前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
奶香臭豆腐12 分钟前
C++ —— 模板类具体化
开发语言·c++·学习
不爱学英文的码字机器18 分钟前
[Linux] Shell 命令及运行原理
linux·运维·服务器
晚夜微雨问海棠呀19 分钟前
长沙景区数据分析项目实现
开发语言·python·信息可视化
graceyun20 分钟前
C语言初阶习题【9】数9的个数
c语言·开发语言
cdut_suye29 分钟前
Linux工具使用指南:从apt管理、gcc编译到makefile构建与gdb调试
java·linux·运维·服务器·c++·人工智能·python
qq_4336184434 分钟前
shell 编程(三)
linux·运维·服务器
小蜗牛慢慢爬行43 分钟前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
波音彬要多做1 小时前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法
Swift社区1 小时前
Excel 列名称转换问题 Swift 解答
开发语言·excel·swift