axum 0.6.0-rc.1 的新特性
2022年8月23日
今天,我们很高兴地宣布 axum
0.6.0-rc.1 版本发布。axum
是一个符合人体工程学且模块化的 Web 框架,它基于 tokio
、tower
和 hyper
构建。
在 0.6 版本中,我们重构了 axum 的一些基础部分,使其更类型安全、更不易产生意外,且更易于使用。在这篇文章中,我想重点介绍一些最具影响力的更改和新功能。
这也包括 axum-core
、axum-extra
和 axum-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 版本。