參考Laravel製作基於golang的路由包
概述
最近在開發自己的 Web 框架ofollow,noindex" target="_blank">Bingo , 也查看了一些市面上的路由工具包,但是都有些無法滿足我的需求,
例如,我希望獲得一些Laravel
框架的特性:
-
快速的路由查詢
-
動態路由支援
-
中介軟體支援
-
路由組支援
而市面上最快的就是httprouter
,這裡本來幾個月前我改造過一次:改造httprouter使其可以支援中介軟體
,但是那時是耦合在bingo
框架中的,並且中介軟體不支援攔截,在這裡我需要將其抽出來製作出一個第三方包,可以直接引用,無需依賴Bingo
框架
所以我依舊選用了httprouter
作為基礎包,將其進行改造,使其支援以上特性。
倉庫地址:bingo-router
用法在專案的README
中已經將的很清楚了,這裡不再贅述,有問題或者有什麼需求可以給我提issue
喔~
也建議先過一遍README.md
再看這篇文章,不然可能會有地方看不懂...
改造主要分為兩部分
-
第一部分是將
httprouter
的 路由樹tree
上掛載的handle
方法改為我們自定義的結構體
httprouter
的原理可以看這篇5.2 router 請求路由
簡單來講,就是把所有介面的路徑,共同構造一顆字首樹,將字首相同的路徑放在一棵樹杈中,這樣可以加速查詢速度,而每片樹葉都代表查詢到了一個路由方法,掛載的就是一個方法,
但是這樣的話這棵字首樹上就只能掛載 方法了,無法新增一些額外資訊,所以第一步就要讓字首樹上掛載一個我們自定義的結構體,讓我們可以查詢到掛載的中介軟體、路由 字首等
-
第二部分是實現中介軟體功能,如果只是 遍歷操作一箇中間件陣列,那麼無法進行一些攔截操作,
比如,我們要實現一箇中間件用來驗證使用者是否登陸 ,未登入使用者將會返回錯誤資訊,那麼如果遍歷執行一箇中間件陣列,最終還是將會執行到最終的路由
為了實現攔截功能,我參考了
Laravel
中的Pipeline
功能的實現原理,實現了一個管道物件,實現上述效果
開始改造
1. 第一部分
-
在我們的計劃中,計劃實現 路由組、中介軟體、路由字首功能,所以我們需要自定義的結構體如下:
// 路由 type Route struct { pathstring// 路徑 targetMethod TargetHandle// 要執行的方法 methodstring// 訪問型別 是get post 或者其他 namestring// 路由名 mount[]*Route// 子路由 middleware[]MiddlewareHandle // 掛載的中介軟體 prefixstring// 路由字首,該字首僅對子路由有效 }
其中的
targetMethod
就是原本掛載在字首樹的handle
方法了,我們需要把原本tree.go
檔案中的Node
結構體上掛載的handle
方法全部 改為Route
,改動較大,且沒有什麼需要特別注意的 ,就不在這裡贅述了,具體可以看
tree.go
檔案 -
在
README
中的路由註冊操作,使用的是責任鏈模式,每個方法最後都返回一個當前物件的指標,就可以實現鏈式操作 其中的Get``Post
等方法,實際上是在向Route
物件中的屬性賦值,沒什麼技術含量,感興趣可以看原始碼 -
實現路由組功能
通過路由組,我們可以給子路由設定公共的字首和中介軟體,
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
方法,讓每一個通過這種方法建立的路由都在Builder
的routes
屬性下:func (b *Builder) NewRoute() *Route { r := NewRoute() b.routes = append(b.routes, r) return r }
在建立的時候將指標放入
Builder
中即可這樣,我們所建立的多個路由 就可以巢狀在一起了,那麼如何利用
httprouter
的Handle
方法,將我們的Route
物件,注入到Router
中呢? -
將路由注入路由器
從
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!