手把手和你一起实现一个Web框架实战——EzWeb框架(三)[Go语言笔记]Go项目实战

手把手和你一起实现一个Web框架实战——EzWeb框架(三)[Go语言笔记]Go项目实战

手把手和你一起实现一个Web框架实战——EzWeb框架(三)[Go语言笔记]Go项目实战

代码仓库:
github
gitee
中文注释,非常详尽,可以配合食用

本篇代码,请选择demo3

这一篇文章我们进行动态路由解析功能的设计,

如xxx/:id/xxx,xxx/xxx/*mrxuexi.md

实现这处理这两类模式的简单小功能,实现起来不简单,原有的map[path]HandlerFunc数据结构只能存储静态路由与方法对应,而无法处理动态路由,我们使用一种树结构来进行路由表的存储。

一、设计这个数据结构

1、节点结构体设计

type node struct {
	path  string 	/* 需要匹配的整体路由 */
	part     string 	/* 路由中的一部分,例如 :lang */
	children []*node 	/* 存储子节点们 */
	isBlurry   bool 	/* 如果模糊匹配则为true */
}

2、一个传入part后,通过遍历该节点的全部子节点们,找到拥有相同part的子节点的方法(返回首个)

func (n *node) matchChild(part string) *node {
   //遍历子节点们,对比子节点的part和part是否相同,是或者遍历到的子节点支持模糊匹配则返回该子节点
   for _, child := range n.children {
      if child.part == part || child.isBlurry {
         return child
      }
   }
   return nil
}

3、一个返回匹配的子节点们的方法(返回全部,包括动态路由的存储的部分)

func (n *node) matchChildren(part string) []*node {
   nodes := make([]*node, 0)
   //遍历选择满足条件的子节点,加入到nodes中,然后返回
   for _, child := range n.children {
      if child.part == part || child.isBlurry {
         nodes = append(nodes, child)
      }
   }
   return nodes
}

4、构造路由表的插入方法,parts[]存储的是根据路由path分解出来的part们,我们拿到part则取检索子节点是否存在这个part,不存在则新建一个子节点,不停的在这个树上深入,直到遍历完我们的全部part,然后递归返回。

//插入方法,用一个递归实现,找匹配的路径直到找不到匹配当前part的节点,新建
func (n *node) insert(path string, parts []string, height int)  {
   //如果遍历到底部了,则将我们的path存入节点,开始返回。递归的归来条件。
   if len(parts) == height{
      n.path = path
      return
   }
   //获取这一节的part,并进行搜索
   part := parts[height]
   child := n.matchChild(part)

   //若没有搜索到匹配的子节点,则根据目前的part构造一个子节点
   if child == nil {
       child = &node{
         part: part,
         isBlurry: part[0] == ":" || part[0] == "*",
       }
       n.children = append(n.children, child)
   }
   child.insert(path, parts, height+1)
}

5、我们带着part们一个个在存储路由表的树中查找,我们拿到某个节点的全部子节点,找到满足part相同或者isBlurry:true的节点。通过递归再往深处挖,挖下去直到发现某一级节点的子节点们,没有对应匹配的part,又返回来,再去上一层的子节点看,这就是一个深度优先遍历的情况。

//搜索方法
func (n *node) search(parts []string, height int) *node {
	//如果节点到头,或者存在*前缀的节点,开始返回
	if len(parts) == height || strings.HasPrefix(n.part,"*") {
		//如果此时遍历到的n没有存储对应的path,说明未到目标最底层,则返回空
		if n.path == "" {
			return nil
		}
		return n
	}
	//搜索找到满足part的子节点们放入children
	part := parts[height]
	children := n.matchChildren(part)
	//接着遍历子节点们,递归调用获得下一级的子节点们,要走到头的同时,找到了对应的节点,才返回最终我们找到的result
	//这里为什么要遍历子节点们进行深入搜索,因为它还存在满足isBlurry:true的节点,我们也需要在其中深入搜索。
	for _, child := range children {
		result := child.search(parts, height+1)
		if result != nil {
			//返回满足要求的节点
			return result
		}
	}
	return nil
}

二、更新路由表的存储结构和处理方法

1、其中roots中的第一层是roots[method]*node

type router struct {
   //用于存储相关方法
   handlers map[string]HandlerFunc
   //用于存储每种请求方式的树的根节点
   roots map[string]*node
}

2、设计一个parsePath方法,对外部传入的路由根据"/"进行分割,存入parts

// parsePath 用于处理传入的url,先将其分开存储到parts中,当然出现*前缀的部分就可以结束
func parsePath(path string) []string {
   vs := strings.Split(path, "/")
   parts := make([]string, 0)
   for _, v := range vs {
      if v != "" {
         parts = append(parts, v)
         if v[0] == "*" {
            break
         }
      }
   }
   return parts
}

3、routeraddRoute 方法,在 handlers map[string]HandlerFunc 中存入路由对应处理方法,进行路由注册。存入形式为例如:{ “GET-/index” : 定义的处理方法 }

注意这里的path使我们用来构造路由表要存入的目标path

// router 中 addRoute 方法,在 handlers map[string]HandlerFunc 中存入路由对应处理方法
//存入形式为例如:{ "GET-/index" : 定义的处理方法 }
func (r *router) addRoute(method string, path string, handler HandlerFunc)  {
	parts := parsePath(path)
	log.Printf("Route %4s  -  %s",method,path)
	key := method + "-" + path
	_, ok := r.roots[method]
	//roots中不存在对应的方法入口则注册相应方法入口
	if !ok {
		r.roots[method] = &node{}
	}
	//调用路由表插入方法,在该数据结构中插入该路由
	r.roots[method].insert(path, parts, 0)
	//把method-path作为key,以及handler方法作为value注入数据结构
	r.handlers[key] = handler
}

4、做一个getRoute方法,进入到对应路由树,找到我们的路由,通过哈希表存入处理动态路由拿到param和找到的*node一起返回。

注意代码中的n.path是我们注册在路由表中的路由,path是外部传入的!

func (r *router) getRoute(method string, path string) (*node, map[string]string) {
   searchParts := parsePath(path)
   params := make(map[string]string)
   root, ok := r.roots[method]
   if !ok {
      return nil, nil
   }
   n := root.search(searchParts, 0)//传入全部路径的字符串数组,寻找到最后对应节点

   if n != nil {
      parts := parsePath(n.path) //n.path包含了完整的路由

      for i, part := range parts {//遍历这一条路径
         //拿到:的参数,存入params,方法中的part作为key,外面传入的path中的数据作为value存入
         if part[0] == ":" {
            params[part[1:]] = searchParts[i]
         }
         //拿到*,此时路由表中的存入的part作为key,外面传入的path中的数据作为value传入params,之后也再没有了
         if part[0] == "*" && len(part) > 1{
            params[part[1:]] = strings.Join(searchParts[i:],"/")
            break
         }
      }
      return n, params
   }
   return nil, nil
}

5.同时我们的hanle方法和上一篇文章不同的是,不是直接拿外部传入的path直接在 handlers map[string]HandlerFunc找对应的方法,因为我们外部传入的path是动态的。我们是先通过getRoute方法拿到参数和对应的找到存储节点,用这个节点中存储的path(它是静态的,是我们之前注入的),再在 handlers map[string]HandlerFunc找到对应的方法。

//根据context中存储的 c.Method 和 c.Path 拿到对应的处理方法,进行执行,如果拿到的路由没有注册,则返回404
func (r *router) handle(c *Context)  {
   //获取匹配到的节点,同时也拿到两类动态路由中参数
   n, params := r.getRoute(c.Method, c.Path)
   if n != nil {
      c.Params = params
      //拿目的节点中的path做key来找handlers
      key := c.Method + "-" + n.path
      r.handlers[key](c)
   }else {
      c.String(http.StatusNotFound,"404 NOT FOUND")
   }
}

三、Context变更

1、修改Context结构体,构造Params来存放处理动态路由拿到的参数

// Context 结构体,内部封装了 http.ResponseWriter, *http.Request
type Context struct {
   Writer http.ResponseWriter
   Req *http.Request
   //请求的信息,包括路由和方法
   Path string
   Method string
   Params map[string]string /*用于存储外面拿到的参数 ":xxx" or "*xxx" */
   //响应的状态码
   StatusCode int
}

2、设计Param方法,拿到处理动态路由的获取参数

// Param 是c的Param的value的获取方法
func (c *Context) Param(key string) string {
   value, _ := c.Params[key]
   return value
}

随便做个测试:

/*
@Time : 2021/8/16 下午4:01
@Author : mrxuexi
@File : main
@Software: GoLand
*/
package main

import (
	"Ez"
	"net/http"
)
func main() {
	r := Ez.New()

	r.POST("/hello/:id/*filepath", func(c *Ez.Context) {
		c.JSON(http.StatusOK,Ez.H{
			"name" : c.PostForm("name"),
			"age" : c.PostForm("age"),
			"id" : c.Param("id"),
			"filepath" : c.Param("filepath"),
		})
	})


	r.Run(":9090")
}

成功!

参考:

[1]: https://github.com/geektutu/7days-golang/tree/master/gee-web “”gee””
[2]: https://github.com/gin-gonic/gin “”gin””

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » 手把手和你一起实现一个Web框架实战——EzWeb框架(三)[Go语言笔记]Go项目实战