Files
2019-06-22 23:41:28 +08:00

288 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 13.2 自訂路由器設計
## HTTP 路由
HTTP 路由元件負責將 HTTP 請求交到對應的函式處理(或者是一個 struct 的方法),如前面小節所描述的結構圖,路由在框架中相當於一個事件處理器,而這個事件包括:
- 使用者請求的路徑(path)(例如:/user/123,/article/123),當然還有查詢串資訊(例如?id=11)
- HTTP 的請求方法(method)(GET、POST、PUT、DELETE、PATCH 等)
路由器就是根據使用者請求的事件資訊轉發到相應的處理函式(控制層)。
## 預設的路由實現
在 3.4 小節有過介紹 Go 的 http 套件的詳解,裡面介紹了 Go 的 http 套件如何設計和實現路由,這裡繼續以一個例子來說明:
```Go
func fooHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
}
http.HandleFunc("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
```
上面的例子呼叫了 http 預設的 DefaultServeMux 來新增路由,需要提供兩個參數,第一個參數是希望使用者訪問此資源的 URL 路徑(儲存在 r.URL.Path),第二參數是即將要執行的函式,以提供使用者訪問的資源。路由的思路主要集中在兩點:
- 新增路由資訊
- 根據使用者請求轉發到要執行的函式
Go 預設的路由新增是透過函式`http.Handle``http.HandleFunc`等來新增,底層都是呼叫了`DefaultServeMux.Handle(pattern string, handler Handler)`,這個函式會把路由資訊儲存在一個 map 資訊中`map[string]muxEntry`,這就解決了上面說的第一點。
Go 監聽埠,然後接收到 tcp 連線會扔給 Handler 來處理,上面的例子預設 nil 即為`http.DefaultServeMux`,透過`DefaultServeMux.ServeHTTP`函式來進行排程,遍歷之前儲存的 map 路由資訊,和使用者訪問的 URL 進行匹配,以查詢對應註冊的處理函式,這樣就實現了上面所說的第二點。
```Go
for k, v := range mux.m {
if !pathMatch(k, path) {
continue
}
if h == nil || len(k) > n {
n = len(k)
h = v.h
}
}
```
## beego 框架路由實現
目前幾乎所有的 Web 應用路由實現都是基於 http 預設的路由器,但是 Go 自帶的路由器有幾個限制:
- 不支援參數設定,例如/user/:uid 這種泛型別匹配
- 無法很好的支援 REST 模式,無法限制訪問的方法,例如上面的例子中,使用者訪問/foo可以用 GET、POST、DELETE、HEAD 等方式訪問
- 一般網站的路由規則太多了,編寫繁瑣。我前面自己開發了一個 API 應用,路由規則有三十幾條,這種路由多了之後其實可以進一步簡化,透過 struct 的方法進行一種簡化
beego 框架的路由器基於上面的幾點限制考慮設計了一種 REST 方式的路由實現,路由設計也是基於上面 Go 預設設計的兩點來考慮:儲存路由和轉向路由
### 儲存路由
針對前面所說的限制點我們首先要解決參數支援就需要用到正則第二和第三點我們透過一種變通的方法來解決REST 的方法對應到 struct 的方法中去,然後路由到 struct 而不是函式,這樣在轉向路由的時候就可以根據 method 來執行不同的方法。
根據上面的思路,我們設計了兩個資料型別 controllerInfo(儲存路徑和對應的 struct這裡是一個 reflect.Type 型別)和 ControllerRegistor(routers 是一個 slice 用來儲存使用者新增的路由資訊,以及 beego 框架的應用資訊)
```Go
type controllerInfo struct {
regex *regexp.Regexp
params map[int]string
controllerType reflect.Type
}
type ControllerRegistor struct {
routers []*controllerInfo
Application *App
}
```
ControllerRegistor 對外的介面函式有
```Go
func (p *ControllerRegistor) Add(pattern string, c ControllerInterface)
```
詳細的實現如下所示:
```Go
func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) {
parts := strings.Split(pattern, "/")
j := 0
params := make(map[int]string)
for i, part := range parts {
if strings.HasPrefix(part, ":") {
expr := "([^/]+)"
//a user may choose to override the defult expression
// similar to expressjs: /user/:id([0-9]+)
if index := strings.Index(part, "("); index != -1 {
expr = part[index:]
part = part[:index]
}
params[j] = part
parts[i] = expr
j++
}
}
//recreate the url pattern, with parameters replaced
//by regular expressions. then compile the regex
pattern = strings.Join(parts, "/")
regex, regexErr := regexp.Compile(pattern)
if regexErr != nil {
//TODO add error handling here to avoid panic
panic(regexErr)
return
}
//now create the Route
t := reflect.Indirect(reflect.ValueOf(c)).Type()
route := &controllerInfo{}
route.regex = regex
route.params = params
route.controllerType = t
p.routers = append(p.routers, route)
}
```
### 靜態路由實現
上面我們實現的動態路由的實現Go 的 http 套件預設支援靜態檔案處理 FileServer由於我們實現了自訂的路由器那麼靜態檔案也需要自己設定beego 的靜態資料夾路徑儲存在全域性變數 StaticDir 中StaticDir 是一個 map 型別,實現如下:
```Go
func (app *App) SetStaticPath(url string, path string) *App {
StaticDir[url] = path
return app
}
```
應用中設定靜態路徑可以使用如下方式實現:
```Go
beego.SetStaticPath("/img","/static/img")
```
### 轉向路由
轉向路由是基於 ControllerRegistor 裡的路由資訊來進行轉發的,詳細的實現如下程式碼所示:
```Go
// AutoRoute
func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
if !RecoverPanic {
// go back to panic
panic(err)
} else {
Critical("Handler crashed with error", err)
for i := 1; ; i += 1 {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
}
Critical(file, line)
}
}
}
}()
var started bool
for prefix, staticDir := range StaticDir {
if strings.HasPrefix(r.URL.Path, prefix) {
file := staticDir + r.URL.Path[len(prefix):]
http.ServeFile(w, r, file)
started = true
return
}
}
requestPath := r.URL.Path
//find a matching Route
for _, route := range p.routers {
//check if Route pattern matches url
if !route.regex.MatchString(requestPath) {
continue
}
//get submatches (params)
matches := route.regex.FindStringSubmatch(requestPath)
//double check that the Route matches the URL pattern.
if len(matches[0]) != len(requestPath) {
continue
}
params := make(map[string]string)
if len(route.params) > 0 {
//add url parameters to the query param map
values := r.URL.Query()
for i, match := range matches[1:] {
values.Add(route.params[i], match)
params[route.params[i]] = match
}
//reassemble query params and add to RawQuery
r.URL.RawQuery = url.Values(values).Encode() + "&" + r.URL.RawQuery
//r.URL.RawQuery = url.Values(values).Encode()
}
//Invoke the request handler
vc := reflect.New(route.controllerType)
init := vc.MethodByName("Init")
in := make([]reflect.Value, 2)
ct := &Context{ResponseWriter: w, Request: r, Params: params}
in[0] = reflect.ValueOf(ct)
in[1] = reflect.ValueOf(route.controllerType.Name())
init.Call(in)
in = make([]reflect.Value, 0)
method := vc.MethodByName("Prepare")
method.Call(in)
if r.Method == "GET" {
method = vc.MethodByName("Get")
method.Call(in)
} else if r.Method == "POST" {
method = vc.MethodByName("Post")
method.Call(in)
} else if r.Method == "HEAD" {
method = vc.MethodByName("Head")
method.Call(in)
} else if r.Method == "DELETE" {
method = vc.MethodByName("Delete")
method.Call(in)
} else if r.Method == "PUT" {
method = vc.MethodByName("Put")
method.Call(in)
} else if r.Method == "PATCH" {
method = vc.MethodByName("Patch")
method.Call(in)
} else if r.Method == "OPTIONS" {
method = vc.MethodByName("Options")
method.Call(in)
}
if AutoRender {
method = vc.MethodByName("Render")
method.Call(in)
}
method = vc.MethodByName("Finish")
method.Call(in)
started = true
break
}
//if no matches to url, throw a not found exception
if started == false {
http.NotFound(w, r)
}
}
```
### 使用入門
基於這樣的路由設計之後就可以解決前面所說的三個限制點,使用的方式如下所示:
基本的使用註冊路由:
```Go
beego.BeeApp.RegisterController("/", &controllers.MainController{})
```
參數註冊:
```Go
beego.BeeApp.RegisterController("/:param", &controllers.UserController{})
```
正則匹配:
```Go
beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{})
```
## links
* [目錄](<preface.md>)
* 上一節:[專案規劃](<13.1.md>)
* 下一節:[controller 設計](<13.3.md>)