參考Laravel製作基於golang的路由包

silsuer在掘金發表於2018-10-27

概述

最近在開發自己的 Web 框架 Bingo, 也檢視了一些市面上的路由工具包,但是都有些無法滿足我的需求,

例如,我希望獲得一些 Laravel 框架的特性:

  • 快速的路由查詢

  • 動態路由支援

  • 中介軟體支援

  • 路由組支援

而市面上最快的就是 httprouter ,這裡本來幾個月前我改造過一次: 改造httprouter使其可以支援中介軟體,但是那時是耦合在bingo框架中的,並且中介軟體不支援攔截,在這裡我需要將其抽出來製作出一個第三方包,可以直接引用,無需依賴 Bingo 框架

所以我依舊選用了 httprouter 作為基礎包,將其進行改造,使其支援以上特性。

倉庫地址: bingo-router

用法在專案的 README 中已經將的很清楚了,這裡不再贅述,有問題或者有什麼需求可以給我提 issue 喔~

也建議先過一遍 README.md 再看這篇文章,不然可能會有地方看不懂...

改造主要分為兩部分

  1. 第一部分是將 httprouter 的 路由樹tree上掛載的 handle方法改為我們自定義的結構體

httprouter 的原理可以看這篇 5.2 router 請求路由

簡單來講,就是把所有介面的路徑,共同構造一顆字首樹,將字首相同的路徑放在一棵樹杈中,這樣可以加速查詢速度,而每片樹葉都代表查詢到了一個路由方法,掛載的就是一個方法,

但是這樣的話這棵字首樹上就只能掛載 方法了,無法新增一些額外資訊,所以第一步就要讓字首樹上掛載一個我們自定義的結構體,讓我們可以查詢到掛載的中介軟體、路由 字首等

  1. 第二部分是實現中介軟體功能,如果只是 遍歷操作一箇中介軟體陣列,那麼無法進行一些攔截操作,

    比如,我們要實現一箇中介軟體用來驗證使用者是否登陸 ,未登入使用者將會返回錯誤資訊,那麼如果遍歷執行一箇中介軟體陣列,最終還是將會執行到最終的路由

    為了實現攔截功能,我參考了 Laravel中的 Pipeline 功能的實現原理,實現了一個管道物件,實現上述效果

開始改造

1. 第一部分

  1. 在我們的計劃中,計劃實現 路由組、中介軟體、路由字首功能,所以我們需要自定義的結構體如下:

    
       // 路由
       type Route struct {
       	path         string             // 路徑
           targetMethod TargetHandle       // 要執行的方法
           method       string             // 訪問型別 是get post 或者其他
           name         string             // 路由名
           mount        []*Route           // 子路由
           middleware   []MiddlewareHandle // 掛載的中介軟體
           prefix       string             // 路由字首,該字首僅對子路由有效
       }
    
    複製程式碼

    其中的 targetMethod 就是原本掛載在字首樹的handle 方法了,我們需要把原本 tree.go 檔案中的 Node 結構體上掛載的 handle 方法全部 改為 Route,

    改動較大,且沒有什麼需要特別注意的 ,就不在這裡贅述了,具體可以看 tree.go 檔案

  2. README 中的路由註冊操作,使用的是責任鏈模式,每個方法最後都返回一個當前物件的指標,就可以實現鏈式操作 其中的 Get``Post 等方法,實際上是在向Route物件中的屬性賦值,沒什麼技術含量,感興趣可以看原始碼

  3. 實現路由組功能

    通過路由組,我們可以給子路由設定公共的字首和中介軟體,Laravel 中是讓路由成組來做的,多個路由組成了一個組物件,而這裡 ,我直接用了子路由的方式,將組物件也變成了一個普通路由,組物件下 的路由就是當前路由的子路由

    寫一個Mount() 方法,讓路由新增子路由:

      // 掛載子路由,這裡只是將回撥中的路由放入
      func (r *Route) Mount(rr func(b *Builder)) *Route {
      	builder := new(Builder)
      	rr(builder)
      	// 遍歷這個路由下建立的所有子路由,將路由放入父路由上
      	for _, route := range builder.routes {
      		r.mount = append(r.mount, route)
      	}
      	return r
      }
    複製程式碼

    其中的 Builder 中包含了一個路由陣列,通過建造者模式,給Builder一個 NewRoute 方法,讓每一個通過這種方法建立的路由都在Builderroutes屬性下:

      func (b *Builder) NewRoute() *Route {
      	r := NewRoute()
      	b.routes = append(b.routes, r)
      	return r
      }
    複製程式碼

    在建立的時候將指標放入 Builder 中即可

    這樣,我們所建立的多個路由 就可以巢狀在一起了,那麼如何利用 httprouterHandle 方法,將我們的 Route 物件,注入到Router 中呢?

  4. 將路由注入路由器

    httprouter 原始碼可以看出,無論是 Get,Post還是其他的方法,最終都是呼叫了 router.Handle() 方法,傳入訪問方式,路徑,和對應的方法,我們剛剛已經把對應的方法改為了路由

    所以這裡就傳入 訪問方式,路徑,和路由物件,並且在注入的時候,讓中介軟體和路由字首等都生效

    編寫一個注入的方法Mount:

```go

  var prefix []string // 當前路由字首,每經過一層,字首就會增加一個,最終將陣列中的字串連線起來就是最後的字首了
  var middlewares map[string][]MiddlewareHandle  // 中介軟體,key標識了這是第幾層路由的中介軟體,值就是對應的中介軟體陣列了
  var currentPointer int // 當前是第幾層路由

  // 掛載方法可以一次性傳入多個路由物件
  func (r *Router) Mount(routes ...*Route) {
  	prefix = []string{}
  	middlewares = make(map[string][]MiddlewareHandle)
  	for _, route := range routes {
  	    // 掛載單個路由
  		r.MountRoute(route)
  	}

  }


// 向其中掛載路由
func (r *Router) MountRoute(route *Route) {

    // 將當前路徑的中介軟體放入集合中
    setMiddlewares(currentPointer, route)

    // 當前路徑是所有字首陣列連線在一起,加上當前路由的path
    p := getPrefix(currentPointer) + route.path

    // 如果一個路由設定了字首,則這個字首會作用在所有的子路由上
    prefix = append(prefix, route.prefix)


    if route.method != "" && p != "" {
        r.Handle(route.method, p, route)  // 路由有效,注入路由器 Router中
    }

    // 如果路由有子路由,則將子路由掛載進去,如果沒有,
    if len(route.mount) > 0 {
        for _, subRoute := range route.mount {
            currentPointer += 1 // 新增一層,進入下一層路由
            r.MountRoute(subRoute)
        }
    } else {
        if currentPointer > 0 {
            currentPointer -= 1 // 減小一層,退回上一層路由
        }
    }

}

// 根據當前是第幾層路由,獲取字首
func getPrefix(current int) string {
    if len(prefix) > current-1 && len(prefix) != 0 {
        return strings.Join(prefix[:current], "")
    }
    return ""
}

// 設定中介軟體,根據當前是第x層路由,將前面的路由放入當前路由中
func setMiddlewares(current int, route *Route) {
    key := "p" + strconv.Itoa(currentPointer)
    for _, v := range route.middleware {
        middlewares[key] = append(middlewares[key], v)
    }

    // 將當前路由的父路由的都放入當前路由中
    for i := 0; i < currentPointer; i++ {
        key = "p" + strconv.Itoa(i)
        if list, ok := middlewares[key]; ok {
            for _, v := range list {
                route.middleware = append(route.middleware, v)
            }
        }
    }
}
```
複製程式碼

首先定義全域性變數 :

  • prefix 記錄每層路由的字首,鍵就是路由層數,值就是路由字首

  • middlewares 記錄每層路由中介軟體,鍵可以標識路由層數,值就是該層中介軟體的所有集合

  • currentPointer 標識當前處在第幾層路由,通過它從上面的兩個變數中取出屬於當前路由層的資料

然後每遍歷一次,就把對應字首和中介軟體組存入全域性變數中,遞迴呼叫,再取出合適的資料,最終執行 Handle 方法注入路由器中

上面只是簡略的介紹了一下如何製作,具體可以直接看程式碼,沒有難點。

2. 第二部分

我們構建的server,都要實現ServeHttp 方法,這樣當請求進來的時候,就會走到我們定義的這個方法中,原本的 httprouter 所定義的ServeHttp可以在這裡看到

過程就是將當前的URL,沿著字首樹尋找樹葉,找到後直接執行,而我們上面將樹葉更改成了Route結構體,這樣當尋找到的時候,需要先執行它的中介軟體,再執行它的 targetMethod方法

而這裡的中介軟體,我們不能直接使用 for 迴圈去遍歷執行,因為這樣不能攔截請求,最終都會走到targetMethod中,並且沒有後置效果,那麼如何製作這種功能呢?

laravel 中用到了一種 Pipeline 的方法,也就是管道,讓每一個 context 順序經過每一箇中介軟體,如果被攔截,則不往下傳遞

具體思路可以看這裡

我實現的原始碼在這裡

下面使用程式碼實現:

我們期待的效果是這樣:

      	// 建立管道,執行中介軟體最終到達路由
      	new(Pipeline).Send(context).Through(route.middleware).Then(func(context *Context) {
      	    route.targetMethod(context)
      	})
複製程式碼

首先建立一個管道結構體:

    type Pipeline struct {
    	send    *Context           // 穿過管道的上下文
    	through []MiddlewareHandle // 中介軟體陣列
    	current int                // 當前執行到第幾個中介軟體
    }
複製程式碼

Send(),Through() 方法都是向其中注入內容的,這裡就不多說了

主要是 Then 方法:

    // 這裡是路由的最後一站
    func (p *Pipeline) Then(then func(context *Context)) {
    	// 按照順序執行
    	// 將then作為最後一站的中介軟體
    	var m MiddlewareHandle
    	m = func(c *Context, next func(c *Context)) {
    		then(c)
    		next(c)
    	}
    	p.through = append(p.through, m)
    	p.Exec()
    }

複製程式碼

then 方法將最終要執行的那個方法也封裝成了一箇中介軟體,加入了管道的最後,然後執行 Exec 方法,開始從頭讓 send 中的物件穿過管道:


  func (p *Pipeline) Exec() {
      if len(p.through) > p.current {
          m := p.through[p.current]
          p.current += 1
          m(p.send, func(c *Context) {
              p.Exec()
          })
      }

  }
複製程式碼

取出當前指標指向的那個中介軟體,將當前指標移動到下一個中介軟體,並且執行剛剛取出的中介軟體,在其中傳入的回撥next,就是遞迴執行這個邏輯,執行下一個中介軟體,

這樣在我們的程式碼中就可以通過 next() 方法的位置,來控制是前置中介軟體還是後置中介軟體了

程式碼不多,但是實現的效果很有趣,感謝 Laravel

我只是重寫了一部分他人的東西,感謝開源,受益匪淺,另外 掛一下自己的 web 框架 Bingo ,求 star,歡迎 PR!

相關文章