Router
什么是路由
Router 定义了一个 HTTP 请求会被哪些中间件和 Handler 处理. 这个是 Salvo 里面最基础也是最核心的功能.
Router 内部实际上是由一系列过滤器(Filter) 组成, 当请求到来时, 路由会按添加的顺序, 由上往下依次测试自身及其子孙项是否能够匹配请求, 如果匹配成功, 则依次执行路由以及其子孙路由形成的整个链上的中间件, 如果处理过程中, Response 的状态被设置为错误(4XX, 5XX) 或者是跳转(3XX), 则后续中间件和 Handler 都会跳过. 你也可以手工调 ctrl.skip_rest() 跳过后续的中间件和 Handler.
在匹配的过程中,存在一个 Url 路径信息,可以认为它是一个在匹配过程中需要完全被 Filter 消费的对象,如果在某个 Router 中所有的 Filter 都匹配成功,并且这个 Url 路径信息已经完全被消费掉,则会认为是“匹配成功”.
比如:
Router::with_path("articles").get(list_articles).post(create_article);
实际上等同于:
Router::new()
// PathFilter 可以过滤请求路径, 只有请求路径里包含 articles 片段时才会匹配成功,
// 否则匹配失败. 比如: /articles/123 是匹配成功的, 而 /articles_list/123
// 虽然里面包含了 articles, 但是因为后面还有 _list, 是匹配不成功的.
.filter(PathFilter::new("articles"))
// 在 root 匹配成功的情况下, 如果请求的 method 是 get, 则内部的子路由可以匹配成功,
// 并且由 list_articles 处理请求.
.push(Router::new().filter(filters::get()).handle(list_articles))
// 在 root 匹配成功的情况下, 如果请求的 method 是 post, 则内部的子路由可以匹配成功,
// 并且由 create_article 处理请求.
.push(Router::new().filter(filters::post()).handle(create_article));
如果访问 GET /articles/ 认为匹配成功,并执行 list_articles. 但是如果访问的是 GET /articles/123 则匹配路由失败并返回 404 错误, 因为 ·Router::with_path("articles")仅仅只消费掉了 Url 路径信息中的 /articles, 还有 /123 部分未被消费掉,因此认为匹配失败. 要想能够匹配成功,路由可以改成:
Router::with_path("articles/{**}").get(list_articles).post(create_article);
这里的 {**} 会匹配任何多余的路径, 所以它能够匹配 GET /articles/123 执行 list_articles.
扁平式定义
我们可以用扁平式的风格定义路由:
Router::with_path("writers").get(list_writers).post(create_writer);
Router::with_path("writers/{id}").get(show_writer).patch(edit_writer).delete(delete_writer);
Router::with_path("writers/{id}/articles").get(list_writer_articles);
树状式定义
我们也可以把路由定义成树状, 这也是推荐的定义方式:
Router::with_path("writers")
.get(list_writers)
.post(create_writer)
.push(
Router::with_path("{id}")
.get(show_writer)
.patch(edit_writer)
.delete(delete_writer)
.push(Router::with_path("articles").get(list_writer_articles)),
);
这种形式的定义对于复杂项目, 可以让 Router 的定义变得层次清晰简单.
在 Router 中有许多方法调用后会返回自己(Self), 以便于链式书写代码. 有时候, 你需要根据某些条件决定如何路由, 路由系统也提供了 then 函数, 也很容易使用:
Router::new()
.push(
Router::with_path("articles")
.get(list_articles)
.push(Router::with_path("{id}").get(show_article))
.then(|router|{
if admin_mode() {
router.post(create_article).push(
Router::with_path("{id}").patch(update_article).delete(delete_writer)
)
} else {
router
}
}),
);
该示例代表仅仅当服务器在 admin_mode 时, 才会添加创建文章, 编辑删除文章等路由.
从路由中获取参数
在上面的代码中, {id} 定义了一个参数. 我们可以通过 Request 实例获取到它的值:
#[handler]
async fn show_writer(req: &mut Request) {
let id = req.param::<i64>("id").unwrap();
}
{id} 匹配了路径中的一个片段, 正常情况下文章的 id 只是一个数字, 此时我们可以使用正则表达式限制 id 的匹配规则, r"{id|\d+}".
对于这种数字类型, 还有一种更简单的方法是使用 <id:num>, 具体写法为:
{id:num}, 匹配任意多个数字字符;
{id:num[10]}, 只匹配固定特定数量的数字字符,这里的 10 代表匹配仅仅匹配 10 个数字字符;
{id:num(..10)}, 代表匹配 1 到 9 个数字字符;
{id:num(3..10)}, 代表匹配 3 到 9 个数字字符;
{id:num(..=10)}, 代表匹配 1 到 10 个数字字符;
{id:num(3..=10)}, 代表匹配 3 到 10 个数字字符;
{id:num(10..)}, 代表匹配至少 10 个数字字符.
还可以通过 {**}, {*+} 或者 {*?} 匹配所有剩余的路径片段. 为了代码易读性性强些, 也可以添加适合的名字, 让路径语义更清晰, 比如: {**file_path}.
{**}: 代表通配符匹配的部分可以是空字符串, 比如路径是 /files/{**rest_path}, 会匹配 /files, /files/abc.txt,/files/dir/abc.txt;
{*+}: 代表通配符匹配的部分必须存在,不能匹配到空字符串, 比如路径是 /files/{*+rest_path}, 不会匹配 /files 但是会匹配 /files/abc.txt,/files/dir/abc.txt;
{*?}: 代表通配符匹配的部分可以是空字符串, 但是只能包含一个路径片段, 比如路径是 /files/{*?rest_path}, 不会匹配 /files/dir/abc.txt 但是会匹配 /files,/files/abc.txt;
允许组合使用多个表达式匹配同一个路径片段, 比如 /articles/article_{id:num}/, /images/{name}.{ext}.
添加中间件
可以通过路由上的 hoop 函数添加中间件:
Router::new()
.hoop(check_authed)
.path("writers")
.get(list_writers)
.post(create_writer)
.push(
Router::with_path("{id}")
.get(show_writer)
.patch(edit_writer)
.delete(delete_writer)
.push(Router::with_path("articles").get(list_writer_articles)),
);
在这个例子, 根路由使用 check_authed 检查当前用户是否已经登录了. 所有子孙路由都会受此中间件影响.
如果用户只是浏览 writer 的信息和文章, 我们更希望他们无需登录即可浏览. 我们可以把路由定义成这个样子:
Router::new()
.push(
Router::new()
.hoop(check_authed)
.path("writers")
.post(create_writer)
.push(Router::with_path("{id}").patch(edit_writer).delete(delete_writer)),
)
.push(
Router::with_path("writers").get(list_writers).push(
Router::with_path("{id}")
.get(show_writer)
.push(Router::with_path("articles").get(list_writer_articles)),
),
);
尽管有两个路由都有相同的路径定义 path("articles"), 他们依然可以被添加到同一个父路由里.
过滤器
Router 内部都是通过过滤器来确定路由是否匹配. 过滤器支持使用 or 或者 and 做基本逻辑运算. 一个路由可以包含多个过滤器, 当所有的过滤器都匹配成功时, 路由匹配成功.
网站的路径信息是一个树状机构, 这个树状机构并不等同于组织路由的树状结构. 网站的一个路径可能对于多个路由节点. 比如, 在 articles/ 这个路径下的某些内容需要登录才可以查看, 而某些有不需要登录. 我们可以把需要登录查看的子路径组织到一个包含登录验证的中间件的路由下面. 不需要登录验证的组织到另一个没有登录验证的路由下面:
Router::new()
.push(
Router::with_path("articles")
.get(list_articles)
.push(Router::new().path("{id}").get(show_article)),
)
.push(
Router::with_path("articles")
.hoop(auth_check)
.post(list_articles)
.push(Router::new().path("{id}").patch(edit_article).delete(delete_article)),
);
路由是使用过滤器过滤请求并且发送给对应的中间件和 Handler 处理的.
path 和 method 是两个最为常用的过滤器. path 用于匹配路径信息; method 用于匹配请求的 Method, 比如: GET, POST, PATCH 等.
我们可以使用 and, or 连接路由的过滤器:
Router::with_filter(filters::path("hello").and(filters::get()));
路径过滤器
基于请求路径的过滤器是使用最频繁的. 路径过滤器中可以定义参数, 比如:
Router::with_path("articles/{id}").get(show_article);
Router::with_path("files/{**rest_path}").get(serve_file)
在 Handler 中, 可以通过 Request 对象的 get_param 函数获取:
#[handler]
pub async fn show_article(req: &mut Request) {
let article_id = req.param::<i64>("id");
}
#[handler]
pub async fn serve_file(req: &mut Request) {
let rest_path = req.param::<i64>("rest_path");
}
Method 过滤器
根据 HTTP 请求的 Method 过滤请求, 比如:
Router::new().get(show_article).patch(update_article).delete(delete_article);
这里的 get, patch, delete 都是 Method 过滤器. 实际等价于:
use salvo::routing::filter;
let mut root_router = Router::new();
let show_router = Router::with_filter(filters::get()).handle(show_article);
let update_router = Router::with_filter(filters::patch()).handle(update_article);
let delete_router = Router::with_filter(filters::get()).handle(delete_article);
Router::new().push(show_router).push(update_router).push(delete_router);
自定义 Wisp
对于某些经常出现的匹配表达式, 我们可以通过 PathFilter::register_wisp_regex 或者 PathFilter::register_wisp_builder 命名一个简短的名称. 举例来说, GUID 格式在路径中经常出现, 正常写法是每次需要匹配时都这样:
Router::with_path("/articles/<id:/[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}/>");
Router::with_path("/users/<id:/[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}/>");
每次这么都要写这复杂的正则表达式会很容易出错, 代码也不美观, 可以这么做:
use salvo::routing::filter::PathFilter;
#[tokio::main]
async fn main() {
let guid = regex::Regex::new("[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}").unwrap();
PathFilter::register_wisp_regex("guid", guid);
Router::new()
.push(Router::with_path("/articles/{id:guid}").get(show_article))
.push(Router::with_path("/users/{id:guid}").get(show_user));
}
仅仅只需要注册一次, 以后就可以直接通过 {id:guid} 这样的简单写法匹配 GUID, 简化代码的书写.
以前学习的是Controller类的web框架 如何理解Router?
路由设计的Web框架(如Salvo)与传统MVC或Controller设计框架的主要区别在于:
-
灵活性:路由设计允许更灵活地定义请求处理流程,可以精确控制每个路径的处理逻辑。例如,在Salvo中你可以直接定义特定路径的处理函数:
Router::with_path("articles").get(list_articles).post(create_article);
而在Controller设计中,通常需要先定义一个控制器类,然后在类中定义多个方法来处理不同的请求:
@Controller
public class ArticleController {
@GetMapping("/articles")
public List<Article> listArticles() { /* ... */ }
@PostMapping("/articles")
public Article createArticle(@RequestBody Article article) { /* ... */ }
}
-
中间件集成:路由框架通常提供更简洁的中间件集成方式,可以针对特定路由应用中间件。Salvo的中间件可以精确应用到特定路由:
Router::new()
.push(
Router::with_path("admin/articles")
.hoop(admin_auth_middleware) // 仅对管理员路由应用认证中间件
.get(list_all_articles)
.post(create_article),
)
.push(
Router::with_path("articles") // 公开路由无需认证
.get(list_public_articles),
);
-
代码组织:路由设计倾向于基于功能或API端点组织代码,而非按MVC的模型-视图-控制器分层。
路由设计鼓励按照API端点功能组织代码:
// user_routes.rs - 用户相关路由和处理逻辑
pub fn user_routes() -> Router {
Router::with_path("users")
.get(list_users)
.post(create_user)
.push(Router::with_path("{id}").get(get_user).delete(delete_user))
}
// article_routes.rs - 文章相关路由和处理逻辑
pub fn article_routes() -> Router {
Router::with_path("articles")
.get(list_articles)
.post(create_article)
}
// 在主应用中组合路由
let router = Router::new()
.push(user_routes())
.push(article_routes());
-
轻量级:通常路由设计更轻量,减少了框架强制的概念和约束。你可以只引入需要的组件,不必遵循严格的框架结构。
路由设计让API开发更加直观,尤其适合构建现代的微服务和RESTful API。在Salvo这样的框架中,路由是核心概念,它直接反映了API的结构和行为,使代码更易于理解和维护。相比之下,传统Controller设计往往需要更多的配置和约定来实现相同的功能。
Router 结构体方法概览