这一章节的内容是介绍 Web 框架如何支持服务端渲染的场景
- 实现静态资源服务(Static Resource)。
- 支持HTML模板渲染。
这一章节很多内容是基于net/http库的,该库已经实现了很多静态文件和HMML模板的相关功能的了。
静态文件
网页的三剑客,JavaScript、CSS 和 HTML。要做到服务端渲染,第一步便是要支持 JS、CSS 等静态文件。
http.FileServer
为了方便使用像JavaScript、CSS和图像这样的静态资源,net/http内置了http文件服务器http.FileServer。
func main() {
fs := http.FileServer(http.Dir("./static"))
http.Handle("/assets/", http.StripPrefix("/assets/", fs))
http.ListenAndServe("localhost:10000", nil)
}
我们使用了内置的http.FileServer
,并将其指向url路径。为了使文件服务器正常工作,它需要知道从哪里提供文件。第二行代码就是告知静态文件是在路径./static。http.FileServer()
方法返回的是 fileHandler
实例,而 fileHandler
结构体实现了 Handler
接口的方法 ServeHTTP()
。
一旦我们的文件服务器就位,我们只需要将一个url路径指向它,就像我们对动态请求所做的一样。需要注意的一点是:为了正确地提供文件,我们需要去掉url路径的一部分。通常这是我们文件所在目录的名称。这是第三行代码http.StripPrefix的操作。
比如当前静态文件是在./static路径中,该路径有文件gee.js。
而用户访问localhost:10000/assets/gee.js。这时服务器就会把/assets/gee.js变成/gee.js,那就是访问./static/gee.js。这是正确的,而不是去访问./static/assers/gee.js。
静态文件Web服务器
找到文件后,如何返回这一步,net/http
库已经实现了。因此,该web 框架要做的,仅仅是解析请求的地址,映射到服务器上文件的真实地址,交给http.FileServer
处理就好了。
来看看使用方式
r := gee.New()
r.Static("/assets", "/usr/static") //"/usr/static"是静态文件的存放路径
// 或相对路径 r.Static("/assets", "./static")
r.Run(":10000")
r.Static就相当于r.GET。用户访问localhost:10000/assets/js/gee.js
,最终会返回/usr/static/js/gee.js
。
代码实现HTML 模板渲染
// 静态文件服务器
func (group *RouterGroup) Static(relativePath string, root string) {
handler := group.createStaticHandler(relativePath, http.Dir(root))
urlPath := path.Join(relativePath, "/*filepath")
group.GET(urlPath, handler)
}
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.Wrtier, c.Req)
}
}
HTML 模板渲染
Go语言内置了text/template
和html/template
2个模板标准库,其中html/template为 HTML 提供了较为完整的支持。包括普通变量渲染、列表渲染、对象渲染等。该web框架的模板渲染直接使用了html/template
提供的能力。
html/template库
这里需要简单介绍下html/template库的函数使用,该流程主要是
构建模板对象New()-->解析数据Parse()-->应用Execute()。
新建一个模板
使用func
(*Template) New(name string) *Template
新建一个模板,并指定模板的名称。
比如:tpl := template.New("test")
文件模板解析:ParseFiles和ParseGlob
ParseFiles接受一个字符串,字符串的内容是一个模板文件的路径(绝对路径or相对路径)。
ParseGlob也差不多,是用正则的方式匹配多个文件。
假设一个目录里有a.txt b.txt c.txt的话,用ParseFiles需要写3行对应3个文件,如果有一百个文件呢?
而用ParseGlob只要写成template.ParseGlob("*.txt") 即可
模板的输出,ExecuteTemplate和Execute
模板下有多套模板,其中有一套模板是当前模板
可以使用Name的方式查看当前模板
err = tmpl.ExecuteTemplate(os.Stdout, "a.html", sweaters) //指定模板名,这次为a.html
err = tmpl.Execute(os.Stdout, sweaters) //模板名省略,打印的是当前模板
添加模板函数
模板文件中支持函数操作,我们可以使用func (t *Template) Funcs(funcMap FuncMap) *Template
方法给模板添加函数。
函数Must,初始化简便
Must函数用于包装返回(*Template, error)的函数/方法调用。它会自动在有err的时候panic,无错的时候只返回其中的*Template,
一般用于变量初始化。这在赋值给变量的时候非常简便。
比如:var t = template.Must(template.New("name").Parse("html"))
模板功能添加入框架
了解了上面的函数使用,之后,就可以在web框架中添加template功能。
type Engine struct {
*RouterGroup
router *router
gorups []*RouterGroup
htmlTemplates *template.Template
funcMap template.FuncMap
}
func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
engine.funcMap = funcMap
}
func (engine *Engine) LoadHTMLGlob(path string) {
engine.htmlTemplates = template.Must(template.New("").Funcs(engine.funcMap).ParseGlob(path))
}
首先为 Engine 添加了 *template.Template
和 template.FuncMap
对象,前者将所有的模板加载进内存,后者是所有的自定义模板渲染函数。
另外,也给用户分别提供了设置自定义渲染函数funcMap
和加载模板LoadHTMLGlob的方法。
注意:从这也看出来,要先使用SetFuncMap方法,才能使用LoadHTMLGlob。
接下来,对原来的 (*Context).HTML()
方法做了些小修改,使之支持根据模板文件名选择模板进行渲染。
type Context struct {
// ...
// engine pointer
engine *Engine //新添加的,为了HTMl方法中能访问到engine.htmlTemplates
}
// func (c *Context) HTML(code int, html string) {
func (c *Context) HTML(code int, name string, data any) {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
if err := c.engine.htmlTemplates.ExecuteTemplate(c.Wrtier, name, data); err != nil {
c.Fail(500, err.Error())
}
//以前的做法
// c.SetHeader("Content-Type", "text/html")
// c.Status(code)
// c.Wrtier.Write([]byte(html))
}
在Context.HTML方法中要想能使用到templates,那么就需要能访问到Engine。那我们在 Context
中添加了成员变量 engine *Engine
,这样就能够通过 Context 访问 Engine 中的 HTML 模板。实例化 Context 时,还需要给 c.engine
赋值。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
.............................
c := newContext(w, req)
c.midHandlers = middlewares
c.engine = engine //给 c.engine 赋值
engine.router.handle(c)
}
测试
该文件目录结构
代码
<!-- templates/custom_func.tmpl -->
<html>
<body>
<p>hello, {{.title}}</p>
<p>Date: {{.now | FormatAsDate}}</p>
</body>
</html>
type student struct {
Name string
Age int8
}
func FormatAsDate(t time.Time) string {
year, month, day := t.Date()
return fmt.Sprintf("%d-%02d-%02d", year, month, day)
//return fmt.Sprintf("%d-%02d-%d", year, month, day)
}
func main() {
r := gee.New()
r.Use(gee.Logger())
//设置自定义渲染函数funcMap,custom_func.tmpl文件中的FormatAsDate格式就是FormatAsDate函数返回的格式
r.SetFuncMap(template.FuncMap{
"FormatAsDate": FormatAsDate,
})
r.LoadHTMLGlob("templates/*")
r.Static("/assets", "./static")
stu1 := &student{Name: "Geektutu", Age: 20}
stu2 := &student{Name: "Jack", Age: 22}
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "css.tmpl", nil)
})
r.GET("/students", func(c *gee.Context) {
c.HTML(http.StatusOK, "arr.tmpl", gee.H{
"title": "gee",
"stuArr": [2]*student{stu1, stu2},
})
})
r.GET("/date", func(c *gee.Context) {
c.HTML(http.StatusOK, "custom_func.tmpl", gee.H{
"title": "gee",
"now": time.Date(2023, 12, 5, 0, 0, 0, 0, time.UTC),
})
})
r.Run("localhost:10000")
}
完整代码:https://github.com/liwook/Go-projects/tree/main/gee-web/6-template