一文讀懂 Rust axu

先從 HTTP 說起,HTTP 是超文本傳輸協議,是一種客戶端 - 服務器協議,它是 Web 上進行任何數據交換的基礎,客戶端與服務端之間通過交換一個個獨立的消息(而非數據流)進行通信。由客戶端通常是個瀏覽器,現在也可能是 APP,發出的消息被稱作_請求_(request),由服務端發出的應答消息被稱作_響應_(response)。請求是由客戶端發起的,服務器給出響應,可以是 HTML 頁面,也可能是接口數據。例如,如果瀏覽器請求 www.example.com/a.html,那麼服務器將根據請求的 URL 獲取到路徑 a.html,然後服務器找到 a.html 資源文件,返回給瀏覽器。如果 APP 請求 www.example.com/b,那麼服務器將根據請求的 URL 獲取到路徑 b,然後服務器找到 b 對應的處理器執行,然後將執行的結果返回給 APP。服務器端的技術一直在迅猛發展,出現了很多框架,能夠很好的處理來自瀏覽器或客戶端的請求,無論是網頁還是接口數據。它們都基於 HTTP 協議。當前的這些 Web 應用框架大都由這幾大組件組成,路由,處理器,中間件。路由處於 Web 服務的最前端,它接收來自客戶端的請求,將請求提交給處理器進行處理,處理器做具體的事務,如果有中間件,它則處於路由和處理器中間,做一些通用的事務。它們協同完成整個 Web 服務。理解了 HTTP,就很容易理解 Web 應用框架。

axum 是 Rust 語言寫的一個 web 應用框架。它有以下特性:將請求路由到具有無宏 API 的處理程序,這個有別於 Rust 中其它的 web 應用框架。使用提取器解析請求。簡單且可預測的錯誤處理模塊。用最少的模板生成響應。axum 沒有自己的中間件系統,它使用了 tower::Serveice。這意味着 axum 可以免費獲得超時、跟蹤、壓縮、授權等。您還能使用 hyper 或 tonic 編寫的應用程序共享中間件。

先來一個 Hello world!看看 axum 的模樣。

use axum::{
    routing::get,
    Router,
};
#[tokio::main]
async fn main() {
    // build our application with a single route
    let app = Router::new().route("/", get(|| async { "Hello, World!" }));
    // run it with hyper on localhost:3000
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

我們將按照路由、處理器、中間件的順序介紹 axum。

一、路由 Routing

路由的作用是設置路徑 Path 和處理器 Handler 的對應關係。路由支持在一個路徑上綁定多個方法。

use axum::{Router, routing::get};
// our router
let app = Router::new()
    .route("/", get(root))
    .route("/foo", get(get_foo).post(post_foo))
    .route("/foo/bar", get(foo_bar));
// which calls one of these handlers
async fn root() {}
async fn get_foo() {}
async fn post_foo() {}
async fn foo_bar() {}

axum 的路由很是強大,支持各種操作。

我們先從路由的路徑來看,它支持普通路由,嵌套路由,合併路由。

a、普通路由

普通路由的路徑,以所見即所得的字符串添加爲路徑。

let app = Router::new()
    .route("/", get(root))
    .route("/users", get(list_users).post(create_user))
    .route("/users/:id", get(show_user))
    .route("/api/:version/users/:id/action", delete(do_users_action))
    .route("/assets/*path", get(serve_asset));

b、嵌套路由

嵌套路由的路徑,有一個相同的前綴。其他語言中可能叫分組。

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);
// Our app now accepts
// - GET /api/users/:id
// - POST /api/teams

說明:嵌套路由看不到原始請求 URI,因爲它去掉了匹配的前綴。如果你提供像靜態文件這樣的服務,那麼請使用 OriginalUri 獲取原始請求 URI。嵌套路由和通配符路由的區別,嵌套路由類似於通配符路由。不同之處在於,通配符路由可以看到整個 URI,而嵌套路由只能看到去掉前綴後的路徑。

c、合併路由

合併路由是將路由合併到一起,合併後的路由路徑是同級的。

// define some routes separately
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));
// combine them into one
let app = Router::new()
    .merge(user_routes)
    .merge(team_routes);
// Our app now accepts
// - GET /users
// - GET /users/:id
// - GET /teams

我們再從路由的處理器 Handler 來看,它支持以下內容:

d、異步函數

具體參考下面的 handler。

e、tower 服務

let app = Router::new()
    .route(
        // Any request to `/` goes to a service
        "/",
        // Services whose response body is not `axum::body::BoxBody`
        // can be wrapped in `axum::routing::any_service` (or one of the other routing filters)
        // to have the response body mapped
        any_service(service_fn(|_: Request<Body>| async {
            let res = Response::new(Body::from("Hi from `GET /`"));
            Ok::<_, Infallible>(res)
        }))
    )
    .route_service(
        "/foo",
        // This service's response body is `axum::body::BoxBody` so
        // it can be routed to directly.
        service_fn(|req: Request<Body>| async move {
            let body = Body::from(format!("Hi from `{} /foo`", req.method()));
            let body = axum::body::boxed(body);
            let res = Response::new(body);
            Ok::<_, Infallible>(res)
        })
    )
    .route_service(
        // GET `/static/Cargo.toml` goes to a service from tower-http
        "/static/Cargo.toml",
        ServeFile::new("Cargo.toml"),
    );

f、路由綁定狀態

將路由與 State 組合時,每個路由必須具有相同類型的狀態。下面會細講。

let routes = Router::new()
    .route("/", get(|State(state): State<AppState>| async {
        // use state
    }))
    .with_state(AppState {});

g、層級

將 tower::Layer 應用於路由器中的所有路由。這可以用於爲一組路由的請求添加額外的處理。

let app = Router::new()
    .route("/foo", get(|| async {}))
    .route("/bar", get(|| async {}))
    .layer(TraceLayer::new_for_http());

在應用多箇中間件時,建議使用 tower::ServiceBuilder。有關更多詳細信息,請參閱中間件。

二、處理程序 Handlers

在 axum 中,處理程序是一個異步函數,它接受零個或多個提取器作爲參數,並返回可以轉換爲響應的內容。處理程序是應用程序邏輯所在的地方。對於要用作處理程序的函數,它必須實現 handler 特性。

我們講了 http,我們知道,當發過來了 http 請求時,我們要根據 http 請求中的內容,進行處理,然後返回客戶端想要的結果,即響應。而 http 請求中可包含我們的需求的地方太多,例如,請求路徑,Http 頭,請求參數,請求 Body 等。而 axum 將這些從請求中獲取內容的手段通過 Extractors 來實現。當我們獲取到請求中的內容時,我們可能會返回給客戶端錯誤,或者它想要的數據。基本流程就是這樣,具體的請參考下面的具體說明。

a、提取器 Extractors

提取器從請求 Request 中提取數據的類型和特徵,是一種實現 FromRequest 或 FromRequestParts 的類型。提取器是您挑選傳入請求以獲得處理程序所需零件的方法。

axum 已實現的提取類型有 Json,TypedHeader,Path,Extension,Query,String,Bytes,Request 等,我們這裏以案例進行一些提取器的具體說明。

1,Path 提取器,它將從 URL 獲取捕獲並使用 serde 對其進行解析。任何百分比編碼的參數都將自動解碼。解碼的參數必須是有效的 UTF-8,否則 Path 將失敗並返回 400 錯誤請求響應。axum 中的 Path 支持 3 種格式:靜態路徑,例如 / a,/a/b 等,捕獲模式,例如 / users/:id,/:key 等,通配符模式,例如 / users/*id,/*key 等,它們意如其名,捕獲和通配符的區別是捕獲隻影響到: 的該字段,而通配符影響到 * 之後的所有字段。通過 Path 提取器可以獲取到字段內容,使用方法如下:

async fn users_teams_show(
    Path((user_id, team_id)): Path<(Uuid, Uuid)>,
) {
    // ...
}
let app = Router::new().route("/users/:user_id/team/:team_id", get(users_teams_show));

2,Query 提取器,將查詢字符串反序列化爲實現了 serde::Deserialize 的類型。如果查詢字符串無法解析,它將以 400 錯誤請求響應拒絕該請求。

#[derive(Deserialize)]
struct Pagination {
    page: usize,
    per_page: usize,
}
// This will parse query strings like `?page=2&per_page=30` into `Pagination`
// structs.
async fn list_things(pagination: Query<Pagination>) {
    let pagination: Pagination = pagination.0;
    // ...
}
let app = Router::new().route("/list_things", get(list_things));

3,Form 提取器,Get 和 Head 方法將請求的參數反序列化,其他的方法將 application/x-www-Form-urlencoded 編碼請求體反序列化。由於解析表單數據可能需要使用請求體,因此如果一個處理程序中有多個提取器,則表單提取器必須是最後一個。具體下面有說明。

#[derive(Deserialize)]
struct SignUp {
    username: String,
    password: String,
}
async fn accept_form(Form(sign_up): Form<SignUp>) {
    // ...
}

4,MultiPart 提取器,用於解析多部分 / 表單數據請求(通常用於文件上傳)。由於從請求中提取多部分表單數據需要消耗主體,因此如果一個處理程序中有多個提取器,則多部分提取器必須是最後一個。具體下面有說明。

use axum::{
    extract::Multipart,
    routing::post,
    Router,
};
use futures_util::stream::StreamExt;
async fn upload(mut multipart: Multipart) {
    while let Some(mut field) = multipart.next_field().await.unwrap() {
        let name = field.name().unwrap().to_string();
        let data = field.bytes().await.unwrap();
        println!("Length of `{}` is {} bytes", name, data.len());
    }
}
let app = Router::new().route("/upload", post(upload));

5,Json 提取器,用於反序列化請求 Body 中的有效 Json 數據爲目標類型。以下情況會失敗,該請求沒有 Content-Type:application/json(或類似的)頭。正文不包含語法有效的 JSON。正文包含語法有效的 JSON,但無法將其反序列化爲目標類型。緩衝請求正文失敗。由於解析 JSON 需要消耗請求體,因此如果一個處理程序中有多個提取器,JSON 提取器必須是最後一個。具體下面有說明。

#[derive(Deserialize)]
struct CreateUser {
    email: String,
    password: String,
}
async fn create_user(extract::Json(payload): extract::Json<CreateUser>) {
    // payload is a `CreateUser`
}
let app = Router::new().route("/users", post(create_user));

6,TypedHeader 提取器,通常,建議通過 TypedHeader 僅提取所需的標頭,而使用 HeaderMap 提取所有標頭。

use axum::{
    TypedHeader,
    headers::UserAgent,
    routing::get,
    Router,
};
async fn users_teams_show(
    TypedHeader(user_agent): TypedHeader<UserAgent>,
) {
    // ...
}
let app = Router::new().route("/users/:user_id/team/:team_id", get(users_teams_show));

上面僅列出了常用的提取器,如果你需要更多提取器,請查閱文檔。

如果你要使用多個提取器,它是有順序的,他的位置爲函數的參數從左到右排序。請求 Body 只允許提取一次。因此 axum 框架要求這個類型的提取器必須位於函數的最後一個參數。

如果提取器定義了,當請求的內容不滿足提取器時,axum 會拒絕處理請求並返回錯誤。當參數不是必選項時,可以使用 Option 包裝設置爲可選。

use axum::{
    extract::Json,
    routing::post,
    Router,
};
use serde_json::Value;
async fn create_user(payload: Option<Json<Value>>) {
    if let Some(payload) = payload {
        // We got a valid JSON payload
    } else {
        // Payload wasn't valid JSON
    }
}
let app = Router::new().route("/users", post(create_user));

自定義提取器

如果你有需求要自定義提取器,只需要實現 FromRequestParts 或 FromRequest,它們的區別是 FromRequestParts 不需要實現請求 body。

自定義實現 FromRequestParts 的例子:

use axum::{
    async_trait,
    extract::FromRequestParts,
    routing::get,
    Router,
    http::{
        StatusCode,
        header::{HeaderValue, USER_AGENT},
        request::Parts,
    },
};
struct ExtractUserAgent(HeaderValue);
#[async_trait]
impl<S> FromRequestParts<S> for ExtractUserAgent
where
    S: Send + Sync,
{
    type Rejection = (StatusCode, &'static str);
    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        if let Some(user_agent) = parts.headers.get(USER_AGENT) {
            Ok(ExtractUserAgent(user_agent.clone()))
        } else {
            Err((StatusCode::BAD_REQUEST, "`User-Agent` header is missing"))
        }
    }
}
async fn handler(ExtractUserAgent(user_agent): ExtractUserAgent) {
    // ...
}

自定義實現 FromRequest 的例子:

use axum::{
    async_trait,
    extract::FromRequest,
    response::{Response, IntoResponse},
    body::Bytes,
    routing::get,
    Router,
    http::{
        StatusCode,
        header::{HeaderValue, USER_AGENT},
        Request,
    },
};
struct ValidatedBody(Bytes);
#[async_trait]
impl<S, B> FromRequest<S, B> for ValidatedBody
where
    Bytes: FromRequest<S, B>,
    B: Send + 'static,
    S: Send + Sync,
{
    type Rejection = Response;
    async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
        let body = Bytes::from_request(req, state)
            .await
            .map_err(IntoResponse::into_response)?;
        // do validation...
        Ok(Self(body))
    }
}
async fn handler(ValidatedBody(body): ValidatedBody) {
    // ...
}
let app = Router::new().route("/foo", get(handler));

b、響應 Responses

任何實現 IntoResponse 的內容都可以從處理程序返回。axum 默認提供了一些類型的實現:Json,Html,(),String,Vec,StatusCode,HeaderMap,impl IntoResponse 等,我們這裏以案例進行一些類型的具體說明。

1,Html,html 響應,它可以使用模板,返回的響應頭將自動獲取 Content-Type: text/html.

普通:

// `Html` will get a `text/html` content-type
async fn html() -> Html<&'static str> {
    Html("<p>Hello, World!</p>")
}

使用模板:

async fn greet(extract::Path(name): extract::Path<String>) -> impl IntoResponse {
    let template = HelloTemplate { name };
    HtmlTemplate(template)
}
#[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate {
    name: String,
}
struct HtmlTemplate<T>(T);
impl<T> IntoResponse for HtmlTemplate<T>
where
    T: Template,
{
    fn into_response(self) -> Response {
        match self.0.render() {
            Ok(html) => Html(html).into_response(),
            Err(err) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Failed to render template. Error: {}", err),
            )
                .into_response(),
        }
    }
}

2,String,返回內容爲字符串。

// String will get a `text/plain; charset=utf-8` content-type
async fn plain_text(uri: Uri) -> String {
    format!("Hi from {}", uri.path())
}

3,(),返回空內容。

// `()` gives an empty response
async fn empty() {}

4,Json,返回 Json 格式的數據內容。

// `Json` will get a `application/json` content-type and work with anything that
// implements `serde::Serialize`
async fn json() -> Json<Vec<String>> {
    Json(vec!["foo".to_owned(), "bar".to_owned()])
}

上面僅列出了常用的響應返回類型,如果你需要更多類型,請查閱文檔。

通常,它也支持元組返回,例如下面的例子:

// `(StatusCode, impl IntoResponse)` will override the status code of the response
async fn with_status(uri: Uri) -> (StatusCode, String) {
    (StatusCode::NOT_FOUND, format!("Not Found: {}", uri.path()))
}
// Use `impl IntoResponse` to avoid having to type the whole type
async fn impl_trait(uri: Uri) -> impl IntoResponse {
    (StatusCode::NOT_FOUND, format!("Not Found: {}", uri.path()))
}
// `(HeaderMap, impl IntoResponse)` to add additional headers
async fn with_headers() -> impl IntoResponse {
    let mut headers = HeaderMap::new();
    headers.insert(header::CONTENT_TYPE, "text/plain".parse().unwrap());
    (headers, "foo")
}
// Or an array of tuples to more easily build the headers
async fn with_array_headers() -> impl IntoResponse {
    ([(header::CONTENT_TYPE, "text/plain")], "foo")
}
// Use string keys for custom headers
async fn with_array_headers_custom() -> impl IntoResponse {
    ([("x-custom", "custom")], "foo")
}
// `(StatusCode, headers, impl IntoResponse)` to set status and add headers
// `headers` can be either a `HeaderMap` or an array of tuples
async fn with_status_and_array_headers() -> impl IntoResponse {
    (
        StatusCode::NOT_FOUND,
        [(header::CONTENT_TYPE, "text/plain")],
        "foo",
    )
}
// `(Extension<_>, impl IntoResponse)` to set response extensions
async fn with_status_extensions() -> impl IntoResponse {
    (
        Extension(Foo("foo")),
        "foo",
    )
}
struct Foo(&'static str);
// Or mix and match all the things
async fn all_the_things(uri: Uri) -> impl IntoResponse {
    let mut header_map = HeaderMap::new();
    if uri.path() == "/" {
        header_map.insert(header::SERVER, "axum".parse().unwrap());
    }
    (
        // set status code
        StatusCode::NOT_FOUND,
        // headers with an array
        [("x-custom", "custom")],
        // some extensions
        Extension(Foo("foo")),
        Extension(Foo("bar")),
        // more headers, built dynamically
        header_map,
        // and finally the body
        "foo",
    )
}

上面的例子,不能意外覆蓋狀態或正文,因爲 IntoResponseParts 只允許設置標頭和擴展名。

要想控制,請使用 Response。

use axum::{
    Json,
    response::{IntoResponse, Response},
    body::{Full, Bytes},
    http::StatusCode,
};
async fn response() -> Response<Full<Bytes>> {
    Response::builder()
        .status(StatusCode::NOT_FOUND)
        .header("x-foo", "custom header")
        .body(Full::from("not found"))
        .unwrap()
}

如果你需要返回多個響應類型,需要調用. into_response() 將內容轉爲 axum::response::Response

use axum::{
    response::{IntoResponse, Redirect, Response},
    http::StatusCode,
};
async fn handle() -> Response {
    if something() {
        "All good!".into_response()
    } else if something_else() {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            "Something went wrong...",
        ).into_response()
    } else {
        Redirect::to("/").into_response()
    }
}
fn something() -> bool {
    // ...
}
fn something_else() -> bool {
    // ...
}

關於使用 impl IntoResponse,當從 handler 返回很大的響應類型時,你可以使用 impl IntoResponse 替代,例如下面的例子:

use axum::http::StatusCode;
async fn handler() -> (StatusCode, [(&'static str, &'static str); 1], &'static str) {
    (StatusCode::OK, [("x-foo", "bar")], "Hello, World!")
}

上面的例子可以調整爲這樣,這樣返回值更簡單一點。

use axum::{http::StatusCode, response::IntoResponse};
async fn impl_into_response() -> impl IntoResponse {
    (StatusCode::OK, [("x-foo", "bar")], "Hello, World!")
}

c、錯誤處理 Error handling

axum 旨在擁有一個簡單且可預測的錯誤處理模型。這意味着將錯誤轉換爲響應非常簡單,並且可以保證所有錯誤都得到處理。

axum 基於 tower::Service,該服務通過其關聯的錯誤類型捆綁錯誤。如果你的服務產生錯誤,並且這個錯誤傳遞到 hyper,則連接將在不發送響應的情況下終止。這通常是不可取的,因此 axum 確保你始終通過依賴類型系統來生成響應。axum 要求所有服務的錯誤類型都是 Infallible,Infallible 是指永遠不會發生錯誤的錯誤類型。

use axum::http::StatusCode;
async fn handler() -> Result<String, StatusCode> {
    // ...
}

雖然它看起來可能會因 StatusCode 而失敗,但實際上這並不是一個 “錯誤”。如果此處理程序返回 Err(some_status_code),則該 Err 仍將被轉換爲響應併發送回客戶端。這是通過 StatusCode 的 IntoResponse 實現完成的。無論您是返回 Err(StatusCode::NOT_FOUND)還是 Err(StatusCode::INTERNAL_SERVER_ERROR)都無關緊要。這些在 axum 中不被視爲錯誤。與其使用直接的 StatusCode,不如使用最終可以轉換爲 Response 的中間錯誤類型。這允許使用?運算符處理程序。參見這些示例:

let app = Router::new().route("/", get(handler));
async fn handler() -> Result<(), AppError> {
    try_thing()?;
    Ok(())
}
fn try_thing() -> Result<(), anyhow::Error> {
    anyhow::bail!("it failed!")
}
// Make our own error that wraps `anyhow::Error`.
struct AppError(anyhow::Error);
// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Something went wrong: {}", self.0),
        )
            .into_response()
    }
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
    E: Into<anyhow::Error>,
{
    fn from(err: E) -> Self {
        Self(err.into())
    }
}

d、在處理程序中共享狀態

在處理程序之間共享某些狀態是很常見的。例如:可能需要共享數據庫連接池或到其他服務的客戶端。如果瞭解 Rust 的多線程共享數據,這裏很方便理解。

最常見的三種方法是:

1,使用 State,State 是一種提取器,它更安全。缺點是它的動態性不如請求擴展。

use axum::{
    extract::State,
    routing::get,
    Router,
};
use std::sync::Arc;
struct AppState {
    // ...
}
let shared_state = Arc::new(AppState { /* ... */ });
let app = Router::new()
    .route("/", get(handler))
    .with_state(shared_state);
async fn handler(
    State(state): State<Arc<AppState>>,
) {
    // ...
}

2,使用請求擴展,這種方法的缺點是,如果您試圖提取一個不存在的擴展,可能是因爲您忘記添加中間件,或者因爲提取了錯誤的類型,則會出現運行時錯誤(特別是 500 內部服務器錯誤響應)。

use axum::{
    extract::Extension,
    routing::get,
    Router,
};
use std::sync::Arc;
struct AppState {
    // ...
}
let shared_state = Arc::new(AppState { /* ... */ });
let app = Router::new()
    .route("/", get(handler))
    .layer(Extension(shared_state));
async fn handler(
    Extension(state): Extension<Arc<AppState>>,
) {
    // ...
}

3,使用閉包捕獲,這種方法的缺點是它比使用 State 或 Extension 更冗長。

use axum::{
    Json,
    extract::{Extension, Path},
    routing::{get, post},
    Router,
};
use std::sync::Arc;
use serde::Deserialize;
struct AppState {
    // ...
}
let shared_state = Arc::new(AppState { /* ... */ });
let app = Router::new()
    .route(
        "/users",
        post({
            let shared_state = Arc::clone(&shared_state);
            move |body| create_user(body, shared_state)
        }),
    )
    .route(
        "/users/:id",
        get({
            let shared_state = Arc::clone(&shared_state);
            move |path| get_user(path, shared_state)
        }),
    );
async fn get_user(Path(user_id): Path<String>, state: Arc<AppState>) {
    // ...
}
async fn create_user(Json(payload): Json<CreateUserPayload>, state: Arc<AppState>) {
    // ...
}
#[derive(Deserialize)]
struct CreateUserPayload {
    // ...
}

三、中間件 Middleware

axum 的獨特之處在於,它沒有自己定製的中間件系統,它與 tower 集成。這意味着 tower 和 tower http 中間件的生態系統,axum 都可以使用。當您使用 Router::layer(或類似功能)添加中間件時,所有先前添加的路由都將封裝在中間件中。一般來說,這導致中間件從下到上執行。建議使用 tower::ServiceBuilder 添加多箇中間件,ServiceBuilder 的工作原理是將所有層組成一個層,使它們從上到下運行。因此,對於前面的代碼,layer_one 將首先接收請求,然後是 layer_2,然後是第三層,然後是處理程序,然後響應將通過第三層、第二層,最後是 layer_one 返回。從上到下執行中間件通常更容易理解和理解,這也是推薦 ServiceBuilder 的原因之一。多箇中間件的執行順序爲:

use tower::ServiceBuilder;
use axum::{routing::get, Router};
async fn handler() {}
let app = Router::new()
    .route("/", get(handler))
    .layer(
        ServiceBuilder::new()
            .layer(layer_one)
            .layer(layer_two)
            .layer(layer_three),
    );

tower 常用的中間件有:

你可以直接使用在你的應用中,使用 layer 添加即可。

當您希望執行一個小的即席操作,例如添加一個標頭。您不打算將您的中間件發佈爲供其他人使用的板條箱。那麼 tower 有幾個實用組合子,可以用來對請求或響應進行簡單的修改。

最常用的是:

ServiceBuilder::map_request

use tower::ServiceBuilder;
use tower::ServiceExt;
// Suppose we have some `Service` whose request type is `String`:
let string_svc = tower::service_fn(|request: String| async move {
    println!("request: {}", request);
    Ok(())
});
// ...but we want to call that service with a `usize`. What do we do?
let usize_svc = ServiceBuilder::new()
     // Add a middlware that converts the request type to a `String`:
    .map_request(|request: usize| format!("{}", request))
    // ...and wrap the string service with that middleware:
    .service(string_svc);
// Now, we can call that service with a `usize`:
usize_svc.oneshot(42).await?;

ServiceBuilder::map_response

// A service returning Result<Record, _>
let service = DatabaseService::new("127.0.0.1:8080");
// Map the response into a new response
let mut new_service = service.map_response(|record| record.name);
// Call the new service
let id = 13;
let name = new_service
    .ready()
    .await?
    .call(id)
    .await?;

ServiceBuilder::then

// A service returning Result<Record, DbError>
let service = DatabaseService::new("127.0.0.1:8080");
// An async function that attempts to recover from errors returned by the
// database.
async fn recover_from_error(error: DbError) -> Result<Record, DbError> {
    // ...
}
// If the database service returns an error, attempt to recover by
// calling `recover_from_error`. Otherwise, return the successful response.
let mut new_service = service.then(|result| async move {
    match result {
        Ok(record) => Ok(record),
        Err(e) => recover_from_error(e).await,
    }
});
// Call the new service
let id = 13;
let record = new_service
    .ready()
    .await?
    .call(id)
    .await?;

ServiceBuilder::and_then

// A service returning Result<Record, _>
let service = DatabaseService::new("127.0.0.1:8080");
// Map the response into a new response
let mut new_service = service.and_then(|record: Record| async move {
    let name = record.name;
    avatar_lookup(name).await
});
// Call the new service
let id = 13;
let avatar = new_service.call(id).await.unwrap();

axum 提供了許多編寫中間件的方法,具有不同的抽象級別,並且有不同的優缺點。

1,在以下情況下使用 axum::middleware::from_fn 編寫中間件:

您對實現自己的 Future 感到不舒服,寧願使用熟悉的 async/await 語法。

您不打算將您的中間件發佈爲供其他人使用的板條箱。像這樣編寫的中間件只與 axum 兼容。

use axum::{
    Router,
    http::{self, Request},
    routing::get,
    response::Response,
    middleware::{self, Next},
};
async fn my_middleware<B>(
    request: Request<B>,
    next: Next<B>,
) -> Response {
    // do something with `request`...
    let response = next.run(request).await;
    // do something with `response`...
    response
}
let app = Router::new()
    .route("/", get(|| async { /* ... */ }))
    .layer(middleware::from_fn(my_middleware));

2,在以下情況下使用 axum::middleware::from_extractor 編寫中間件:

您有一種類型,有時您想用作提取器,有時又想用作中間件。如果您只需要將您的類型作爲中間件,則更喜歡 middleware::from_fn。

use axum::{
    extract::FromRequestParts,
    middleware::from_extractor,
    routing::{get, post},
    Router,
    http::{header, StatusCode, request::Parts},
};
use async_trait::async_trait;
// An extractor that performs authorization.
struct RequireAuth;
#[async_trait]
impl<S> FromRequestParts<S> for RequireAuth
where
    S: Send + Sync,
{
    type Rejection = StatusCode;
    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        let auth_header = parts
            .headers
            .get(header::AUTHORIZATION)
            .and_then(|value| value.to_str().ok());
        match auth_header {
            Some(auth_header) if token_is_valid(auth_header) => {
                Ok(Self)
            }
            _ => Err(StatusCode::UNAUTHORIZED),
        }
    }
}
fn token_is_valid(token: &str) -> bool {
    // ...
}
async fn handler() {
    // If we get here the request has been authorized
}
async fn other_handler() {
    // If we get here the request has been authorized
}
let app = Router::new()
    .route("/", get(handler))
    .route("/foo", post(other_handler))
    // The extractor will run before all routes
    .route_layer(from_extractor::<RequireAuth>());

庫集成 Axum 說明

想要提供 FromRequest、FromRequestParts 或 IntoResponse 實現的庫,應該依賴 axum-core,而不是 axum(如果可能的話)。axum-core 包含核心類型和特徵,不太可能受到破壞性變化。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/G8rS2G9xFMn_CmjmZPuw-Q