• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

参考Laravel弄一个golang的路由包

武飞扬头像
silsuer在掘金
帮助3

概述

最近在开发自己的 Web 框架 Bingo, 也查看了一些市面上的路由工具包,但是都有些无法满足我的需求,

例如,我希望获得一些 Laravel 框架的特性:

  • 快速的路由查找

  • 动态路由支持

  • 中间件支持

  • 路由组支持

而市面上最快的就是 httprouter ,这里本来几个月前我改造过一次: 改造httprouter使其可以支持中间件,但是那时是耦合在bingo框架中的,并且中间件不支持拦截,在这里我需要将其抽出来制作出一个第三方包,可以直接引用,无需依赖 Bingo 框架

所以我依旧选用了 httprouter 作为基础包,将其进行改造,使其支持以上特性。

仓库地址: bingo-router

用法在项目的 README 中已经将的很清楚了,这里不再赘述,有问题或者有什么需求可以给我提 issue 喔~

也建议先过一遍 README.md 再看这篇文章,不然可能会有地方看不懂...

改造主要分为两部分

  1. 第一部分是将 httprouter 的 路由树tree上挂载的 handle方法改为我们自定义的结构体

httprouter 的原理可以看这篇 5.2 router 请求路由

简单来讲,就是把所有接口的路径,共同构造一颗前缀树,将前缀相同的路径放在一棵树杈中,这样可以加速查找速度,而每片树叶都代表查找到了一个路由方法,挂载的就是一个方法,

但是这样的话这棵前缀树上就只能挂载 方法了,无法添加一些额外信息,所以第一步就要让前缀树上挂载一个我们自定义的结构体,让我们可以查找到挂载的中间件、路由 前缀等

  1. 第二部分是实现中间件功能,如果只是 遍历操作一个中间件数组,那么无法进行一些拦截操作,

    比如,我们要实现一个中间件用来验证用户是否登陆 ,未登录用户将会返回错误信息,那么如果遍历执行一个中间件数组,最终还是将会执行到最终的路由

    为了实现拦截功能,我参考了 Laravel中的 Pipeline 功能的实现原理,实现了一个管道对象,实现上述效果

开始改造

1. 第一部分

  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 文件

  2. README 中的路由注册操作,使用的是责任链模式,每个方法最后都返回一个当前对象的指针,就可以实现链式操作 其中的 Get``Post 等方法,实际上是在向Route对象中的属性赋值,没什么技术含量,感兴趣可以看源码

  3. 实现路由组功能

    通过路由组,我们可以给子路由设置公共的前缀和中间件,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 方法,让每一个通过这种方法创建的路由都在Builderroutes属性下:

      func (b *Builder) NewRoute() *Route {
      	r := NewRoute()
      	b.routes = append(b.routes, r)
      	return r
      }
    

    在创建的时候将指针放入 Builder 中即可

    这样,我们所建立的多个路由 就可以嵌套在一起了,那么如何利用 httprouterHandle 方法,将我们的 Route 对象,注入到Router 中呢?

  4. 将路由注入路由器

    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!

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanheagecc
系列文章
更多 icon
同类精品
更多 icon
继续加载