RUST web框架axum快速入门教程4之路由
本文主要讨论axum
的路由,通过路由我们可以灵活的来将不同的请求路径路由到不同的handler,也能自由的组合不同的路由对象来处理请求。
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("<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的对象注册到路由中。
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 {
"<h1>Hello, World!</h1>"
}
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
的路由还是比较简单明了的,通过路由规则和路由嵌套应该能够应付大多数情况了。不过本文也有一些东西这里没有提到,那就是嵌套路由的状态共享和中间件,这个需要看看官方文档或者源代码。