侧边栏壁纸
博主头像
hanlibaby博主等级

念念不忘,必有回响

  • 累计撰写 59 篇文章
  • 累计创建 92 个标签
  • 累计收到 20 条评论

Golang Web(四) 动态路由

hanlibaby
2022-05-12 / 0 评论 / 0 点赞 / 17 阅读 / 3,929 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-05-12,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

在上周五一期间答辩结束啦🎉🎉🎉

前言

上一篇文章中使用Trie 树来保存路由信息,实现了路由的插入以及查询

目标

  • 完成动态路由:
    1. 参数匹配:例如:/user/{userId} 匹配 /user/xxx 其中 xxx 表示任意多个英文字母和数字组成的字符串,或者可以给 userId 加个限制:比如 /user/{userId:[\d]+} ,这样就只能匹配数字
    2. 通配符 * 匹配:例如:/post/* 可以匹配以 /post 为前缀的任何路由

实现

实现动态路由的过程中,并没有对原来的代码改动太大。

1、为树节点添加一个是否是叶节点的标记:

// isLeaf is it a leaf node
isLeaf bool

2、在 insert 函数的最后加上一句:

cur.isLeaf = true

表示当前节点是叶子节点

3、在 Trie 树中,主要改变的是查询函数,由于加入了动态路由,那么最后待匹配的路由可能就不止一个了,所以查询函数我们修改为返回一个树节点的切片。

新增一个 regex 参数,表示是否是正则匹配。

代码如下:

// search return a match node from trie
func (t *Trie) search(pattern string, regex bool) (candidates []*Node) {
	cur := t.root

	if pattern == cur.path {
		candidates = append(candidates, cur)
		return
	}

	if !regex {
		pattern = trimPathPrefix(pattern)
	}

	routes := splitPattern(pattern)

	for _, route := range routes {
		child, ok := cur.children[route]
		if !ok && regex {
			break
		}

		if !ok && !regex {
			return
		}

		if child.path == pattern && !regex {
			candidates = append(candidates, child)
			return
		}
		cur = child
	}

	return cur.searchLeaf()
}

若未查询到对应的路由,在代码的最后一行我们返回当前树中所有的叶子节点,用于进行正则匹配。对应的实现如下:

func (n *Node) searchLeaf() (leafs []*Node) {
	que := []*Node{n}
	for len(que) > 0 {
		var tmpQue []*Node
		for _, node := range que {
			if node.isLeaf {
				leafs = append(leafs, node)
			}
			for _, child := range node.children {
				tmpQue = append(tmpQue, child)
			}
		}
		que = tmpQue
	}
	return
}

在这个函数中,通过遍历每层节点,将树节点加入结果中返回

4、在 router 实现中,主要变动在 Handle 函数

// Handle find the corresponding routing node to process the request
func (r *Router) Handle(ctx *context.Context) {
	var (
		requestURL = ctx.Request.URL.Path
		method     = ctx.Request.Method
	)

	nodes := r.trees[method].search(requestURL, false)

	if len(nodes) > 0 {
		node := nodes[0]
		if node.handle != nil {
			if node.path == requestURL || node.path == requestURL[1:] {
				node.handle(ctx)
			}
		}
	} else {
		routes := strings.Split(requestURL, "/")
		prefix := routes[1]
		nodes = r.trees[method].search(prefix, true)
		for _, node := range nodes {
			if handler := node.handle; handler != nil && node.path != requestURL {
				if matchParams, match := r.matchAndParse(requestURL, node.path); match {
					ctx.WithValue(matchParams)
					handler(ctx)
					return
				} else {
					log.Printf("Can't match requestURL: %4s, route to match: %4s", requestURL, node.path)
				}
			}
		}
	}
}

首先先直接匹配路由,若能匹配到则无需进行正则匹配,若不能匹配到,则以请求路由的前缀去树中查询,获取该前缀下所有树节点。

之后遍历查询到的所有节点,通过判断请求路由和节点保存的全路由是否匹配来进行处理。

匹配函数如下:

// matchAndParse match router and parse params
func (r *Router) matchAndParse(requestURL, path string) (matchParams pathVariableMapType, match bool) {
	var (
		matchName []string
		pattern   string
	)

	match = true
	matchParams = make(pathVariableMapType)

	routes := strings.Split(path, "/")
	for _, route := range routes {
		if route == "" {
			continue
		}

		routeLen := len(route)
		firstLetter, lastLetter := string(route[0]), string(route[routeLen-1])
		if firstLetter == "{" && lastLetter == "}" {
			matchStr := route[1 : routeLen-1]
			res := strings.Split(matchStr, ":")
			if len(res) == 1 {
				pattern += "/(" + defaultPattern + ")"
			} else {
				pattern += "/(" + res[1] + ")"
			}
			matchName = append(matchName, res[0])
		} else if firstLetter == ":" {
			matchStr := route
			res := strings.Split(matchStr, ":")
			matchName = append(matchName, res[1])
			if strings.Contains(strings.ToLower(res[1]), idKey) {
				pattern += "/(" + idPattern + ")"
			} else {
				pattern += "/(" + defaultPattern + ")"
			}
		} else if firstLetter == "*" {
			pattern += "/(.*)"
		} else {
			pattern += "/" + route
		}
	}

	compile := regexp.MustCompile(pattern)
	if subMatch := compile.FindSubmatch([]byte(requestURL)); subMatch != nil {
		if string(subMatch[0]) == requestURL {
			if len(matchName) > 0 {
				for k, v := range subMatch[1:] {
					matchParams[matchName[k]] = string(v)
				}
			}
			return
		}
	}

	return nil, false
}

函数实现了最开始提到的参数匹配、通配符匹配,以及将请求路由中的参数值进行处理保存至上下文中。

4、上下文中新增保存 keyvalue 键值对的 map

type (
	M map[string]any

	Context struct {
		// request path
		Path string
		// request method
		Method string
		// response code
		StatusCode int

		Writer  http.ResponseWriter
		Request *http.Request

		// context key value map
		valueCtx M
	}
)
func (c *Context) WithValue(valueMap M) {
	for k, v := range valueMap {
		c.valueCtx[k] = v
	}
}

func (c *Context) PathVariable(key string) any {
	if value, ok := c.valueCtx[key]; ok {
		return value
	}
	return nil
}

提供了一个保存变量值的 WithValue 函数,以及获取请求路径参数值的函数。

总结

至此,实现了动态路由以后,到目前所写的内容已经完全足够使用了,后续有需要的话,我们还可以添加分组控制、中间件等等功能。

代码:

0

评论区