本文主要讨论axum的路由,通过路由我们可以灵活的来将不同的请求路径路由到不同的handler,也能自由的组合不同的路由对象来处理请求。

<!--more-->

axum的路由主要分为两个部分,一部分是匹配规则,一部分是路由对象的组合方式。

往期文章:

  • https://youerning.top/post/axum/quickstart-1
  • https://youerning.top/post/axum/quickstart-2
  • https://youerning.top/post/axum/quickstart-3

匹配规则

一般来说,路由的匹配都是通过前缀树算法来实现的,axum的路由规则也是前缀树,不过axum并没有自己实现这个前缀树的算法,而是使用现有的第三方库matchit,支持三种匹配方式,完全匹配,命名参数匹配,通配符匹配,代码如下:

// https://youerning.top/post/axum/quickstart-4
use matchit::Router;

fn main() -> Result<(), Box<dyn std::error::Error>>{
    let mut router = Router::new();
    // 完全匹配
    router.insert("/home", "youerning.top")?;
    let matched = router.at("/home")?;
    assert_eq!(*matched.value, "youerning.top");

    // 命名参数匹配  官方叫做Named Parameters
    router.insert("/users/:id", "A User")?;
    let matched = router.at("/users/978")?;
    assert_eq!(matched.params.get("id"), Some("978"));
    assert_eq!(*matched.value, "A User");

    // 通配符匹配   官方叫做Catch-all Parameters
    router.insert("/*p", "youerning.top")?;

    assert_eq!(router.at("/foo.js")?.params.get("p"), Some("foo.js"));
    assert_eq!(router.at("/c/bar.css")?.params.get("p"), Some("c/bar.css"));
    // 注意不能匹配到/
    assert!(router.at("/").is_err());
    Ok(())
}

上面代码改自matchit的官方示例,为啥用它,我想是因为它超级快吧,下面是它的性能测试比较, 200纳秒以内!!! 性能恐怖如斯。

Compare Routers/matchit 
time:   [197.57 ns 198.74 ns 199.83 ns]

Compare Routers/actix
time:   [26.805 us 26.811 us 26.816 us]

Compare Routers/path-tree
time:   [468.95 ns 470.34 ns 471.65 ns]

Compare Routers/regex
time:   [22.539 us 22.584 us 22.639 us]

Compare Routers/route-recognizer
time:   [3.7552 us 3.7732 us 3.8027 us]

Compare Routers/routefinder
time:   [5.7313 us 5.7405 us 5.7514 us]

下面是axum的代码,跟上面的代码没有太多区别


use axum::{response::Html, routing::get, Router, extract::Path};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index_hanlder))
        .route("/users/:id", get(user_handler1))
        .route("/download/*path", get(download_handler2))
        ;

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}

async fn index_hanlder() -> Html<&'static str> {
    Html("Hello, World!")
}

async fn user_handler1(Path(id): Path<i32>) -> String {
    format!("name: {id}")
}

async fn download_handler2(Path(path): Path<String>) -> String {
    format!("download path: /{path}")
}

服务(Service)

axum是构筑在其他框架之上的框架,所以可以复用其他框架的一些特性,比如Tower里面的Service概念(一个trait),我们可以很简单的将实现了这个trait的对象注册到路由中。


use axum::{
    body::Body,
    response::Response,
    routing::get,
    Router,
    http::Request,
    routing::any_service,
};
use std::convert::Infallible;
use tower::service_fn;


#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index_handler))
        .route(
            "/",
            any_service(service_fn(|req: Request<Body>| async move {
                let body = Body::from(format!("Hi from `{} /`", req.method()));
                let res = Response::new(body);
                Ok::<_, Infallible>(res)
            }))
        )
        .route(
            "/service1",
            any_service(service_fn(|req: Request<Body>| async move {
                let body = Body::from(format!("Hi from `{} /service1`", req.method()));
                let res = Response::new(body);
                Ok::<_, Infallible>(res)
            }))
        )
        .route_service(
            "/service2",
            service_fn(|req: Request<Body>| async move {
                let body = Body::from(format!("Hi from `{} /service2`", req.method()));
                let res = Response::new(body);
                Ok::<_, Infallible>(res)
            })
        )
        ;

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}

async fn index_handler() -> String {
    format!("index page")
}

值得注意的是,注册服务的路由的时候会跟常规的路由合并, 比如这里的/路由跟后面的服务路由合并了,如果使用GET方法将会调用index_handler,其他方法就调用这个服务对象

使用Service大致有两个好处,一是捕获所有方法的路由,二是可以将实现了tower Service trait的对象直接纳入进来(如果你有的话)。tower是一个很棒的框架,很多框架都使用了它,比如hyper, reqwest, 以及本文的axum

路由嵌套

很多时候我们会将路由分割成一个个部分,这些部分会有层级关系,比如/api/users,/api/products, 我们一般将/api称为前缀,而后面的路由由一个个小的路由对象来提供请求。


use axum::{
    body::Body,
    routing::get,
    Router,
    extract::Path,
    http::Request,
};


#[tokio::main]
async fn main() {
    let user_routes = Router::new()
        .route("/:id", get(path_handler));

    let product_routes = Router::new()
        .route("/:id", get(path_handler));

    let api_routes = Router::new()
        .route("/", get(api_handler))
        .nest("/users", user_routes)
        .nest("/products", product_routes);

    let app = Router::new()
        .route("/", get(index_handler))
        .nest("/api", api_routes);

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}

async fn index_handler() -> String {
    format!("hello world")
}

async fn api_handler() -> String {
    format!("api handler")
}

async fn path_handler(Path(id): Path<i32>, req: Request<Body>) -> String {
    format!("id[{id}] at {}", req.uri())
}

值得注意的是,在嵌套的子路由里面看到的uri不是完整的uri,比如/api/users/1看到的路由是"/1", 如果需要完整的url路径需要使用OriginalUri

当然了,还可以嵌套Service这和上面的路由差不多,这里就不演示

fallback

默认情况下,创建的路由都有一个404的默认fallback用于捕获无法匹配的路由,axum自然也是支持手动指定的。


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


#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index_handler))
        .fallback(fallback);

    let addr = "0.0.0.0:8080";
    axum::Server::bind(&addr.parse().unwrap())
      .serve(app.into_make_service())
      .await
      .unwrap();
}

async fn index_handler() -> &'static str {
    "Hello, World!"
}

async fn fallback() -> String {
    format!("youerning.top")
}

Handler的简单介绍

axum通过宏生成了支持最多17个参数的Handler实现,这也是为啥我们可以只用一个简单的异步函数就能作为handler的原因, 它的实现代码写得很漂亮。

// 为各种类型生成Handler实现的声明函
macro_rules! impl_handler {
    (
        [$($ty:ident),*], $last:ident
    ) => {
        #[allow(non_snake_case, unused_mut)]
        impl<F, Fut, S, B, Res, M, $($ty,)* $last> Handler<(M, $($ty,)* $last,), S, B> for F
        where
            F: FnOnce($($ty,)* $last,) -> Fut + Clone + Send + 'static,
            Fut: Future<Output = Res> + Send,
            B: Send + 'static,
            S: Send + Sync + 'static,
            Res: IntoResponse,
            $( $ty: FromRequestParts<S> + Send, )*
            $last: FromRequest<S, B, M> + Send,
        {
            type Future = Pin<Box<dyn Future<Output = Response> + Send>>;

            fn call(self, req: Request<B>, state: S) -> Self::Future {
                Box::pin(async move {
                    let (mut parts, body) = req.into_parts();
                    let state = &state;

                    $(
                        let $ty = match $ty::from_request_parts(&mut parts, state).await {
                            Ok(value) => value,
                            Err(rejection) => return rejection.into_response(),
                        };
                    )*

                    let req = Request::from_parts(parts, body);

                    let $last = match $last::from_request(req, state).await {
                        Ok(value) => value,
                        Err(rejection) => return rejection.into_response(),
                    };

                    let res = self($($ty,)* $last,).await;

                    res.into_response()
                })
            }
        }
    };
}

// 空参数的实现
impl<F, Fut, Res, S, B> Handler<((),), S, B> for F
where
    F: FnOnce() -> Fut + Clone + Send + 'static,
    Fut: Future<Output = Res> + Send,
    Res: IntoResponse,
    B: Send + 'static,
{
    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;

    fn call(self, _req: Request<B>, _state: S) -> Self::Future {
        Box::pin(async move { self().await.into_response() })
    }
}

// 为1到16个参数的函数签名生成对的handler实现
all_the_tuples!(impl_handler);
macro_rules! all_the_tuples {
    ($name:ident) => {
        $name!([], T1);
        $name!([T1], T2);
    	// 省略其他列表
        $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], T16);
    };
}


小结

axum的路由还是比较简单明了的,通过路由规则和路由嵌套应该能够应付大多数情况了。不过本文也有一些东西这里没有提到,那就是嵌套路由的状态共享和中间件,这个需要看看官方文档或者源代码。

参考链接

  • https://docs.rs/axum/latest/axum/
  • https://github.com/ibraheemdev/matchit
  • https://github.com/tower-rs/tower