axum 0.5 版本的新特性
2022 年 3 月 31 日
今天,我们很高兴宣布 axum
0.5 版本。axum
是一个符合人体工程学和模块化的 Web 框架,基于 tokio
、tower
和 hyper
构建。
0.5 版本包含许多新功能,我想在此重点介绍其中的一些。
这也包括 axum-core
、axum-extra
和 axum-macros
的新主要版本。
新的 IntoResponseParts
特性
axum
一直支持通过组合各个部分来构建响应
use axum::{
Json,
response::IntoResponse,
http::{StatusCode, HeaderMap},
};
use serde_json::json;
// returns a JSON response
async fn json() -> impl IntoResponse {
Json(json!({ ... }))
}
// returns a JSON response with a `201 Created` status code and
// a custom header
async fn json_with_status_and_header() -> impl IntoResponse {
let mut headers = HeaderMap::new();
headers.insert("x-foo", "custom".parse().unwrap());
(StatusCode::CREATED, headers, Json(json!({})))
}
但是,您无法轻松提供自己的自定义响应部分。axum
必须专门允许将 HeaderMap
包含在响应中,并且您无法使用自己的类型扩展此系统。
新的 IntoResponseParts
特性解决了这个问题!
例如,我们可以添加我们自己的 SetHeader
类型来设置单个标头,并为其实现 IntoResponseParts
。
use axum::{
response::{ResponseParts, IntoResponseParts},
http::{StatusCode, header::{HeaderName, HeaderValue}},
};
struct SetHeader<'a>(&'a str, &'a str);
impl<'a> IntoResponseParts for SetHeader<'a> {
type Error = StatusCode;
fn into_response_parts(
self,
mut res: ResponseParts,
) -> Result<ResponseParts, Self::Error> {
let name = self.0.parse::<HeaderName>()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let value = self.1.parse::<HeaderValue>()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
res.headers_mut().insert(name, value);
Ok(res)
}
}
我们现在可以在响应中使用 SetHeader
use axum::{Json, response::IntoResponse, http::StatusCode};
use serde_json::json;
async fn json_with_status_and_header() -> impl IntoResponse {
(
StatusCode::CREATED,
SetHeader("x-foo", "custom"),
SetHeader("x-bar", "another custom header"),
Json(json!({})),
)
}
IntoResponseParts
也为 Extension
实现了,使其易于设置响应扩展。例如,这可以用于与中间件共享状态
use axum::{Extension, Json, response::IntoResponse};
use serde_json::json;
async fn json_extensions() -> impl IntoResponse {
(
Extension(some_value),
Extension(some_other_value),
Json(json!({})),
)
}
如果包含状态码,则它必须是元组的第一个元素,并且任何响应主体都必须是最后一个。这确保您只设置这些部分一次,并且不会意外覆盖它们。
有关更多详细信息,请参阅 axum::response
。
Cookies
在 IntoResponseParts
的基础上,axum-extra
有一个新的 CookieJar
提取器
use axum_extra::extract::cookie::{CookieJar, Cookie};
use axum::response::IntoResponseParts;
async fn handler(jar: CookieJar) -> impl IntoResponse {
if let Some(cookie_value) = jar.get("some-cookie") {
tracing::info!(?cookie_value);
}
let updated_jar = jar
.add(Cookie::new("session_id", "value"))
.remove(Cookie::named("some-cookie"));
(updated_jar, "response body...")
}
它还带有一个 SignedCookieJar
变体,它将使用密钥对 cookie 进行签名,因此您可以确保没有人篡改它们。
IntoResponseParts
使这一切成为可能,而无需任何中间件。
有关更多详细信息,请参阅 axum_extra::extract::cookie
。
HeaderMap
提取器
您一直可以使用 HeaderMap
作为提取器来访问请求中的标头。但是您可能没有意识到的是,这将隐式地消耗标头,从而使其他提取器无法访问它们。
例如,这微妙地破坏了
use axum::{http::HeaderMap, extract::Form};
async fn handler(
headers: HeaderMap,
form: Form<Payload>,
) {
// ...
}
由于我们首先运行 HeaderMap
,因此 Form
将无法访问它们,并将以 500 Internal Server Error
失败。这非常令人惊讶,并给一些用户带来了麻烦。
但是,在 axum
0.5 中,这个问题消失了,它可以正常工作了!
更灵活的 Router::merge
Router::merge
可用于将两个路由器合并为一个。在 axum 0.5 中,它变得稍微灵活了一些,现在接受任何 impl Into<Router>
。这允许您拥有构建 Router
的自定义方法,并使它们与 axum
无缝协作。
人们可以想象一种像这样组合 REST 和 gRPC 的方法
let rest_routes = Router::new().route(...);
// with `impl From<GrpcService> for Router`
let grpc_service = GrpcService::new(GrpcServiceImpl::new());
let app = Router::new()
.merge(rest_routes)
.merge(grpc_service);
荣誉提名
以下功能不是 0.5 版本的新功能,但最近已发布,值得一提。
middleware::from_fn
axum
使用 tower::Service
特性作为中间件。但是,实现它可能有点令人生畏,主要是由于 Rust 中缺少异步特性。
但是,借助 axum::middleware::from_fn
,您可以隐藏所有这些复杂性并使用熟悉的异步函数
use axum::{
Router,
http::{Request, StatusCode},
routing::get,
response::IntoResponse,
middleware::{self, Next},
};
async fn my_middleware<B>(
req: Request<B>,
next: Next<B>,
) -> impl IntoResponse {
// transform the request...
let response = next.run(request).await;
// transform the response...
response
}
let app = Router::new()
.route("/", get(|| async { /* ... */ }))
// add our middleware function
.layer(middleware::from_fn(my_middleware));
中间件文档 也经过了重新设计,并更详细地介绍了编写中间件的不同方法、何时选择哪种方法、排序如何工作等等。
类型安全的路由
在 axum-extra
中,我们正在试验“类型安全的路由”。这个想法是在路径和相应的处理程序之间建立类型安全的连接。
以前,可以添加像 /users
这样的路径并应用 Path<u32>
提取器,这总是会在运行时失败,因为该路径不包含任何参数。
我们可以使用 axum-extra
的类型安全路由在编译时防止该问题
use serde::Deserialize;
use axum::Router;
use axum_extra::routing::{
TypedPath,
RouterExt, // for `Router::typed_get`
};
// A type-safe path
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
struct UsersMember {
id: u32,
}
// A regular handler function that takes `UsersMember` as the
// first argument and thus creates a typed connection between
// this handler and the `/users/:id` path.
async fn users_show(path: UsersMember) {
tracing::info!(?path.id, "users#show called!");
}
let app = Router::new()
// Add our typed route to the router.
//
// The path will be inferred to `/users/:id` since `users_show`'s
// first argument is `UsersMember` which implements `TypedPath`
.typed_get(users_show);
这里的关键是我们的 users_show
函数没有任何宏,因此 IDE 集成继续工作良好。
有关更多详细信息,请参阅 axum_extra::routing::TypedPath
。
更新
axum
0.5 版本也包含一些重大更改,但我想说它们都相当小。如果您在升级时遇到问题或有一般性问题,请随时联系我们!您可以在 Tokio Discord 服务器 的 #axum
频道中找到我们。