Dawn's Blogs

分享技术 记录成长

0%

从零实现一个Web框架 (6) 模板

在本节,最终的代码目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dain/
|--context.go
|--dain.go
|--logger.go
|--router.go
|--trie.go
|--go.mod
static/
|--css/
|--index.css
templates/
|--index.tmpl
main.go
go.mod

实现目标

实现加载静态文件以及模板渲染功能:

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"DawnGin/dain"
"net/http"
)

func main() {
e := dain.New()

// 使用 Logger 中间件
e.Use(dain.Logger())

// 加载静态文件
e.Static("/static", "./static")

// 加载模板
e.LoadHTMLGlob("templates/*")

e.Get("/index", func(c *dain.Context) {
c.HTML(http.StatusOK, "index.tmpl", c.Path)
})

e.Run(":9000")
}

templates/index.tmpl

1
2
3
4
<html>
<link rel="stylesheet" href="/static/css/index.css">
<p>index.css is loaded, path is {{.}}</p>
</html>

static/css/index.css

1
2
3
4
5
p {
color: orange;
font-weight: 700;
font-size: 20px;
}

静态文件

dain/dain.go

  • Static 这个方法是暴露给用户的。用户可以将磁盘上的某个文件夹 root 映射到路由 relativePath

  • createStaticHandler 方法用于提供一个利用 relativePath 来访问本地文件系统的 HTTP 处理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
absolutePath := path.Join(group.prefix, relativePath)
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
return func(c *Context) {
file := c.Param("filepath")
// 检查文件是否可以访问
if _, err := fs.Open(file); err != nil {
c.Status(http.StatusNotFound)
return
}
fileServer.ServeHTTP(c.Writer, c.Req)
}
}

// Static 将磁盘上的某个路径 root 映射到 relativePath 上
func (group *RouterGroup) Static(relativePath string, root string) {
handler := group.createStaticHandler(relativePath, http.Dir(root))
urlPattern := path.Join(relativePath, "/*filepath")
// 注册
group.Get(urlPattern, handler)
}

模板渲染

Engine

dain/dain.go

修改 Engine 结构,添加用于记录模板的属性:

1
2
3
4
5
6
7
8
9
10
11
type Engine struct {
// 路由器
router *router
// 继承 RouterGroup,把根也看作是一个分组
*RouterGroup
// 记录所有的路由分组
groups []*RouterGroup
// 模板
htmlTemplates *template.Template
funcMap template.FuncMap
}

增加两个方法,分别用于注册模板函数以及加载模板文件

1
2
3
4
5
6
7
func (e *Engine) SetFuncMap(funcMap template.FuncMap) {
e.funcMap = funcMap
}

func (e *Engine) LoadHTMLGlob(pattern string) {
e.htmlTemplates = template.Must(template.New("").Funcs(e.funcMap).ParseGlob(pattern))
}

Context

dain/context.go

Context 结构体中增加指向引擎 Engine 的属性,用于访问模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Context struct {
// HTTP 请求 响应
Writer http.ResponseWriter
Req *http.Request
// 请求信息
Path string // 请求路径
Method string // 请求方法
Params map[string]string // 路由参数,如 /hello/:user 匹配 /hello/dawn,则 Params["user"]=dawn
// 响应信息
StatusCode int // 响应码
// 中间件
handlers []HandlerFunc // 存储中间件
index int // 执行的中间件下标
// 指向 *Engine
engine *Engine
}

同时,修改 c.HTML 方法,使之能够渲染模板:

1
2
3
4
5
6
7
8
9
10
11
12
func (c *Context) Fail(code int, err string) {
c.index = len(c.handlers)
c.JSON(code, H{"msg": err})
}

func (c *Context) HTML(code int, name string, data interface{}) {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
if err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data); err != nil {
c.Fail(http.StatusInternalServerError, err.Error())
}
}

dain/dain.go

因为在 Context 中增加了指向 Engine 的字段,所以需要在 ServeHTTP 中对 c.engine 赋值:

1
2
3
4
5
6
7
8
9
10
11
// 实现 http.Handler 接口,自定义路由器
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := NewContext(w, r)
for _, group := range e.groups {
if strings.HasPrefix(r.URL.Path, group.prefix) {
c.handlers = append(c.handlers, group.middleware...)
}
}
c.engine = e
e.router.handle(c)
}