RUST web框架axum快速入门教程4之路由

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

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

往期文章:

匹配规则

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

rust 复制代码
// 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纳秒以内!!! 性能恐怖如斯。

less 复制代码
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的代码,跟上面的代码没有太多区别

rust 复制代码
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("<h1>Hello, World!</h1>")
}

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的对象注册到路由中。

rust 复制代码
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称为前缀,而后面的路由由一个个小的路由对象来提供请求。

rust 复制代码
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自然也是支持手动指定的。

rust 复制代码
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 {
    "<h1>Hello, World!</h1>"
}

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

Handler的简单介绍

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

rust 复制代码
// 为各种类型生成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的路由还是比较简单明了的,通过路由规则和路由嵌套应该能够应付大多数情况了。不过本文也有一些东西这里没有提到,那就是嵌套路由的状态共享和中间件,这个需要看看官方文档或者源代码。

参考链接

相关推荐
摸鱼的春哥5 分钟前
春哥的Agent通关秘籍07:5分钟实现文件归类助手【实战】
前端·javascript·后端
Smart-Space5 分钟前
htmlbuilder - rust灵活构建html
rust·html
魔力军15 分钟前
Rust学习Day2: 变量与可变性、数据类型和函数和控制流
开发语言·学习·rust
Victor35621 分钟前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack22 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo23 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor35624 分钟前
MongoDB(3)什么是文档(Document)?
后端
牛奔2 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌7 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX9 小时前
服务异步通信
开发语言·后端·微服务·ruby