宣布 axum 0.6.0

2022 年 11 月 25 日

早在八月份,我们宣布了 axum 0.6.0-rc.1,今天我很高兴地宣布预发布期已结束,axum 0.6.0 正式发布!

axum 是一个符合人体工程学且模块化的 Web 框架,使用 tokiotowerhyper 构建。

本次发布还包括 axum-coreaxum-extraaxum-macros 的新的主要版本。

如果您已经阅读过 rc.1 公告,那么其中一些内容您会感到熟悉。然而,API 的许多细节都经过微调,以便更易于使用和更灵活。

类型安全的 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 {};

let app = Router::new()
    .route("/", get(handler))
    // Provide the state for the router
    .with_state(state);

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 也支持提取“子状态”

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

// Our top level state that contains an `HttpClient` and a `Database`
//
// `#[derive(FromRef)]` makes them sub states so they can be extracted
// independently
#[derive(Clone, FromRef)]
struct AppState {
    client: HttpClient,
    database: Database,
}

#[derive(Clone)]
struct HttpClient {}

#[derive(Clone)]
struct Database {}

let state = AppState {
    client: HttpClient {},
    database: Database {},
};

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

async fn handler(
    // We can extract both `State<HttpClient>` and `State<Database>`
    State(client): State<HttpClient>,
    State(database): State<Database>,
) {}

也可以在合并和嵌套的子路由器上使用不同的状态类型

let app = Router::new()
    // A route on the outermost router that requires `OuterState` as the
    // state
    .route("/", get(|_: State<OuterState>| { ... }))
    // Nest a router under `/api` that requires an `ApiState`
    //
    // We have to provide the state when nesting it into another router
    // since it uses a different state type
    .nest("/api", api_router().with_state(ApiState {}))
    // Same goes for routers we merge
    .merge(some_other_routes().with_state(SomeOtherState {}))
    // Finally provide the `OuterState` needed by the first route we
    // added
    .with_state(OuterState {});

// We don't need to provide the state when constructing the sub routers
//
// We only need to do that when putting everything together. That means
// we don't need to pass the different states around to each function
// that builds a sub router
fn api_router() -> Router<ApiState> {
    Router::new()
        .route("/users", get(|_: State<ApiState>| { ... }))
}

fn some_other_state() -> Router<SomeOtherState> {
    Router::new()
        .route("/foo", get(|_: State<SomeOtherState>| { ... }))
}

#[derive(Clone)]
struct ApiState {};

#[derive(Clone)]
struct SomeOtherState {};

#[derive(Clone)]
struct OuterState {};

虽然 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 来完成的。

这也意味着,如果您有不需要请求体的 FromRequest 实现,那么您应该改为实现 FromRequestParts

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));

还有新的 map_requestmap_response 中间件函数,其工作方式类似于 middleware::from_fn,以及支持提取 Statefrom_fn_with_state、[map_request_with_state,] 和 map_response_with_state 版本。

嵌套路由器的回退继承

axum 0.5 中,嵌套路由器不允许有回退,并且会导致 panic

let api_router = Router::new()
    .route("/users", get(|| { ... }))
    .fallback(api_fallback);

let app = Router::new()
    // this would panic since `api_router` has a fallback
    .nest("/api", api_router);

然而,在 0.6 版本中,这现在可以正常工作了,以 /api 开头但与 api_router 不匹配的请求将转到 api_fallback

如果嵌套路由器没有自己的回退,则外部路由器的回退仍然适用

// This time without a fallback
let api_router = Router::new().route("/users", get(|| { ... }));

let app = Router::new()
    .nest("/api", api_router)
    // `api_fallback` will inherit this fallback
    .fallback(app_fallback);

因此,通常您只需在需要的地方放置回退,axum 就会做正确的事情!

WebAssembly 支持

axum 现在支持通过禁用 tokio 功能编译为 WebAssembly

axum = { version = "0.6", default-features = false }

default-features = false 将禁用 tokio 功能,该功能是默认功能之一。

这将禁用 tokiohyperaxum 中不支持 WebAssembly 的部分。

删除了尾部斜杠重定向

以前,如果您有 /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() {}

如果您想选择使用旧的行为,可以使用来自 axum-extraRouterExt::route_with_tsr

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

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

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 的提示。

此外,如果您在更新时遇到问题或发现错误,请在 Discord 中提问或 提交 issue

— David Pedersen (@davidpdrsn)