title: Go语言动手写Web框架 - Gee第六天 模板(HTML Template)
date: 2019-09-08 20:10:00
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了如何为Web框架添加HTML模板(HTML Template)以及静态文件(Serve Static Files)的功能。
tags:

  • Go
    nav: 从零实现
    categories:
  • Web框架 - Gee
    keywords:
  • Go语言
  • 从零实现Web框架
  • 动手写Web框架
  • Template
    image: post/gee-day6/html.png
    github: https://github.com/geektutu/7days-golang
    book: 七天用Go从零实现系列
    book_title: Day6 模板 Template

本文是 7天用Go从零实现Web框架Gee教程系列的第六篇。

  • 实现静态资源服务(Static Resource)。
  • 支持HTML模板渲染。

服务端渲染

现在越来越流行前后端分离的开发模式,即 Web 后端提供 RESTful 接口,返回结构化的数据(通常为 JSON 或者 XML)。前端使用 AJAX 技术请求到所需的数据,利用 JavaScript 进行渲染。Vue/React 等前端框架持续火热,这种开发模式前后端解耦,优势非常突出。后端童鞋专心解决资源利用,并发,数据库等问题,只需要考虑数据如何生成;前端童鞋专注于界面设计实现,只需要考虑拿到数据后如何渲染即可。使用 JSP 写过网站的童鞋,应该能感受到前后端耦合的痛苦。JSP 的表现力肯定是远不如 Vue/React 等专业做前端渲染的框架的。而且前后端分离在当前还有另外一个不可忽视的优势。因为后端只关注于数据,接口返回值是结构化的,与前端解耦。同一套后端服务能够同时支撑小程序、移动APP、PC端 Web 页面,以及对外提供的接口。随着前端工程化的不断地发展,Webpack,gulp 等工具层出不穷,前端技术越来越自成体系了。

但前后分离的一大问题在于,页面是在客户端渲染的,比如浏览器,这对于爬虫并不友好。Google 爬虫已经能够爬取渲染后的网页,但是短期内爬取服务端直接渲染的 HTML 页面仍是主流。

今天的内容便是介绍 Web 框架如何支持服务端渲染的场景。

静态文件(Serve Static Files)

网页的三剑客,JavaScript、CSS 和 HTML。要做到服务端渲染,第一步便是要支持 JS、CSS 等静态文件。还记得我们之前设计动态路由的时候,支持通配符*匹配多级子路径。比如路由规则/assets/*filepath,可以匹配/assets/开头的所有的地址。例如/assets/js/geektutu.js,匹配后,参数filepath就赋值为js/geektutu.js

那如果我么将所有的静态文件放在/usr/web目录下,那么filepath的值即是该目录下文件的相对地址。映射到真实的文件后,将文件返回,静态服务器就实现了。

找到文件后,如何返回这一步,net/http库已经实现了。因此,gee 框架要做的,仅仅是解析请求的地址,映射到服务器上文件的真实地址,交给http.FileServer处理就好了。

day6-template/gee/gee.go

  1. // create static handler
  2. func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
  3. absolutePath := path.Join(group.prefix, relativePath)
  4. fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
  5. return func(c *Context) {
  6. file := c.Param("filepath")
  7. // Check if file exists and/or if we have permission to access it
  8. if _, err := fs.Open(file); err != nil {
  9. c.Status(http.StatusNotFound)
  10. return
  11. }
  12. fileServer.ServeHTTP(c.Writer, c.Req)
  13. }
  14. }
  15. // serve static files
  16. func (group *RouterGroup) Static(relativePath string, root string) {
  17. handler := group.createStaticHandler(relativePath, http.Dir(root))
  18. urlPattern := path.Join(relativePath, "/*filepath")
  19. // Register GET handlers
  20. group.GET(urlPattern, handler)
  21. }

我们给RouterGroup添加了2个方法,Static这个方法是暴露给用户的。用户可以将磁盘上的某个文件夹root映射到路由relativePath。例如:

  1. r := gee.New()
  2. r.Static("/assets", "/usr/geektutu/blog/static")
  3. // 或相对路径 r.Static("/assets", "./static")
  4. r.Run(":9999")

用户访问localhost:9999/assets/js/geektutu.js,最终返回/usr/geektutu/blog/static/js/geektutu.js

HTML 模板渲染

Go语言内置了text/templatehtml/template2个模板标准库,其中html/template为 HTML 提供了较为完整的支持。包括普通变量渲染、列表渲染、对象渲染等。gee 框架的模板渲染直接使用了html/template提供的能力。

  1. Engine struct {
  2. *RouterGroup
  3. router *router
  4. groups []*RouterGroup // store all groups
  5. htmlTemplates *template.Template // for html render
  6. funcMap template.FuncMap // for html render
  7. }
  8. func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
  9. engine.funcMap = funcMap
  10. }
  11. func (engine *Engine) LoadHTMLGlob(pattern string) {
  12. engine.htmlTemplates = template.Must(template.New("").Funcs(engine.funcMap).ParseGlob(pattern))
  13. }

首先为 Engine 示例添加了 *template.Templatetemplate.FuncMap对象,前者将所有的模板加载进内存,后者是所有的自定义模板渲染函数。

另外,给用户分别提供了设置自定义渲染函数funcMap和加载模板的方法。

接下来,对原来的 (*Context).HTML()方法做了些小修改,使之支持根据模板文件名选择模板进行渲染。

day6-template/gee/context.go

  1. type Context struct {
  2. // ...
  3. // engine pointer
  4. engine *Engine
  5. }
  6. func (c *Context) HTML(code int, name string, data interface{}) {
  7. c.SetHeader("Content-Type", "text/html")
  8. c.Status(code)
  9. if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil {
  10. c.Fail(500, err.Error())
  11. }
  12. }

我们在 Context 中添加了成员变量 engine *Engine,这样就能够通过 Context 访问 Engine 中的 HTML 模板。实例化 Context 时,还需要给 c.engine 赋值。

day6-template/gee/gee.go

  1. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  2. // ...
  3. c := newContext(w, req)
  4. c.handlers = middlewares
  5. c.engine = engine
  6. engine.router.handle(c)
  7. }

使用Demo

最终的目录结构

  1. ---gee/
  2. ---static/
  3. |---css/
  4. |---geektutu.css
  5. |---file1.txt
  6. ---templates/
  7. |---arr.tmpl
  8. |---css.tmpl
  9. |---custom_func.tmpl
  10. ---main.go
  1. <!-- day6-template/templates/css.tmpl -->
  2. <html>
  3. <link rel="stylesheet" href="/assets/css/geektutu.css">
  4. <p>geektutu.css is loaded</p>
  5. </html>

day6-template/main.go

  1. type student struct {
  2. Name string
  3. Age int8
  4. }
  5. func FormatAsDate(t time.Time) string {
  6. year, month, day := t.Date()
  7. return fmt.Sprintf("%d-%02d-%02d", year, month, day)
  8. }
  9. func main() {
  10. r := gee.New()
  11. r.Use(gee.Logger())
  12. r.SetFuncMap(template.FuncMap{
  13. "FormatAsDate": FormatAsDate,
  14. })
  15. r.LoadHTMLGlob("templates/*")
  16. r.Static("/assets", "./static")
  17. stu1 := &student{Name: "Geektutu", Age: 20}
  18. stu2 := &student{Name: "Jack", Age: 22}
  19. r.GET("/", func(c *gee.Context) {
  20. c.HTML(http.StatusOK, "css.tmpl", nil)
  21. })
  22. r.GET("/students", func(c *gee.Context) {
  23. c.HTML(http.StatusOK, "arr.tmpl", gee.H{
  24. "title": "gee",
  25. "stuArr": [2]*student{stu1, stu2},
  26. })
  27. })
  28. r.GET("/date", func(c *gee.Context) {
  29. c.HTML(http.StatusOK, "custom_func.tmpl", gee.H{
  30. "title": "gee",
  31. "now": time.Date(2019, 8, 17, 0, 0, 0, 0, time.UTC),
  32. })
  33. })
  34. r.Run(":9999")
  35. }

访问下主页,模板正常渲染,CSS 静态文件加载成功。

gee-day6 - 图1