Axum

axum官网地址:https://docs.rs/axum/latest/axum/

从hello world认识axum

开始

#![allow(unused)]
 
use std::net::SocketAddr;
 
use axum::{response::Html, routing::get, Router};
 
#[tokio::main]
async fn main() {
    //1.create router
    let hello_routes = Router::new().route(
        "/hello",
        get(|| async { Html("<strong>Hello World<strong/>") }),
    );
    //2.create server
    let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
    println!("--> LISTEN ON {addr}\n");
 
    //3.run server
    axum::Server::bind(&addr)
        .serve(hello_routes.into_make_service())
        .await
        .unwrap();
}

对于hello_routes.into_make_service()中这个into_make_service函数,文档中是这样描述的:

Convert this router into a [MakeService], that is a [Service](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) whose response is another service.

This is useful when running your application with hyper’s [Server](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html):

理解上面一句话,我们需要理解下面几个概念:

  • Router: 在http请求中,router负责基于http请求路径和请求方法将http请求映射到正确的http handler上面。
  • Hyper: 一个rust中的http库。
  • Service: 在web框架中,service是http handler的抽象。它负责实际上的接受请求并且进行相应。
  • MakeService: 这是一个比service稍微高级一点的抽象,它实际上是创建Service实例的工厂。 每次新的连接被建立的时候,MakeService就会被调用创建一个处理着恶搞连接的请求处理器。

因此这个函数的作用是将Router转换成MakeService,是为了创建一个更有效和更有弹性的处理http connection的方式。

什么?不想每次写完api都要去打开浏览器或者http client进行测试? 这里推荐一个更方便的方式来便利我们的开发:

1、安装依赖:

cargo add --dev httpc_test
cargo install cargo-watch

2、编写测试类:

#![allow(unused)]
 
use anyhow::Result;
 
#[tokio::test]
async fn quick_dev() -> Result<()> {
    let hc = httpc_test::new_client("http://localhost:8000")?;
    hc.do_get("/hello").await?.print().await?;
    Ok(())
}

3、查看结果:

  • 使用cargo-watch来监听文件的变化:
cargo watch -q -c -w src/ -x run

执行上面的命令,cargo会以watch的形式监听src下面文件的变化,实现热更新,无需重新编译项目。

  • 使用cargo-watch来监听测试类:
cargo watch -q -c -w tests/ -x "test -q quick_dev" -- --nocapture

--nocapture是指不对标准输出进行捕获,比如println!

这样,每次当我们对文件进行修改并保存后,cargo watch都会帮我们进行热更新。

自定义handler

你有没有注意到,现在我们是以闭包作为我们请求的handler,这在处理简单的场景下是十分方便的,但是当情况复杂起来了,闭包函数就不太能适用了。

我们定义如下的hello_handler

async fn hello_handler() -> impl IntoResponse {
    println!("--> REQUEST: /hello");
    Html("<h1>Hello, World!</h1>")
}

这里的返回值一般是你或者项目的构建者创建的统一返回结果CommonResult

请求参数获取

我们需要安装serde依赖来帮助我们处理请求中的参数:

cargo add serde --features derive
  • 处理/hello?name=blkcor
#[derive(Debug, Deserialize)]
struct HelloParams {
    name: Option<String>,
}
 
async fn hello_handler(params: <HelloParams>) -> String {
    println!("--> REQUEST: /hello --> {:?}", params);
    let name = params.name.as_deref().unwrap_or("world");
    format!("Hello, {}!", name)
}
  • 处理/hello/blkcor
//路由如下:route("/hello2/:name", get(hello_handler2));
async fn hello_handler2(Path(name): Path<String>) -> String {
    println!("--> REQUEST: /hello2 -->");
    format!("Hello, {}!", name)
}

自定义router

将我们的hello_router提取出来成一个函数:

fn routes_hello() -> Router {
    Router::new()
        .route("/hello", get(hello_handler))
        .route("/hello2/:name", get(hello_handler2))
}

将所有的分支路由合并到根路由上:

...
let routes_all = Router::new().merge(routes_hello());
...

静态文件服务

我们需要安装tower-http帮助我们实现静态服务器的功能:

cargo add tower-http --features fs

编写静态文件路由:

fn routes_static() -> Router {
    Router::new().nest_service("/", get_service(ServeDir::new("./")))
}

注册路由:

let routes_all = Router::new()
				...
        .fallback_service(routes_static());

这里的fallback_service是对前面路由全部匹配失败后的兜底操作,我们当然也可以定义我们自己的静态资源处理路径,比如static

编写API

一般在编写项目的业务代码之前,我们需要先编写通用的方法或者对象。

创建error模块编写通用错误处理

创建/error/main.rs文件:

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
};
 
pub type Result<T> = core::result::Result<T, Error>;
 
#[derive(Debug)]
pub enum Error {
    LoginFail,
}
 
impl IntoResponse for Error {
    fn into_response(self) -> Response {
        println!("->> {:<12} - {self:?}", "INTO_RES");
 
        (StatusCode::INTERNAL_SERVER_ERROR, "UNHANDED_CLIENT_ERROR").into_response()
    }
}

main.rs中引入mod:

pub use self::error::{Error, Result};
mod error;

创建web模块编写api

新建/web/mod.rs文件:这个文件是web目录下其他mod的入口:

pub mod routes_login;

routes_login这个模块中实现具体的登录逻辑:

use axum::{routing::post, Json, Router};
use serde::Deserialize;
use serde_json::{json, Value};
 
pub use crate::error::{Error, Result};
 
pub fn routes_login() -> Router {
    Router::new().route("/api/login", post(api_login))
}
 
#[derive(Debug, Deserialize)]
struct LoginPayload {
    username: String,
    pwd: String,
}
 
async fn api_login(payload: Json<LoginPayload>) -> Result<Json<Value>> {
    println!("->> {:<12} - api_login", "HANDLER");
 
    //TODO: Implement real db/auth logic.
    if (payload.username != "blkcor" || payload.pwd != "123456") {
        return Err(Error::LoginFail);
    }
 
    //Set cookie.
 
    //Create a successful body
    let body = Json(json!(
      {
        "result":{
          "success":true
        }
      }
    ));
 
    Ok(body)
}

main.rs中引入web这个mod并且注册路由:

...
mod web;
 
#[tokio::main]
async fn main() {
    //1.create router
    let routes_all = Router::new().merge(web::routes_login::routes_login());
 ... 
}  

分别测试一下成功和失败的结果:

...
hc.do_post(
        "/api/login",
        json!({
            "username":"blkcor",
            "pwd":"123456"
        }),
    )
    .await?
    .print()
    .await?;
...

![CleanShot 2023-12-06 at 13.31.31](/Users/chenzilong/Library/Application Support/CleanShot/media/media_7VMBtLTs4G/CleanShot 2023-12-06 at 13.31.31.png)

  ...	
hc.do_post(
        "/api/login",
        json!({
            "username":"blkcor",
            "pwd":"12345678"
        }),
    )
    .await?
    .print()
    .await?;
...

![CleanShot 2023-12-06 at 13.32.25](/Users/chenzilong/Library/Application Support/CleanShot/media/media_DmEiWfA7ZK/CleanShot 2023-12-06 at 13.32.25.png)

Ok,非常棒,错误码和反回信息都打印出来了!

创建响应对象映射

我们需要特殊的一层来拦截响应并做一些最后需要进行的工作:(比如添加cookie)。

官方的描述是这样的:Create a middleware from an async function that transforms a response.就是对我们的response进行一定的转换。

  • 安装tower-cookies
cargo add tower-cookies
  • 补充login中的添加cookie逻辑
async fn api_login(cookies: Cookies, payload: Json<LoginPayload>) -> Result<Json<Value>> {
    println!("->> {:<12} - api_login", "HANDLER");
 
    //TODO: Implement real db/auth logic.
    if (payload.username != "blkcor" || payload.pwd != "123456") {
        return Err(Error::LoginFail);
    }
 
    //Set cookie.
    cookies.add(Cookie::new(web::AUTH_TOKEN, "user-1.exp.sign"));
    //Create a successful body
    let body = Json(json!(
      {
        "result":{
          "success":true
        }
      }
    ));
 
    Ok(body)
}
  • 注意,我们需要添加CookieManagerLayer才能在使用cookie
 let routes_all = Router::new()
        ...
        .layer(CookieManagerLayer::new());

接下来编写我们的layer并使用:

async fn main_response_mapper(res: Response) -> Response {
    println!("->> {:<12} - main_response_mapper", "RES_MAPPER");
 
    //set cookie
 
    res
}
 
 
let routes_all = Router::new()
        ...
        .layer(map_response(main_response_mapper))
				...

里面的逻辑将在之后完成。

编写model

创建model.rs