RUST web框架axum快速入门教程2之响应构造 n
上一篇文章讨论了axum如何获取参数,这一节看看axum是怎么构造响应内容的,如果你还不知道如何处理axum的请求参数,可以阅读我之前的文章: https://youerning.top/post/axum/quickstart-1。
一般来说,现在常见的响应内容有两类,HTML和JSON, 其对应的Content-Type
是text/html
和application/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
的源代码,会发现它所接受的对象只有一个约束,那就是serde
的Serialize
,这是一个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框架的一个很大的不同在于,它是基于tokio
和tower
技术栈的,也就是它可以从这两者那里继承很多的优势,这里很大的优势有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
。