axum 0.5 版本的新特性

2022 年 3 月 31 日

今天,我们很高兴宣布 axum 0.5 版本。axum 是一个符合人体工程学和模块化的 Web 框架,基于 tokiotowerhyper 构建。

0.5 版本包含许多新功能,我想在此重点介绍其中的一些。

这也包括 axum-coreaxum-extraaxum-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 频道中找到我们。

— David Pedersen (@davidpdrsn)