极客兔兔七天系列学习笔记--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请求到达时,我们至少要考虑并完成以下动作:
- 获取请求的参数,进行保存。
- 找寻路由对应的方法,进行调用。
- 如果有路由分组,考虑链式调用下的中间件。
- 动态路由的映射(可选)。
由于要暂时保存参数和中间件方法,所以我们构造唯一的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()
}
}
补充:注册路由功能是由分组去调用GET,POST方法完成,最终是调用engine的router模块方法。实现静态文件服务也很简单,首先借助前缀路由树完成文件路径获取,最终通过FileServer的ServeHTTP方法完成文件请求。