用 rust 从零开发一套 web 框架:day4

3,077 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情

元婴出窍

前面我们实现了动态路由解析,总算是把最难的一关给跨过去了。那今天在上一章的基础上,实现一些简单点的路由分组功能。

分组的意义

分组控制(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/indexhttp://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,
}

现在加上路由分组,我们在路由控制上面更近一步。当然,这一章可以说是相当简单的,毕竟难度都在前面的路由节点树上面了,路由分组功能只不过是锦上添花而已。