概述
最近在開發自己的 Web 框架 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 { 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
檔案 -
在
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!