手搓 Web 框架 -- #3 实现一棵路由树
初步设计
- 设计一颗多叉树。
- 同时按照 HTTP 方法来组织路由树,每个 HTTP 方法一棵树
- 每个节点维持住自己的子节点
全静态匹配
我们利用全静态匹配来构建路由树,后面再考虑重构路由树以支持通配符匹配、参数路由等复杂匹配。
所谓的静态匹配,就是路径的每一段都必须严格相等。
接口设计
关键类型:
- router:维持住了所有的路由树,它是整个路由注册和查找的总入口。router 里面维护了一个map,是按照 HTTP 方法来组织路由树的
type router struct {
// trees 是按照 HTTP 方法来组织的
// 如 GET => *node
trees map[string]*node
}
- node:代表的是节点。它里面有一个 children 的 map 结构,使用 map 结构是为了快速查找到子节点
type node struct {
path string
// children 子节点
// 子节点的 path => node
children map[string]*node
// handler 命中路由之后执行的逻辑
handler HandleFunc
}
TDD 起步
在有了类型定义之后,我们就可以考虑按照 TDD 的思路,用测试来驱动我们的实现。
注册路由的测试如下。
func Test_router_AddRoute(t *testing.T) {
testRoutes := []struct {
method string
path string
}{
{
method: http.MethodGet,
path: "/",
},
}
mockHandler := func(ctx *Context) {}
r := newRouter()
for _, tr := range testRoutes {
r.addRoute(tr.method, tr.path, mockHandler)
}
...
}
我们使用简化版的TDD,即:
- 定义 API
- 定义测试
- 添加测试用例
- 实现,并且确保实现能够通过测试用例
- 重复3-4直到考虑了所有的场景
- 重复步骤 1-5
接口实现
注册路由
// addRoute 注册路由。
// method 是 HTTP 方法
// path 必须以 / 开始并且结尾不能有 /,中间也不允许有连续的 /
func (r *router) addRoute(method string, path string, handler HandleFunc) {
if path == "" {
panic("web: 路由是空字符串")
}
if path[0] != '/' {
panic("web: 路由必须以 / 开头")
}
if path != "/" && path[len(path)-1] == '/' {
panic("web: 路由不能以 / 结尾")
}
root, ok := r.trees[method]
// 这是一个全新的 HTTP 方法,创建根节点
if !ok {
// 创建根节点
root = &node{path: "/"}
r.trees[method] = root
}
if path == "/" {
if root.handler != nil {
panic("web: 路由冲突[/]")
}
root.handler = handler
return
}
segs := strings.Split(path[1:], "/")
// 开始一段段处理
for _, s := range segs {
if s == "" {
panic(fmt.Sprintf("web: 非法路由。不允许使用 //a/b, /a//b 之类的路由, [%s]", path))
}
root = root.childOrCreate(s)
}
if root.handler != nil {
panic(fmt.Sprintf("web: 路由冲突[%s]", path))
}
root.handler = handler
}
// childOrCreate 查找子节点,如果子节点不存在就创建一个
// 并且将子节点放回去了 children 中
func (n *node) childOrCreate(path string) *node {
if n.children == nil {
n.children = make(map[string]*node)
}
child, ok := n.children[path]
if !ok {
child = &node{path: path}
n.children[path] = child
}
return child
}
查找路由
// findRoute 查找对应的节点
// 注意,返回的 node 内部 HandleFunc 不为 nil 才算是注册了路由
func (r *router) findRoute(method string, path string) (*node, bool) {
root, ok := r.trees[method]
if !ok {
return nil, false
}
if path == "/" {
return root, true
}
segs := strings.Split(strings.Trim(path, "/"), "/")
for _, s := range segs {
root, ok = root.childOf(s)
if !ok {
return nil, false
}
}
return root, true
}
func (n *node) childOf(path string) (*node, bool) {
if n.children == nil {
return nil, false
}
res, ok := n.children[path]
return res, ok
}
整合
func (s *HTTPServer) Get(path string, handler HandleFunc) {
s.addRoute(http.MethodGet, path, handler)
}
func (s *HTTPServer) serve(ctx *Context) {
n, ok := s.findRoute(ctx.Req.Method, ctx.Req.URL.Path)
if !ok || n.handler == nil {
ctx.Resp.WriteHeader(404)
ctx.Resp.Write([]byte("Not Found"))
return
}
n.handler(ctx)
}
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。