Files
build-web-application-with-…/zh-tw/13.2.md
2019-02-26 01:40:54 +08:00

277 lines
9.1 KiB
Markdown
Raw 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>)