上一篇文章,我简要概述了 Axum 框架的基本组成。这篇文章,我将详细展开讲讲响应、路由和中间件。
在 Axum 中,处理器的返回值会被自动转换成 http::response::Response。这个转换过程由 IntoResponse 特征实现。所以只有实现 IntoResponse 的类型才能作为处理器的返回值。
一些常见类型已经实现了 IntoResponse,比如 String、&'static str、Vec<u8>、&'static [u8]、StatusCode、() 等等。因此它们可以直接从处理器返回。
Result<T, E> 也实现了 IntoResponse,只要 T 和 E 同时实现 IntoResponse 就行。
use axum::{
Router,
body::{self, Bytes},
routing::get,
http::StatusCode,
response::{IntoResponse, Response},
};
enum MyError {
SomethingWentWrong,
SomethingElseWentWrong,
}
impl IntoResponse for MyError {
fn into_response(self) -> Response {
let body = match self {
MyError::SomethingWentWrong => "something went wrong",
MyError::SomethingElseWentWrong => "something else went wrong",
};
// Axum 提供了 `(StatusCode, R: IntoResponse)` 的实现
(StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
}
}
// 创建路由
let app = Router::new().route("/", get(handler));
// 一个处理器
async fn handler() -> Result<(), MyError> {
Err(MyError::SomethingWentWrong)
}
关于响应还有一个重要的特征:IntoResponseParts——它负责为响应添加响应头和一些自定义扩展数据。
可以将一个或多个 IntoResponseParts 类型连续放入元组中,并且只要元组的末位元素是 IntoResponse 的(首位元素可以是 IntoResponseParts 的类型,也可以是 StatusCode 等类型),这个元组就能在处理器中返回。
如果你想追求更高的灵活性,也可以手动构造 http::response::Response。
路由(routing)是分发请求的中枢。所有发往服务器的请求都要先过路由这一关,它负责将请求按照其 URL 路径和请求方法进行匹配,匹配成功就让对应的中间件(如果有的话)和处理器去处理,否则就返回 404 Not Found。
Axum 中,路由由 Router 实现。
route 方法注册路由一个 Router 能使用 .route 方法注册多个路由,这个方法接收一条 &str 路径和一个 MethodRouter 实例。
路径可以是静态的(例如 /、/foo、/users/123),也可以是捕获性的,还可以是通配符。
捕获性的路径需使用花括号包裹路径段,它可以将对应的路径参数捕获为一个同名变量,后续可以使用提取器在处理器中使用这个变量。
比如 /{key} 会匹配像 /mykey、/1234 这样的路径,并将字符串 mykey、1234 存储在变量 key 当中;同理,/users/{id}/tweets 会匹配路径 /users/1104/tweets 并将 1104 存储在变量 id 当中。
通配符路径只需要在捕获性路径的捕获变量前加 * 即可,它会匹配剩余所有路径段,但不会匹配空路径段。相比捕获性路径,它只能出现在路径末尾;它和前者一样有捕获功能。
比如 /{*key} 会匹配 /a、/a/ 等路径,但不匹配 /;/assets/{*path} 不匹配 /assets/,但匹配 /assets/styles.css、/assets/image/img.jpg 等。
MethodRouter 实例通常是借助方法路由函数构造的。
方法路由是一系列在 axum::routing::method_routing 中定义的函数,它们都接收一个处理器并返回一个 MethodRouter 实例。
use axum::{
routing::any,
Router,
};
/// 这是一个处理器
async fn handler() {}
// 创建路由,匹配 `/` 路径
//
// 这里的 `any` 函数就是方法路由
// 它接收一个处理器并构造出 `MethodRouter`
let app = Router::new().route("/", any(handler));
以下是常用的方法路由:
| 函数名 | 含义 |
|---|---|
any |
匹配任意请求方法 |
get |
只匹配 GET 请求 |
post |
只匹配 POST 请求 |
方法路由还允许进行链式调用。
async fn handler() {}
async fn other_handler() {}
// `POST /` 请求交给 `other_handler`,其它则都由 `handler` 处理
let app = Router::new().route("/", any(handler).post(other_handler));
使用 .nest 方法可以将一个 Router 嵌套到另一个 Router 中,这样就可以将多个路由组合成一个更大的路由。
这在构建大型复杂 Web 应用时很有用,因为它可以允许我们将路由划分为多个模块,便于管理和维护。
use axum::{
routing::{get, post},
Router,
};
let user_routes = Router::new().route("/{id}", get(|| async {}));
let team_routes = Router::new().route("/", post(|| async {}));
let api_routes = Router::new()
.nest("/users", user_routes)
.nest("/teams", team_routes);
let app = Router::new().nest("/api", api_routes);
// 路径在路由嵌套时会被拼接起来,因此
// `GET /api/users/{id}` 会由 `user_routes` 处理
// `POST /api/teams` 会由 `team_routes` 处理
使用 .merge 方法可以将一个 Router 与另一个 Router 合并。这两个路由的路径会被平行地合并,而不是像 .nest 那样进行嵌套拼接。
同样地,这在构建大型复杂 Web 应用时对我们很有用。
use axum::{
routing::get,
Router,
};
let user_routes = Router::new()
.route("/users", get(users_list))
.route("/users/{id}", get(users_show));
let team_routes = Router::new()
.route("/teams", get(teams_list));
let app = Router::new()
.merge(user_routes)
.merge(team_routes);
// 也可以写作 `user_routes.merge(team_routes)`
// 现在会匹配 `GET /users`、`GET /users/{id}`、`GET /teams`