极客兔兔七天系列学习笔记--GeeWeb


框架的意义在于,封装大部分手工操作的功能,提升开发效率。

引擎封装

Gee框架模仿Gin,于是我们整体构造一个Web引擎类Engine。如果直接使用基础库http,启动整个Web应用依赖以下代码:
http.ListenAndServe(addr, engine)
使用Engine进行封装:

func (engine *Engine) Run(addr string) error {
	return http.ListenAndServe(addr, engine)
}

于是Engine自然要实现Handler接口的ServeHTTP方法。

处理Web请求的大门---ServeHTTP

当一个Web请求到达时,我们至少要考虑并完成以下动作:

  1. 获取请求的参数,进行保存。
  2. 找寻路由对应的方法,进行调用。
  3. 如果有路由分组,考虑链式调用下的中间件。
  4. 动态路由的映射(可选)。

由于要暂时保存参数和中间件方法,所以我们构造唯一的Context进行状态管理。

type Context struct {
	Req        *http.Request
	Res        http.ResponseWriter
	param      map[string]string
	Path       string
	Method     string
	index      int
	handlers   []HandleFunc
	StatusCode int
	engine     *Engine //为了访问engine中加载的模版
}
func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	context := newContext(writer, request)
	var middlers []HandleFunc
	for _, group := range engine.groups {
		if strings.HasPrefix(context.Path, group.prefiex) {
			middlers = append(middlers, group.middlers...)
		}
	}
	context.engine = engine
	context.handlers = middlers
	engine.router.handle(context)
}

首先基于当前request和response构造Context,然后去遍历分组列表,简单根据前缀获取当前请求对应分组,将对应的中间件加入进来。之后交给engine的router模块处理请求。注意,此时参数以封装为context,我们所有关于本次请求的信息都能在context中找到。

Group分组思想

这里分组的思想依然模仿Gin,根据group的prefix和parent实现分组件的父子关系,每个group都会维护自身的中间件切片。

func (group *RouterGroup) Group(prefix string) *RouterGroup {
	engine := group.engine
	newGroup := &RouterGroup{
		engine:  engine,
		prefiex: group.prefiex + prefix,
		parent:  group,
	}
	engine.groups = append(engine.groups, newGroup)
	return newGroup
}

func (group *RouterGroup) Use(handlers ...HandleFunc) {
	group.middlers = append(group.middlers, handlers...)
}

func New() *Engine {
	engine := &Engine{
		router: newRouter(),
	}
	engine.RouterGroup = &RouterGroup{
		engine: engine,
	}
	//这里会有默认加载的中间件
	engine.groups = []*RouterGroup{engine.RouterGroup}
	return engine
}

我们规定,engine本身就是默认的特殊group,因为框架可能不需要分组就能添加路由映射,或是定义于引擎全局的中间件,例如日志和错误回复。当然,它还要同时维护router对象和分组切片。

Router模块

起码在调用handle方法时,能根据字符串地址查找到对应方法调用,同样实现动态路由我们采用了前缀树的数据结构,所以需要维护两个map。

type HandleFunc func(context *Context)

type Router struct {
	roots    map[string]*Node
	handlers map[string]HandleFunc
}

我们实现的动态路由满足两个功能,参数匹配和通配*。数据结构如下:

点击查看代码
type node struct {
	pattern  string // 待匹配路由,例如 /p/:lang
	part     string // 路由中的一部分,例如 :lang
	children []*node // 子节点,例如 [doc, tutorial, intro]
	isWild   bool // 是否精确匹配,part 含有 : 或 * 时为true
}
配合原官网图片一目了然:

极客兔兔-前缀树

我们注册路由时需要添加方法映射,添加前缀树节点。查询路由时需要查询前缀树进行参数匹配,根据映射查找方法。所以前缀树Trie只对外提供search和insert方法即可。(有关前缀树方法自行前往官网或其他路径学习)。
参数匹配后添加路由对应方法就可以开始调用。

func (r *Router) handle(ctx *Context) {
	n, params := r.getRoute(ctx.Method, ctx.Path)
	if n != nil {
		ctx.param = params
		key := ctx.Method + "-" + n.pattern
		ctx.handlers = append(ctx.handlers, r.handlers[key])
	} else {
		ctx.handlers = append(ctx.handlers, func(context *Context) {
			context.String(http.StatusNotFound, "404 NOT FOUND: %s", ctx.Path)
		})
	}
	ctx.Next()
}

Next方法体本身是一个for循环(有一个记录位置的索引数字),但我们的中间件方法可能在结束请求后需要继续处理相关操作,以错误回复中间件为例:

点击查看代码
func Recovery() HandleFunc {
	return func(context *Context) {
		defer func() {
			if err := recover(); err != nil {
				message := fmt.Sprintf("%s", err)
				log.Printf("%s\n", trace(message))
				context.Fail(http.StatusInternalServerError, "Internal Server Error")
			}
		}()
		context.Next()
	}
}
通过主动调用Next方法,此时的中间件成为了父函数,子函数调用后返回当然能继续逻辑处理,也能实现错误恢复。

补充:注册路由功能是由分组去调用GET,POST方法完成,最终是调用engine的router模块方法。实现静态文件服务也很简单,首先借助前缀路由树完成文件路径获取,最终通过FileServer的ServeHTTP方法完成文件请求。