RUST web框架axum快速入门教程2之响应构造 n

文章目录

上一篇文章讨论了axum如何获取参数,这一节看看axum是怎么构造响应内容的,如果你还不知道如何处理axum的请求参数,可以阅读我之前的文章: https://youerning.top/post/axum/quickstart-1

一般来说,现在常见的响应内容有两类,HTML和JSON, 其对应的Content-Typetext/htmlapplication/json,前者是直接渲染前端页面,一般会配合一些模板引擎的库使用,比如askama, 后者主要是做接口开发,这样就能一套后端各种前端使用了。

本文用到的依赖如下:

[dependencies]
axum = { version="0.6", features=["default", "headers"] }
axum-extra = { version = "0.8" }
tokio = { version = "1.0", features = ["full"] }
reqwest = { version="0.11.22", features=["json", "multipart"]}
serde = { version = "1.0", features = ["derive"] }
tower = { version = "0.4", features = ["util"] }
tower-http = { version = "0.4.0", features = ["fs", "trace"] }
serde_json = "1.0.107"
askama = "0.12"

HTML

首先看看HTML的响应,下面是比较常见的用法。

use axum::{
    response::Html,
    routing::get, Router,
};


#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/html1", get(handler1))
        .route("/html2", get(handler2))
        .route("/html3", get(handler3));

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}

async fn handler1() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}

async fn handler2() -> Html<String> {
    Html(String::from("<h1>Hello, World!</h1>"))
}

async fn handler3() -> Html<&'static str> {
    Html(include_str!("templates/index.html"))
}

如果只是返回静态的内容,那就太无趣了,所以一般会结合模板引擎使用。

use axum::{
    routing::get, Router,
    extract::Path,
    response::{Html, IntoResponse, Response},
    http::StatusCode
};
use askama::Template;


#[derive(Template, Default)]
#[template(path = "hello.html")]
struct HelloTemplate {
    name: String,
}

struct TemplateRespone<T>(T);
impl<T> IntoResponse for TemplateRespone<T>
where
    T: Template,
{
    fn into_response(self) -> Response {
        match self.0.render() {
            Ok(html) => Html(html).into_response(),
            Err(err) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Failed to render template. Error: {err}"),
            )
                .into_response(),
        }
    }
}


#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/:name", get(handler));

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}


async fn handler(Path(name): Path<String>) ->  impl IntoResponse {
    let tpl = HelloTemplate{name};
    TemplateRespone(tpl)
}

值得注意的是: askama默认模板在当前目录的templates目录下,所以需要指定templates/目录前缀。

hello.html的内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    hello {{name}}
</body>
</html>

使用curl的请求结果如下:

$ curl  http://127.0.0.1:8080/youerning.top
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    hello youerning.top
</body>
</html>

askama的模板语法和Jinja2的语法几乎一致,不过也会有些不同,这是由于其实现的语言的特性决定的,详细的内容可以查看: https://djc.github.io/askama/template_syntax.html

JSON

下面是一些常用的代码

use axum::{
    extract::Path,
    routing::get,
    response::{Json, IntoResponse},
    Router,
};
use serde::Serialize;
use serde_json::{Value, json};


#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/json1", get(handler1))
        .route("/json2/:name", get(handler2))
        .route("/json3/:name", get(handler3))
        .route("/json4/:name", get(handler4));

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

#[derive(Serialize, Debug)]
struct SuccessResp {
    status: String,
    message: String
}

async fn handler1() -> Json<Value> {
    Json(json!({
        "status": "ok", 
        "message": "success"
        })
    )
}

async fn handler2(Path(name): Path<String>) -> Json<SuccessResp> {
    Json(SuccessResp{
        status: "ok".into(),
        message: format!("your name is {name}"),
    })
}

async fn handler3(Path(name): Path<String>) -> Json<Value> {
    Json(serde_json::to_value(&SuccessResp{
        status: "ok".into(),
        message: format!("your name is {name}"),
    }).unwrap())
}

async fn handler4(Path(name): Path<String>) -> impl IntoResponse {
    Json(SuccessResp{
        status: "ok".into(),
        message: format!("your name is {name}"),
    })
}

json3到json4的结果是一样的,这里简单的展示一下对应的请求和响应

$ curl   http://127.0.0.1:8080/json4/youerning.top
{"status":"ok","message":"your name is youerning.top"}

如果看一下Json的源代码,会发现它所接受的对象只有一个约束,那就是serdeSerialize,这是一个trait,它代表可序列化, 至于怎么序列化, Json这个对象会负责处理,这里简单的贴一下对应的源代码。

impl<T> IntoResponse for Json<T>
where
	// 类型约束
    T: Serialize,
{
    fn into_response(self) -> Response {
        // Use a small initial capacity of 128 bytes like serde_json::to_vec
        // https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189
        let mut buf = BytesMut::with_capacity(128).writer();
        match serde_json::to_writer(&mut buf, &self.0) {
            Ok(()) => (
                [(
                    header::CONTENT_TYPE,
                    HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
                )],
                buf.into_inner().freeze(),
            )
                .into_response(),
            Err(err) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                [(
                    header::CONTENT_TYPE,
                    HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
                )],
                err.to_string(),
            )
                .into_response(),
        }
    }
}

所以一些数据库的model也可以通过#[derive(Serialize)]来实现Serialize, 这样就可以很方便的将查询到的数据结果返回给前端。

状态码

至此,我们已经能够处理大部分的响应了,但是有一个问题一直没有解决,那就是怎么指定状态码,我们不可能总是将响应码设置成200吧, 默认情况下,如果只返回一个实现IntoResponse trait的对象,状态码都是200。

axum当然会考虑到这种情况,所以我们可以返回一个元组而非返回单个对象,元组的第一个对象是状态码,下面是一个简单的例子。


use axum::{
    response::{Html, IntoResponse},
    routing::get, Router,
    http::StatusCode
};


#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(handler_200))
        .route("/400", get(handler_400))
        .route("/404", get(handler_404))
        .route("/500", get(handler_500));

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}

async fn handler_200() -> Html<&'static str> {
    Html("<h1>OK</h1>")
}

async fn handler_400() -> impl IntoResponse {
    (StatusCode::BAD_REQUEST, Html("bad request"))
}

async fn handler_404() ->  impl IntoResponse {
    (StatusCode::NOT_FOUND, Html("not found"))
}

async fn handler_500() ->  impl IntoResponse {
    (StatusCode::INTERNAL_SERVER_ERROR, Html("internal server error"))
}

这个例子应该比较简单,就不展示对应的请求和响应了。

静态文件

至此,我们已经解决了web开发中的多数响应相关的问题了,那么怎么提供静态文件呢? 比如响应CSS, JS等静态资源文件,虽然rust有一个强大的include_str宏,但是这么一个功能自己写起来还是很无趣的,所以axum应该有相关支持,或者说大多数web框架都是支持的,不过axum支持静态文件的方式和其他rust web框架不太一样。

axum和其他rust web框架的一个很大的不同在于,它是基于tokiotower技术栈的,也就是它可以从这两者那里继承很多的优势,这里很大的优势有tower的中间件服务,这些中间件包括但不限于超时,重连,重试等中间件服务。

tower是一个模块化和可复用的库,提供了一套很棒的请求-响应模型,可以用来开发健壮的网络客户端和服务端。


use axum::{
    response::Html,
    routing::get, Router,
};
use tower_http::services::ServeDir;

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(handler))
        .nest_service("/templates", ServeDir::new("templates"));

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}

async fn handler() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}

上面的例子就是将本地的templates目录映射到服务端的/templates。

小结

除了本文提到的响应,axum其实还支持很多常用的响应类型,比如Redirect, SSE等,这应该能够满足大部分的需求了,如果不行的话,可以自己实现IntoResponse

参考链接