七天从零实现Web框架Gee - 2

  • 在第一天中我们将路由(router)独立出来,方便之后增强,今天我们要做的是设计上下文(Context),封装 Request 和 Response ,提供对 JSON、HTML 等返回类型的支持

我们为什么要设计Context

对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。

我们用返回 JSON 数据作比较,感受下封装前后的差距

obj = map[string]interface{}{
    "name": "geektutu",
    "password": "1234",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w)
if err := encoder.Encode(obj); err != nil {
    http.Error(w, err.Error(), 500)
}

封装后

c.JSON(http.StatusOK, gee.H{
    "username": c.PostForm("username"),
    "password": c.PostForm("password"),
})

针对使用场景,封装*http.Request和http.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name,参数:name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。

具体实现

在context.go首先给map[string]interface{}起一个别名gee.H,构建JSON数据时,显得更简洁,然后开始定义Context结构体的内容

type H map[string]interface{}

type Context struct {
	// origin objects
	Writer http.ResponseWriter
	Req    *http.Request
	// request info
	Path   string
	Method string
	// response info
	StatusCode int
}

然后定义一个newContext函数,返回一个构造好的Context, Context目前只包含了http.ResponseWriter和*http.Request,另外提供了对 Method 和 Path 这两个常用属性的直接访问

接下来定义一些消息方法

  • PostForm方法获取POST请求中的表单参数值,即通过表单提交的数据
  • Query方法获取 GET 请求中的查询参数值,即通过 URL 查询字符串传递的数据
  • Status方法显示调用 WriteHeader ,发送 http 回复的头域和状态码
  • SetHeader方法返回一个Header类型值,该值会被Write.Header方法发送,其中 Set(key, value string)方法的作用是向Header对象添加键值对,如键已存在则会用只有新值一个元素的切片取代旧值切片支持JSON,HTML等返回类型

快速构造String/Data/JSON/HTML响应的方法

  • String类型
func (c *Context) String(code int, format string, values ...interface{}) {
	c.SetHeader("Content-Type", "text/plain;charset=UTF-8")
	c.Status(code)
	c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
  • JSON类型
func (c *Context) JSON(code int, obj interface{}) {
	c.SetHeader("Content-Type", "application/json;charset=UTF-8")
	c.Status(code)
	encoder := json.NewEncoder(c.Writer)
	if err := encoder.Encode(obj); err != nil {
		http.Error(c.Writer, err.Error(), 500)
	}
}
  • Data类型
func (c *Context) Data(code int, data []byte) {
	c.Status(code)
	c.Writer.Write(data)
}
  • HTML类型
func (c *Context) HTML(code int, html string) {
	c.SetHeader("Content-Type", "text/html")
	c.Status(code)
	c.Writer.Write([]byte(html))
}

我们将和路由相关的方法和结构提取了出来,放到了一个新的文件中router.go,方便我们下一次对 router 的功能进行增强,例如提供动态路由的支持。 router 的 handle 方法作了一个细微的调整,即 handler 的参数,变成了 Context

首先是定义结构体 router,即gee.go中的Engine结构体,然后是newRouter方法用来存放路由地址以及处理程序,addRouter方法,将路由地址与对应的处理程序添加到路由哈希表中,最后就是handle方法, 根据路由地址调用对应的处理程序

type router struct {
	handler map[string]HandlerFunc
}

func newRouter() *router {
	return &router{handler: make(map[string]HandlerFunc)}
}

func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
	key := method + "-" + pattern
	log.Printf("Route %4s - %s", method, pattern)
	r.handler[key] = handler
}

func (r *router) handle(c *Context) {
	key := c.Method + "-" + c.Path
	if handler, ok := r.handler[key]; ok {
		handler(c)
	} else {
		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
	}
}

最后就是修改gee.go中的代码,将HandlerFunc中的参数改为*Context,Engine结构体中map类型数据改为*router类型数据,修改SeverHTTP函数

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := newContext(w, req)
	engine.router.handle(c)
}

相比第一天的代码,这个方法也有细微的调整,在调用 router.handle 之前,构造了一个 Context 对象。这个对象目前还非常简单,仅仅是包装了原来的两个参数,之后我们会慢慢地给Context插上翅膀。

在main.go文件中将func中的参数同样改为c *gee.Context

func main() {
	r := gee.New()
	r.GET("/", func(c *gee.Context) {
		c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
	})
	r.GET("/hello", func(c *gee.Context) {
		// expect /hello?name=geektutu
		c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
	})

	r.POST("/login", func(c *gee.Context) {
		c.JSON(http.StatusOK, gee.H{
			"username": c.PostForm("username"),
			"password": c.PostForm("password"),
		})
	})
	r.Run(":8090")
}

总结

由gee的实例Engine开始,里面存着一个router,router里面存着一个map,k的Method + “-” + Path,而v则是对应的回调函数,那么这个回调的参数全部由context存储,在ServeHTTP中,每有一个新的请求过来,都会创建一个context,然后调用engine.router.handle,并把context传进去,给请求发送响应,由main中用户自己定义,但是都是调用context中api来实现的,所以context承载了整个会话的生命周期

猜你喜欢

转载自blog.csdn.net/qq_47431008/article/details/130658160