改造httprouter使其支持中间件

silsuer · · 2657 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

# 改造httprouter使其支持中间件 ## 写在前面 `httprouter`在业界广受好评,主要就是因为它的性能 `httprouter`项目地址:[httprouter](https://github.com/julienschmidt/httprouter) `httprouter`的原理:[点这里点这里~](http://www.okyes.me/2016/05/08/httprouter.html) 而`httprouter`默认是不支持中间件等功能的 `README`中说: `Where can I find Middleware X? This package just provides a very efficient request router with a few extra features. The router is just a http.Handler, you can chain any http.Handler compatible middleware before the router, for example the Gorilla handlers. Or you could just write your own, it's very easy!` 而我目前在写的Bingo框架也是基于`httprouter`的,所以准备对它进行改造,让他支持中间件的功能 我的项目地址[silsuer/bingo](https://github.com/silsuer/bingo) ## 开始改造 1. 原理 查看httprouter的源代码,可以看到,它使用一个前缀树来管理注册的路由,而挂载到这棵树上的,是一个`Handle`类型的方法, 这个方法长这样`type Handle func(http.ResponseWriter, *http.Request, Params)` 使用`httprouter.Handle(args)`方法,会将路由根据路径放置在这棵树中, 在每一个http请求进来的时候,会走到`ServeHttp`方法中,这个方法就是一个多路的路由器,代码如下: ```go func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { // 判断panic函数 if r.PanicHandler != nil { defer r.recv(w, req) } // 开始去查找注册的路由函数 if root := r.trees[req.Method]; root != nil { path := req.URL.Path if handle, ps, tsr := root.getValue(path); handle != nil { // 查找到了,执行这个函数 handle(w, req, ps) return } else if req.Method != "CONNECT" && path != "/" { // 没有找到,开始进行重定向或者其他操作 code := 301 // Permanent redirect, request with GET method if req.Method != "GET" { // Temporary redirect, request with same method // As of Go 1.3, Go does not support status code 308. code = 307 } if tsr && r.RedirectTrailingSlash { if len(path) > 1 && path[len(path)-1] == '/' { req.URL.Path = path[:len(path)-1] } else { req.URL.Path = path + "/" } http.Redirect(w, req, req.URL.String(), code) return } // Try to fix the request path if r.RedirectFixedPath { fixedPath, found := root.findCaseInsensitivePath( CleanPath(path), r.RedirectTrailingSlash, ) if found { req.URL.Path = string(fixedPath) http.Redirect(w, req, req.URL.String(), code) return } } } } // Handle 405 if r.HandleMethodNotAllowed { for method := range r.trees { // Skip the requested method - we already tried this one if method == req.Method { continue } handle, _, _ := r.trees[method].getValue(req.URL.Path) if handle != nil { if r.MethodNotAllowed != nil { r.MethodNotAllowed(w, req) } else { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed, ) } return } } } // 如果定义了NotFound函数的话,在查找不成功的时候会执行这个函数,否则执行默认的NotFound方法 // Handle 404 if r.NotFound != nil { r.NotFound(w, req) } else { http.NotFound(w, req) } } ``` 而所谓中间件,就是在查找成功之后,首先执行中间件的方法,然后再执行handle方法,那么我们的思路就有了 不再在tree上挂载handle方法,而是挂载一个我们的自定义的结构体,当查找成功的时候,先查看这个结构体是否有中间件 如果有执行,如果没有,直接执行handle方法 2. 自定义结构体 以前我写的两篇文章里 [使用Go写一个简易的MVC的Web框架](https://studygolang.com/articles/12818) [使用Go封装一个便捷的ORM](https://studygolang.com/articles/12825) 也介绍过,我们的路由结构体是这样的: ```go type Route struct { Path string // 路径 Target Handle // 对应的控制器路径 Controller@index 这样的方法 Method string // 访问类型 是get post 或者其他 Alias string // 路由的别名,并没有什么卵用的样子....... Middleware []Handle // 中间件名称 } ``` 其中的Handle是使用上面`httprouter`定义的方法, 接下来我们改造一下这个结构体 ```go // 上下文结构体 type Context struct { Writer http.ResponseWriter // 响应 Request *http.Request // 请求 Params Params //参数 } type TargetHandle func(context *Context) type MiddlewareHandle func(context *Context) *Context // 中间件需要把上下文返回回来,用来传入TargetHandle中 type Route struct { Path string // 路径 Target TargetHandle // 要执行的方法 Method string // 访问类型 是get post 或者其他 Alias string // 路由的别名,并没有什么卵用的样子....... Middleware []MiddlewareHandle // 中间件名称,在执行TargetHandle之前执行的方法 } ``` 我们将原来Handle中的参数都装入一个上下文结构体中,然后在Route结构体中指明函数的类型 3. 把Route结构体挂载到Tree上 查看`httprouter`的注册路由的方法: ```go func (r *Router) Handle(method, path string, handle Handle) { // 判断路径的格式是否正确 if path[0] != '/' { panic("path must begin with '/' in path '" + path + "'") } // 如果前缀树是空的,就新建一颗 if r.trees == nil { r.trees = make(map[string]*node) } root := r.trees[method] // 如果树的根节点为空,就新建一个根节点 if root == nil { root = new(node) r.trees[method] = root } // 根据路径,把要执行的方法挂载到树上 root.addRoute(path, handle) } ``` 现在我们要把其中的Handle类型的数据都改成我们自己的Route类型, 很简单,代码就不贴了,想看的请看[commit变更记录](https://github.com/silsuer/bingo/commit/b651757e328b9a711ad2fe274ece8326c954d762) 接下来更改整个tree文件,实际上就是把`tree`的`node`结构体中的`handle`改为route 然后将tree文件中用到`handle`的地方,都用`n.route.Target`代替,虽然是无脑操作,但是要改不少行 也不贴代码了... commit记录在这里... [commit变更记录](https://github.com/silsuer/bingo/commit/b651757e328b9a711ad2fe274ece8326c954d762) 改完之后的 `ServeHttp`是这样滴~ ```go func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { if r.PanicHandler != nil { defer r.recv(w, req) } // 在查找之前,要先看看是否存在中间件 // 注册路由的时候,应该把中间件也放在此处 // 开始去查找注册的路由函数 if root := r.trees[req.Method]; root != nil { path := req.URL.Path if route, ps, tsr := root.getValue(path); route.Target != nil { // 封装上下文 context := &Context{w,req,ps} // 执行目标函数 route.Target(context) return } else if req.Method != "CONNECT" && path != "/" { code := 301 // Permanent redirect, request with GET method if req.Method != "GET" { // Temporary redirect, request with same method // As of Go 1.3, Go does not support status code 308. code = 307 } if tsr && r.RedirectTrailingSlash { if len(path) > 1 && path[len(path)-1] == '/' { req.URL.Path = path[:len(path)-1] } else { req.URL.Path = path + "/" } http.Redirect(w, req, req.URL.String(), code) return } // Try to fix the request path if r.RedirectFixedPath { fixedPath, found := root.findCaseInsensitivePath( CleanPath(path), r.RedirectTrailingSlash, ) if found { req.URL.Path = string(fixedPath) http.Redirect(w, req, req.URL.String(), code) return } } } } // Handle 405 if r.HandleMethodNotAllowed { for method := range r.trees { // Skip the requested method - we already tried this one if method == req.Method { continue } route, _, _ := r.trees[method].getValue(req.URL.Path) if route.Target != nil { if r.MethodNotAllowed != nil { r.MethodNotAllowed(w, req) } else { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed, ) } return } } } // Handle 404 if r.NotFound != nil { r.NotFound(w, req) } else { http.NotFound(w, req) } } ``` 可以看到,在查找到节点后,我封装了一个`Context`,接下来执行了TargetHandle方法, 4.更改代码,支持中间件: ```go // 封装上下文 context := &Context{w,req,ps} // 判断路由是否有中间件列表,如果有,就执行 if len(route.Middleware)!=0{ for _,middleHandle:= range route.Middleware{ context = middleHandle(context) // 顺序执行中间件,得到的返回结果重新注入到上下文中 } } // 执行目标函数 route.Target(context) return ``` 现在,我们定义如下一个路由: ```go var R = []bingo.Route{ { Path: "/home", Method: bingo.GET, Target: home.Index, Middleware: []bingo.MiddlewareHandle{ home.M1, home.M2, }, }, } ``` 其中, Index,M1,M2定义如下: ```go func M1(c *bingo.Context) *bingo.Context { fmt.Fprintln(c.Writer,"这是中间件1") return c } func M2(c *bingo.Context) *bingo.Context { fmt.Fprintln(c.Writer,"这是中间件2") return c } func Index(c *bingo.Context) { fmt.Fprint(c.Writer,"Hello World") } ``` 然后执行 `go run start.go` ,浏览器访问 `localhost:12345` ,就可以看到中间件执行成功的痕迹了, 改造成功 Bingo! 欢迎star,欢迎PR~~~~

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2657 次点击  
加入收藏 微博
14 回复  |  直到 2018-04-20 20:46:30
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传