在上周五一期间答辩结束啦🎉🎉🎉
前言
上一篇文章中使用Trie
树来保存路由信息,实现了路由的插入以及查询
目标
- 完成动态路由:
- 参数匹配:例如:
/user/{userId}
匹配/user/xxx
其中xxx
表示任意多个英文字母和数字组成的字符串,或者可以给userId
加个限制:比如/user/{userId:[\d]+}
,这样就只能匹配数字 - 通配符
*
匹配:例如:/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、上下文中新增保存 key
、value
键值对的 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
函数,以及获取请求路径参数值的函数。
总结
至此,实现了动态路由以后,到目前所写的内容已经完全足够使用了,后续有需要的话,我们还可以添加分组控制、中间件等等功能。
代码:
评论区