|
1 |
| -# 13.3 自定义路由器设计 |
| 1 | +# 13.3 自定义路由器设计 |
| 2 | + |
| 3 | +## HTTP路由 |
| 4 | +HTTP路由组件负责将HTTP请求交到对应的函数处理(或者是一个struct的方法),如前面小节所描述的结构图,路由在框架中相当于一个事件处理器,而这个事件包括: |
| 5 | + |
| 6 | +- 用户请求的路径(例如:/user/123,/article/123),当然还有查询串信息(例如?id=11) |
| 7 | +- HTTP的请求method(GET、POST、PUT、DELETE、PATCH等) |
| 8 | + |
| 9 | +路由器就是根据用户请求的这个信息定位到相应的处理函数。 |
| 10 | +## 默认的路由实现 |
| 11 | +在3.4小节有过介绍Go的http包的详解,里面介绍了Go的http包如何设计和实现路由,这里继续以一个例子来说明: |
| 12 | + |
| 13 | + func fooHandler(w http.ResponseWriter, r *http.Request) { |
| 14 | + fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) |
| 15 | + } |
| 16 | + |
| 17 | + http.Handle("/foo", fooHandler) |
| 18 | + |
| 19 | + http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { |
| 20 | + fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) |
| 21 | + }) |
| 22 | + |
| 23 | + log.Fatal(http.ListenAndServe(":8080", nil)) |
| 24 | + |
| 25 | +上面的例子调用了http默认的DefaultServeMux来添加路由,两个参数,第一个参数是前面所讲的用户请求的路径(Go中保存在r.URL.Path),第二参数是定位要执行的函数,路由的思路主要集中在两点: |
| 26 | + |
| 27 | +- 添加路由信息 |
| 28 | +- 根据用户请求转发到要执行的函数 |
| 29 | + |
| 30 | +Go默认的包添加是通过函数`http.Handle`和`http.HandleFunc`等来添加,底层都是调用了`DefaultServeMux.Handle(pattern string, handler Handler)`,这个函数会把路由信息存储在一个map信息中`map[string]muxEntry`,这就解决了上面说的第一点。 |
| 31 | + |
| 32 | +Go的监听端口,然后接收到tcp连接会扔给Handler来处理,上面的例子默认nil即为`http.DefaultServeMux`,通过`DefaultServeMux.ServeHTTP`函数来进行调度,循环上面存储的map信息,和访问url进行比对查询注册的处理函数,这样就实现了上面所说的第二点。 |
| 33 | + |
| 34 | + for k, v := range mux.m { |
| 35 | + if !pathMatch(k, path) { |
| 36 | + continue |
| 37 | + } |
| 38 | + if h == nil || len(k) > n { |
| 39 | + n = len(k) |
| 40 | + h = v.h |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | + |
| 45 | +## beego框架路由实现 |
| 46 | +目前几乎所有的Web应用路由实现都是基于http默认的路由器,但是默认的路由器有几个限制点: |
| 47 | + |
| 48 | +- 不支持参数设定,例如/user/:uid 这种泛类型匹配 |
| 49 | +- 无法很好的支持REST模式,无法限制访问的方法,例如上面的例子中,用户访问/foo,可以用GET、POST、DELETE、HEAD等方式访问 |
| 50 | +- 默认的路由规则太多了,我前面自己开发了一个API的应用,路由规则有三十几条,这种路由多了之后其实可以进一步简化,通过struct的方法进行一种简化 |
| 51 | + |
| 52 | +beego框架的路由器基于上面的几点限制考虑设计了一种REST方式的路由实现,路由设计也是基于上面的默认设计的两点来考虑:存储路由和转发路由 |
| 53 | + |
| 54 | +### 存储路由 |
| 55 | +针对前面所说的限制点,我们首先要解决参数支持就需要用到正则,第二和第三点我们通过一种变通的方法来解决,REST的方法对应到struct的方法中去,然后路由到struct而不是函数,这样在转发路由的时候就可以根据method来执行不同的方法。 |
| 56 | + |
| 57 | +根据上面的思路,我们设计了两个数据类型controllerInfo(保存路径和对应的struct,这里是一个reflect.Type类型)和ControllerRegistor(routers是一个slice用来保存用户添加的路由信息,已经beego框架的信息) |
| 58 | + |
| 59 | + type controllerInfo struct { |
| 60 | + regex *regexp.Regexp |
| 61 | + params map[int]string |
| 62 | + controllerType reflect.Type |
| 63 | + } |
| 64 | + |
| 65 | + type ControllerRegistor struct { |
| 66 | + routers []*controllerInfo |
| 67 | + Application *App |
| 68 | + } |
| 69 | + |
| 70 | + |
| 71 | +ControllerRegistor对外的接口函数有 |
| 72 | + |
| 73 | + func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) |
| 74 | + |
| 75 | +详细的实现如下所示: |
| 76 | + |
| 77 | + func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) { |
| 78 | + parts := strings.Split(pattern, "/") |
| 79 | + |
| 80 | + j := 0 |
| 81 | + params := make(map[int]string) |
| 82 | + for i, part := range parts { |
| 83 | + if strings.HasPrefix(part, ":") { |
| 84 | + expr := "([^/]+)" |
| 85 | + //a user may choose to override the defult expression |
| 86 | + // similar to expressjs: ‘/user/:id([0-9]+)’ |
| 87 | + if index := strings.Index(part, "("); index != -1 { |
| 88 | + expr = part[index:] |
| 89 | + part = part[:index] |
| 90 | + } |
| 91 | + params[j] = part |
| 92 | + parts[i] = expr |
| 93 | + j++ |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + //recreate the url pattern, with parameters replaced |
| 98 | + //by regular expressions. then compile the regex |
| 99 | + pattern = strings.Join(parts, "/") |
| 100 | + regex, regexErr := regexp.Compile(pattern) |
| 101 | + if regexErr != nil { |
| 102 | + //TODO add error handling here to avoid panic |
| 103 | + panic(regexErr) |
| 104 | + return |
| 105 | + } |
| 106 | + |
| 107 | + //now create the Route |
| 108 | + t := reflect.Indirect(reflect.ValueOf(c)).Type() |
| 109 | + route := &controllerInfo{} |
| 110 | + route.regex = regex |
| 111 | + route.params = params |
| 112 | + route.controllerType = t |
| 113 | + |
| 114 | + p.routers = append(p.routers, route) |
| 115 | + |
| 116 | + } |
| 117 | + |
| 118 | +### 静态路由实现 |
| 119 | +上面我们实现的动态路由的实现,Go的http包默认支持静态文件处理FileServer,由于我们实现了自定义的路由器,那么静态文件也需要自己设定,beego的静态文件夹保存在全局变量StaticDir中,StaticDir是一个map类型,实现如下: |
| 120 | + |
| 121 | + func (app *App) SetStaticPath(url string, path string) *App { |
| 122 | + StaticDir[url] = path |
| 123 | + return app |
| 124 | + } |
| 125 | + |
| 126 | +应用中设置静态路径可以使用如下方式实现: |
| 127 | + |
| 128 | + beego.SetStaticPath("/img","/static/img") |
| 129 | + |
| 130 | + |
| 131 | +### 转发路由 |
| 132 | +转发路由是基于ControllerRegistor的路由信息来进行转发的,详细的实现如下代码所示: |
| 133 | + |
| 134 | + // AutoRoute |
| 135 | + func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| 136 | + defer func() { |
| 137 | + if err := recover(); err != nil { |
| 138 | + if !RecoverPanic { |
| 139 | + // go back to panic |
| 140 | + panic(err) |
| 141 | + } else { |
| 142 | + Critical("Handler crashed with error", err) |
| 143 | + for i := 1; ; i += 1 { |
| 144 | + _, file, line, ok := runtime.Caller(i) |
| 145 | + if !ok { |
| 146 | + break |
| 147 | + } |
| 148 | + Critical(file, line) |
| 149 | + } |
| 150 | + } |
| 151 | + } |
| 152 | + }() |
| 153 | + var started bool |
| 154 | + for prefix, staticDir := range StaticDir { |
| 155 | + if strings.HasPrefix(r.URL.Path, prefix) { |
| 156 | + file := staticDir + r.URL.Path[len(prefix):] |
| 157 | + http.ServeFile(w, r, file) |
| 158 | + started = true |
| 159 | + return |
| 160 | + } |
| 161 | + } |
| 162 | + requestPath := r.URL.Path |
| 163 | + |
| 164 | + //find a matching Route |
| 165 | + for _, route := range p.routers { |
| 166 | + |
| 167 | + //check if Route pattern matches url |
| 168 | + if !route.regex.MatchString(requestPath) { |
| 169 | + continue |
| 170 | + } |
| 171 | + |
| 172 | + //get submatches (params) |
| 173 | + matches := route.regex.FindStringSubmatch(requestPath) |
| 174 | + |
| 175 | + //double check that the Route matches the URL pattern. |
| 176 | + if len(matches[0]) != len(requestPath) { |
| 177 | + continue |
| 178 | + } |
| 179 | + |
| 180 | + params := make(map[string]string) |
| 181 | + if len(route.params) > 0 { |
| 182 | + //add url parameters to the query param map |
| 183 | + values := r.URL.Query() |
| 184 | + for i, match := range matches[1:] { |
| 185 | + values.Add(route.params[i], match) |
| 186 | + params[route.params[i]] = match |
| 187 | + } |
| 188 | + |
| 189 | + //reassemble query params and add to RawQuery |
| 190 | + r.URL.RawQuery = url.Values(values).Encode() + "&" + r.URL.RawQuery |
| 191 | + //r.URL.RawQuery = url.Values(values).Encode() |
| 192 | + } |
| 193 | + //Invoke the request handler |
| 194 | + vc := reflect.New(route.controllerType) |
| 195 | + init := vc.MethodByName("Init") |
| 196 | + in := make([]reflect.Value, 2) |
| 197 | + ct := &Context{ResponseWriter: w, Request: r, Params: params} |
| 198 | + in[0] = reflect.ValueOf(ct) |
| 199 | + in[1] = reflect.ValueOf(route.controllerType.Name()) |
| 200 | + init.Call(in) |
| 201 | + in = make([]reflect.Value, 0) |
| 202 | + method := vc.MethodByName("Prepare") |
| 203 | + method.Call(in) |
| 204 | + if r.Method == "GET" { |
| 205 | + method = vc.MethodByName("Get") |
| 206 | + method.Call(in) |
| 207 | + } else if r.Method == "POST" { |
| 208 | + method = vc.MethodByName("Post") |
| 209 | + method.Call(in) |
| 210 | + } else if r.Method == "HEAD" { |
| 211 | + method = vc.MethodByName("Head") |
| 212 | + method.Call(in) |
| 213 | + } else if r.Method == "DELETE" { |
| 214 | + method = vc.MethodByName("Delete") |
| 215 | + method.Call(in) |
| 216 | + } else if r.Method == "PUT" { |
| 217 | + method = vc.MethodByName("Put") |
| 218 | + method.Call(in) |
| 219 | + } else if r.Method == "PATCH" { |
| 220 | + method = vc.MethodByName("Patch") |
| 221 | + method.Call(in) |
| 222 | + } else if r.Method == "OPTIONS" { |
| 223 | + method = vc.MethodByName("Options") |
| 224 | + method.Call(in) |
| 225 | + } |
| 226 | + if AutoRender { |
| 227 | + method = vc.MethodByName("Render") |
| 228 | + method.Call(in) |
| 229 | + } |
| 230 | + method = vc.MethodByName("Finish") |
| 231 | + method.Call(in) |
| 232 | + started = true |
| 233 | + break |
| 234 | + } |
| 235 | + |
| 236 | + //if no matches to url, throw a not found exception |
| 237 | + if started == false { |
| 238 | + http.NotFound(w, r) |
| 239 | + } |
| 240 | + } |
| 241 | + |
| 242 | +### 使用入门 |
| 243 | +基于这样的路由设计之后就可以解决前面所说的三个限制点,使用的方式如下所示: |
| 244 | + |
| 245 | +基本的使用注册路由: |
| 246 | + |
| 247 | + beego.BeeApp.RegisterController("/", &controllers.MainController{}) |
| 248 | + |
| 249 | +参数注册: |
| 250 | + |
| 251 | + beego.BeeApp.RegisterController("/:param", &controllers.UserController{}) |
| 252 | + |
| 253 | +正则匹配: |
| 254 | + |
| 255 | + beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{}) |
2 | 256 |
|
3 | 257 | ## links
|
4 | 258 | * [目录](<preface.md>)
|
|
0 commit comments