手把手和你一起實現一個Web框架實戰——EzWeb框架(三)[Go語言筆記]Go專案實戰

學習先生 發表於 2021-08-18
框架 Go

手把手和你一起實現一個Web框架實戰——EzWeb框架(三)[Go語言筆記]Go專案實戰

程式碼倉庫:
github
gitee
中文註釋,非常詳盡,可以配合食用

本篇程式碼,請選擇demo3

這一篇文章我們進行動態路由解析功能的設計,

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

實現這處理這兩類模式的簡單小功能,實現起來不簡單,原有的map[path]HandlerFunc資料結構只能儲存靜態路由與方法對應,而無法處理動態路由,我們使用一種樹結構來進行路由表的儲存。

手把手和你一起實現一個Web框架實戰——EzWeb框架(三)[Go語言筆記]Go專案實戰

一、設計這個資料結構

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")
}

手把手和你一起實現一個Web框架實戰——EzWeb框架(三)[Go語言筆記]Go專案實戰

成功!

參考:

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