rust:axum+sqlx实战学习笔记1

3,884 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情 

最近工作颇为清(摸)闲(鱼),既然如此,那就开个新坑。学习一下如何在rust进行简单的web后端crud。

首先上主角,当然是看我们需要用到哪些crate库:

tokio = {version = "1.20.0", features = ["full"]}
axum = {version = "0.5.13", features = ["headers"]}
sqlx = { version = "0.6.0", features = [ "runtime-tokio-rustls" ,"mysql","chrono"] }
once_cell = "1.13.0"
serde = {version = "1.0.140", features = ['derive']}
serde_json = "1.0.82"
tracing = "0.1.36"
tracing-subscriber = {version = "0.3.15", features = ["env-filter"]}
dotenv = "0.15.0"
jsonwebtoken = "8.1.1"
chrono = { version = "0.4.19",features = ["serde"] }
  1. Tokio异步运行时,rust目前最流行,最有名气的异步库。
  2. axun,Tokio系旗下的web框架,这次就是要体验一下Tokio全家桶
  3. sqlx 异步数据库框架,并不是orm框架那种,没有DSL,用户自己编写sql语句,将查询结果按列取出或映射到struct上
  4. once_cell初始化全局变量库
  5. serde、serde_json序列化专用库
  6. tracing、tracing-subscriber,日志库
  7. dotenv环境变量库
  8. jsonwebtoken jwt认证库
  9. chrono时间库

在正式开始之前,先看一下axum官方的例子:

use axum::{
    routing::{get, post},
    http::StatusCode,
    response::IntoResponse,
    Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    // initialize tracing
    //启动日志记录
    tracing_subscriber::fmt::init();

    // build our application with a route
    //构建路由
    let app = Router::new()
        // `GET /` goes to `root`
        .route("/", get(root))
        // `POST /users` goes to `create_user`
        .route("/users", post(create_user));

    // run our app with hyper
    // `axum::Server` is a re-export of `hyper::Server`
    //服务端口
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    tracing::debug!("listening on {}", addr);
    //启动服务
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

// basic handler that responds with a static string
//handler函数
async fn root() -> &'static str {
    "Hello, World!"
}

//handler函数
async fn create_user(
    // this argument tells axum to parse the request body
    // as JSON into a `CreateUser` type
    Json(payload): Json<CreateUser>,
) -> impl IntoResponse {
    // insert your application logic here
    let user = User {
        id: 1337,
        username: payload.username,
    };

    // this will be converted into a JSON response
    // with a status code of `201 Created`
    (StatusCode::CREATED, Json(user))
}

// the input to our `create_user` handler
#[derive(Deserialize)]
struct CreateUser {
    username: String,
}

// the output to our `create_user` handler
#[derive(Serialize)]
struct User {
    id: u64,
    username: String,
}

通过官方例子,我们对axum的基本结构有了一个大概的了解。用route函数设置路由,参数为路由路径和handler函数。至于什么是handler函数? 再来看axum文档中对handler的定义:

image.png

全是英文看不懂?其实我也不懂,靠着翻译和自己的理解。用人话来说,handler是一个异步函数用来处理程序逻辑并且返回响应内容。也就是说,我们在handler中处理路由的逻辑并返回内容或者是报错信息。

接下来我们的crud基本就是围绕handler函数的处理了。从create_user函数中,能看到一个完整的handler处理函数。

先看入参,也就是axum从Request请求中获取参数的办法,用axum里的概念叫Extract(提取解析)。

一、从path中提取参数

.route("/user/:id", get(user_info))

// eg: /user/1,将解析出id=1 
async fn user_info(Path(id): Path<i32>) -> String { format!("user id:{}", id) }

通过正则匹配获取路由路径中的值,不仅可以获取单个值,也可以匹配多个值

.route("/person/:id/:age", get(person))

// eg: /person/12/3,将解析出id=12, age=30
async fn person(Path((id, age)): Path<(i32, i32)>) -> String { format!("id:{},age:{}", id, age) }

也可以封装成一个struct进行匹配

.route("/path_req/:a/:b/:c/:d", get(path_req))

#[derive(Deserialize)]
struct SomeRequest{
  a: String, 
  b: i32, 
  c: String,
  d: u32, 
} 

// eg: path_req/a1/b1/c1/d1
async fn path_req(Path(req): Path<SomeRequest>) -> String {
   format!("a:{},b:{},c:{},d:{}", req.a, req.b, req.c, req.d)
}

二、从Url获取参数

.route("/query_req", get(query_req))

#[derive(Deserialize)]
struct SomeRequest{
  a: String, 
  b: i32, 
  c: String,
  d: Option<u32>, 
} 

//eg: query_req/?a=test&b=2&c=abc&d=80
async fn query_req(Query(args): Query<SomeRequest>) -> String { 
  format!("a:{},b:{},c:{},d:{}", args.a, args.b, args.c, args.d) 
}

聪明的小伙伴肯定发现了,怎么这个struct好像不对劲?里面有值是Option<T>?因为如果希望有些参数可为空,可以把相应的字段改成Option。

如果想获取所有的QueryString参数,可以用HashMap进行映射:

.route("/query", get(query))

//eg: query?a=1&b=1.0&c=xxx 
async fn query(Query(params): Query<HashMap<String, String>>) -> String {
  for (key, value) in &params {
   println!("key:{},value:{}", key, value); 
  }
  format!("{:?}", params) 
}

三、从Form表单获取提取参数

.route("/form", post(form_request))

#[derive(Deserialize)] struct SomeRequest2 {
a: Option<String>,
b: Option<i32>,
c: Option<String>,
d: Option<u32>,
}

// 表单提交
async fn form_request(Form(model): Form<SomeRequest2>) -> String { 
  format!( "a:{},b:{},c:{},d:{}",
   model.a.unwrap_or_default(), 
   model.b.unwrap_or(-1), 
   //b缺省值指定为-1 model.c.unwrap_or_default(),
   model.d.unwrap_or_default()) 
}

四、从applicataion/json获取参数

.route("/json", post(json_request))

// json提交 
async fn json_request(Json(model): Json<SomeRequest>) -> String { 
  format!("a:{},b:{},c:{},d:{}", model.a, model.b, model.c, model.d) 
}

五、获取所有请求头

.route("/header", get(get_all_header))

/** * 获取所有请求头 */ 
async fn get_all_header(headers: HeaderMap) -> String { 
 for (key, value) in &headers { 
   println!("key:{:?} , value:{:?}", key, value); 
  } 
 format!("{:?}", headers) }

如果像获取指定请求头参数,可以这样。提取指定header头,比如user-agent

.route("/user_agent", get(get_user_agent_header))

/** * 获取http headers中的user_agent头 */ 
async fn get_user_agent_header(TypedHeader(user_agent): TypedHeader<headers::UserAgent>) -> String { 
  user_agent.to_string() 
}

如何想要获取cookie,可以这样写:

async fn user_center(headers: HeaderMap) -> Result<Html<String>, &'static str> { 
let cookies = headers .get(axum::http::header::COOKIE) 
.and_then(|v| v.to_str().ok()) 
.map(|v| v.to_string()) 
.unwrap_or("".to_string());
// 从请求头获取所有COOKIE
if cookies.is_empty() { 
  return Err("NO COOKIE SETTED"); 
// 没有 Cookie }
let mut logined_username: Option<String> = None; let cookies: Vec<&str> = cookies.split(';').collect(); 
// 多个cookie用;分割 
for cookie in cookies { 
let cookie_pair: Vec<&str> = cookie.split('=').collect(); 
// 每个cookie都是用=分割的键值对
let cookie_name = cookie_pair[0].trim(); let cookie_value = cookie_pair[1].trim();
// 如果 cookie 的名称是我们希望的,并且值不为空 
if cookie_name == COOKIE_NAME && !cookie_value.is_empty() {
  logined_username = Some(String::from(cookie_value)); 
// 设置已登录用户的用户名 break; } 
} if logined_username.is_none() { 
  return Err("COOKIE IS EMPTY"); 
// 没有我们需要的cookie 
}
let html = format!( r#" 
<!DOCTYPE html> <html lang="zh-Hans"> 
<head> <meta charset="utf-8" />
<meta name="author" content="axum.rs (team@axum.rs)" /> 
<title> 用户中心-AXUM中文网 </title> 
</head>
<body> <p>你好,<strong>{}</strong>!你已成功登录。[<a href="/logout">退出登录</a>] </body> </html>
"#,
logined_username.unwrap() ); 
Ok(Html(html)) }

总结:我们对axum的使用方法有了一个基本的了解,主要是通过路由上的handler函数对逻辑进行处理,而handler函数获取路由参数的方法共有path、query、form、json、header请求头等5种。

下一章,我们将对handler函数的返回内容即请求响应内容或者报错信息进行学习。

最后特别鸣谢大佬: 菩提树下的杨过

因为作者太懒,本文中大部分的代码例子都是从大佬的文章中复制粘贴的。