axum 0.6.0-rc.1 的新特性

2022年8月23日

今天,我们很高兴地宣布 axum 0.6.0-rc.1 版本发布。axum 是一个符合人体工程学且模块化的 Web 框架,它基于 tokiotowerhyper 构建。

在 0.6 版本中,我们重构了 axum 的一些基础部分,使其更类型安全、更不易产生意外,且更易于使用。在这篇文章中,我想重点介绍一些最具影响力的更改和新功能。

这也包括 axum-coreaxum-extraaxum-macros 的新主要版本。

类型安全的 State 提取器

之前,与处理程序共享状态的推荐方法是使用 Extension 中间件和提取器

use axum::{
    Router,
    Extension,
    routing::get,
};

#[derive(Clone)]
struct AppState {}

let state = AppState {};

let app = Router::new()
    .route("/", get(handler))
    // Add `Extension` as a middleware
    .layer(Extension(state));

async fn handler(
    // And extract our shared `AppState` using `Extension`
    Extension(state): Extension<AppState>,
) {}

然而,这并非类型安全,因此如果您忘记了 .layer(Extension(...)),编译会正常进行,但在调用 handler 时会出现运行时错误。

在 0.6 版本中,您可以使用新的 State 提取器,它的工作方式与 Extension 类似,但类型安全

use axum::{
    Router,
    extract::State,
    routing::get,
};

#[derive(Clone)]
struct AppState {}

let state = AppState {};

// Create the `Router` using `with_state`
let app = Router::with_state(state)
    .route("/", get(handler));

async fn handler(
    // And extract our shared `AppState` using `State`
    //
    // This will only compile if the type passed to `Router::with_state`
    // matches what we're extracting here
    State(state): State<AppState>,
) {}

State 也支持提取“子状态”。有关更多详细信息,请参阅 文档

虽然 Extension 仍然有效,但我们建议用户迁移到 State,不仅因为它更类型安全,而且因为它速度更快。

类型安全的提取器排序

延续类型安全的主题,axum 现在强制执行只有一个提取器可以消耗请求体。在 0.5 版本中,这可以正常编译,但在运行时会失败

use axum::{
    Router,
    Json,
    routing::post,
    body::Body,
    http::Request,
};

let app = Router::new()
    .route("/", post(handler).get(other_handler));

async fn handler(
    // This would consume the request body
    json_body: Json<serde_json::Value>,
    // This would also attempt to consume the body but fail
    // since it is gone
    request: Request<Body>,
) {}

async fn other_handler(
    request: Request<Body>,
    // This would also fail at runtime, even if the `AppState` extension
    // was set since `Request<Body>` consumes all extensions
    state: Extension<AppState>,
) {}

解决方案是手动确保您只使用一个消耗请求的提取器,并且它是最后一个提取器。axum 0.6 现在在编译时强制执行这一点

use axum::{
    Router,
    Json,
    routing::post,
    body::Body,
    http::Request,
};

let app = Router::new()
    .route("/", post(handler).get(other_handler));

async fn handler(
    // We cannot extract both `Request` and `Json`, have to pick one
    json_body: Json<serde_json::Value>,
) {}

async fn other_handler(
    state: Extension<AppState>,
    // `Request` must be extracted last
    request: Request<Body>,
) {}

这是通过重构 FromRequest trait 并添加新的 FromRequestParts trait 来完成的。

middleware::from_fn 运行提取器

middleware::from_fn 使使用熟悉的 async/await 语法编写中间件变得容易。在 0.6 版本中,此类中间件也可以运行提取器

use axum::{
    Router,
    middleware::{self, Next},
    response::{Response, IntoResponse},
    http::Request,
    routing::get,
};
use axum_extra::extract::cookie::{CookieJar, Cookie};

async fn my_middleware<B>(
    // Run the `CookieJar` extractor as part of this middleware
    cookie_jar: CookieJar,
    request: Request<B>,
    next: Next<B>,
) -> Response {
    let response = next.run(request).await;

    // Add a cookie to the jar
    let updated_cookie_jar = cookie_jar.add(Cookie::new("foo", "bar"));

    // Add the new cookies to the response
    (updated_cookie_jar, response).into_response()
}

let app = Router::new()
    .route("/", get(|| async { "Hello, World!" }))
    .layer(middleware::from_fn(my_middleware));

请注意,这不能用于提取 State。有关更多详细信息,请参阅 文档

带有回退的嵌套路由器

Router::nest 允许您将所有具有匹配前缀的请求发送到其他路由器或服务。然而,在 0.5 版本中,嵌套路由器不可能拥有自己的回退。在 0.6 版本中,这现在可以工作了

use axum::{Router, Json, http::StatusCode, routing::get};
use serde_json::{Value, json};

let api = Router::new()
    .route("/users/:id", get(|| async {}))
    // We'd like our API fallback to return JSON
    .fallback(api_fallback);

let app = Router::new()
    .nest("/api", api)
    // And our top level fallback to return plain text
    .fallback(top_level_fallback);

async fn api_fallback() -> (StatusCode, Json<Value>) {
    let body = json!({
        "status": 404,
        "message": "Not Found",
    });
    (StatusCode::NOT_FOUND, Json(body))
}

async fn top_level_fallback() -> (StatusCode, &'static str) {
    (StatusCode::NOT_FOUND, "Not Found")
}

删除了尾部斜杠重定向

以前,如果您有一个 /foo 的路由,但收到了一个带有 /foo/ 的请求,axum 会发送一个重定向响应到 /foo。然而,许多人发现这种行为令人惊讶,并且当与执行自身重定向的服务结合使用时,会出现边缘情况错误,因此在 0.6 版本中,我们决定删除此功能。

推荐的解决方案是显式添加您想要的路由

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

let app = Router::new()
    // Send `/foo` and `/foo/` to the same handler
    .route("/foo", get(foo))
    .route("/foo/", get(foo));

async fn foo() {}

如果您想选择使用旧的行为,可以使用 RouterExt::route_with_tsr

混合通配符路由和常规路由

axum 的 Router 现在更好地支持混合通配符路由和常规路由

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

let app = Router::new()
    // In 0.5 these routes would be considered overlapping and not be allowed
    // but in 0.6 it just works
    .route("/foo/*rest", get(|| async {}))
    .route("/foo/bar", get(|| async {}));

请参阅更新日志了解更多信息

我鼓励您阅读 更新日志,以查看所有更改以及有关如何从 0.5 更新到 0.6.0-rc.1 的提示。

此外,如果您在更新时遇到问题或发现错误,请在 Discord 中提问或 提交 issue。如果一切顺利,我们预计将在几周内发布 0.6.0 版本。

— David Pedersen (@davidpdrsn)