用 rust 从零开发一套 web 框架:day4
元婴出窍
前面我们实现了动态路由解析,总算是把最难的一关给跨过去了。那今天在上一章的基础上,实现一些简单点的路由分组功能。
分组的意义
分组控制(Group Control)是 Web 框架应提供的基础功能之一。所谓分组,是指路由的分组。如果没有路由分组,我们需要针对每一个路由进行控制。但是真实的业务场景中,往往某一组路由需要相似的处理。例如:
以/user
开头的路由匿名可访问。
以/admin
开头的路由需要鉴权。
以/api
开头的路由是 RESTful
接口,可以对接第三方平台,需要三方平台鉴权。
大部分情况下的路由分组,是以相同的前缀来区分的。因此,我们今天实现的分组控制也是以前缀来区分,并且支持分组的嵌套。例如/user
是一个分组,/user/a
和/user/b
可以是该分组下的子分组。作用在/user
分组上的中间件(middleware),也都会作用在子分组,子分组还可以应用自己特有的中间件。
中间件可以给框架提供无限的扩展能力,应用在分组上,可以使得分组控制的收益更为明显,而不是共享相同的路由前缀这么简单。例如/admin
的分组,可以应用鉴权中间件;/分组应用日志中间件,/是默认的最顶层的分组,也就意味着给所有的路由,即整个框架增加了记录日志的能力。
提供扩展能力支持中间件的内容,我们将在下一节当中介绍。
分组嵌套
一个 Group 对象需要具备哪些属性呢?首先是前缀(prefix),比如/
,或者/admin
;要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁;当然了,按照我们一开始的分析,中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares)。还记得,我们之前调用函数 add_route()
来映射所有的路由规则和 Handler
。如果 Group
对象需要直接映射路由规则的话,比如我们想在使用框架时,这么调用:
let mut router = Router::new();
let g1 = router.group("/admin");
g1.get("/hello", |c: &mut AppContext| {
let s = format!("hello world from admin");
c.string(None, &s)
})
.get("/index", |c: &mut AppContext| {
let s = format!("hello world from index");
c.string(None, &s)
});
因为/admin
路由分组本质上其实也是路由,但是直接把整个路由 Router 结构体嵌套进去的话,似乎也不太合适。因为这样会获取数据的所有权。
所以我定义的路由分组是这样的:
///路由组,对路由进行分组控制
pub struct RouterGroup<'a> {
path: String,
router: &'a mut Router,
}
RouterGroup
只得到路由Router
的可变引用,这样就不必复制Router
的内容到RouterGroup
里面造成数据重复的多余性能开销和所有权冲突。
那么,接下来,为RouterGroup
实现一系列的路由操作:
impl<'a> RouterGroup<'a> {
fn new(path: &str, router: &'a mut Router) -> Self {
Self {
path: path.to_string(),
router,
}
}
fn add<F>(self, path: &str, method: Method, handler: F) -> Self
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
let mut join_path = String::new();
if self.path.as_str() == "/" {
join_path.push_str(path);
} else if path == "/" {
join_path.push_str(self.path.as_str());
} else {
join_path = format!("{}{}", self.path, path)
}
self.router.add(&join_path, method, handler);
self
}
/// 封装各类请求
pub fn get<F>(self, path: &str, handler: F) -> Self
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
self.add(path, Method::GET, handler)
}
pub fn post<F>(self, path: &str, handler: F) -> Self
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
self.add(path, Method::POST, handler)
}
pub fn put<F>(self, path: &str, handler: F) -> Self
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
self.add(path, Method::PUT, handler)
}
pub fn patch<F>(self, path: &str, handler: F) -> Self
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
self.add(path, Method::PATCH, handler)
}
pub fn delete<F>(self, path: &str, handler: F) -> Self
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
self.add(path, Method::DELETE, handler)
}
}
封装好路由的增删改查操作和RouterGroup
的初始化。注意:这里的方法,都是传入self
作为参数,和之前的Router
实现的增删改查一样,有兴趣的同学可以思考一下能否改为 &mut self
或者mut self
。改动之后,又会有什么样的变化。
另外,细心的同学可能发现了,router
中需要新增一个分组group
方法。
同时新增路由的方法似乎变为了add
,我们来看一下Router
实现的add
函数和之前的add_router
有何区别:
impl Router {
...
/// 路由分组
pub fn group(&mut self, path: &str) -> RouterGroup {
RouterGroup::new(path, self)
}
//分组新增路由
fn add<F>(&mut self, path: &str, method: Method, handler: F)
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
let key = format!("{}+{}", method.as_ref(), path);
self.handlers.insert(key, Box::new(handler));
let parts = parse_pattern(path);
self.roots.insert(method.clone(), path, parts.clone(), 0);
}
//新增路由
fn add_route<F>(mut self, path: &str, method: Method, handler: F) -> Router
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
let key = format!("{}+{}", method.as_ref(), path);
self.handlers.insert(key, Box::new(handler));
let parts = parse_pattern(path);
self.roots.insert(method.clone(), path, parts.clone(), 0);
self
}
}
不看不知道,一看吓一跳。这不是一模一样的函数吗?没错,这两个函数几乎就就一样的。但是吧,只有一个地方不一样,就是一个入参是mut self
,另一个是&mut self
。可别小看这个&
,这里面区别可大了。一个拥有 self
的所有权,一个是借用。如果你想偷懒,把二者合并为一。那你就准备接受编译器的毒打吧 😄。
当然了,同样可以想办法压缩一下冗余代码:
impl Router {
...
//新增路由
fn add_route<F>(mut self, path: &str, method: Method, handler: F) -> Router
where
F: Fn(&mut AppContext) + Send + Sync + 'static,
{
self.add(path, method, handler);
self
}
}
接下来,进行测试一下效果,在 main 函数写上分组的group
:
#[tokio::main]
async fn main() {
let handle_hello = |c: &mut AppContext| c.string(None, "hello world from handler");
let mut router = Router::new().get("/user", handle_hello).get("/ghost/:id", handle_hello);
let g1 = router.group("/admin");
g1.get("/hello", |c: &mut AppContext| {
let s = format!("hello world from admin");
c.string(None, &s)
})
.get("/index", |c: &mut AppContext| {
let s = format!("hello world from index");
c.string(None, &s)
});
// Run the server like above...
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
server::run(addr, router).await;
}
然后在浏览器中分别打开:http://localhost:3000/admin/index
和http://localhost:3000/admin/hello
同时,查看终端生成的路由节点树:
当前节点:RouterNode {
pattern: None,
part: None,
children: [
RouterNode {
pattern: Some(
"/user",
),
part: None,
children: [],
is_match: false,
hooks: [],
method: Some(
"GET",
),
},
RouterNode {
pattern: Some(
"/ghost",
),
part: None,
children: [
RouterNode {
pattern: Some(
"/ghost/:id",
),
part: Some(
":id",
),
children: [],
is_match: true,
hooks: [],
method: Some(
"GET",
),
},
],
is_match: false,
hooks: [],
method: None,
},
RouterNode {
pattern: Some(
"/admin",
),
part: None,
children: [
RouterNode {
pattern: Some(
"/admin/hello",
),
part: None,
children: [],
is_match: false,
hooks: [],
method: Some(
"GET",
),
},
RouterNode {
pattern: Some(
"/admin/index",
),
part: None,
children: [],
is_match: false,
hooks: [],
method: Some(
"GET",
),
},
],
is_match: false,
hooks: [],
method: None,
},
],
is_match: false,
hooks: [],
method: None,
}
现在加上路由分组,我们在路由控制上面更近一步。当然,这一章可以说是相当简单的,毕竟难度都在前面的路由节点树上面了,路由分组功能只不过是锦上添花而已。
转载自:https://juejin.cn/post/7173957168498999309