Go 视图模板篇(一):模板引擎的定义、解析与执行


模板和模板引擎

在 Web 编程中,模板引擎用于聚合数据和模板并生成最终的 HTML 文档,处理器调用模板引擎来完成这一工作并将 HTML 文档作为响应实体发送给客户端:

-w603

虽然模板引擎没有统一的标准,甚至不同的模板引擎提供的功能特性也是天差地别,但是仍然可以划分为两种不同的类型:

  • 无业务逻辑:数据通过指定占位符替换,模板中不包含业务逻辑,所有业务逻辑都在处理器中完成,这样做的好处是将业务逻辑和数据渲染很好的隔离开。
  • 嵌入业务逻辑:在视图模板中嵌入业务逻辑,这使得视图模板的功能非常强大,但是这样一来,也使得代码维护非常困难。

我们倾向于无业务逻辑嵌入的模板引擎,这样的视图模板性能更好,可维护性更好,但是绝对的无业务逻辑嵌入也是做不到的(比如一些简单的条件判断和循环),大部分时候这取决于业务开发团队的约定,尽量不要在视图模板中编写业务逻辑代码。

PHP 诞生之初就是一个将业务逻辑和 HTML 视图混为一体的脚本语言,不过现在的 PHP 脚本中已经很少看到 HTML 代码了,这是 PHP 框架的功劳,比如 Laravel、Yii,PHP 自身作为一个模板引擎,现在已经衍生出独立的模板引擎,比如 Smarty、Blade。

实际上,大部分模板引擎都是介于以上两种类型之间,只能说离谁更近一些。

Go 语言官方提供的模板引擎 text/templatehtml/template 也是这样的混合物。

Go 模板引擎

Go 模板引擎都是在处理器中触发,指定要解析的模板文件,并传入待渲染的数据,最后返回由模板引擎最终生成的 HTML 作为 HTTP 响应发送给客户端:

-w597

Go 模板都是文本文档,在 Web 应用中,通常是 HTML 文档,其中包含了嵌入的命令。这些文档会被 Go 模板引擎解析和执行,生成另外的文本片段(替换完命令和数据)。

Go 标准库提供了 text/template 库用于解析任意类型的文本格式模板,以及 html/template 库用于解析并处理 HTML 格式模板。

在这些模板中,命令以 {{}} 包裹(实际上,这些界定符可以通过程序进行修改),下面我们看一段简单的模板代码 tmpl.html

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
		<title>Go Web Programming</title>
	</head>
	<body>
		{{ . }}
	</body>
</html>

模板文件中的内容必须是可读的文本格式,但是扩展名可以随意,在这里由于最终生成的是 HTML 文档,所以我们使用了 .html 扩展名(tmpl.html)。

{{ . }} 中的 . 就是一个命令,用于在模板执行时替换从处理器传入的变量。

使用 Go 模板引擎通常包括以下两个步骤:

  • 解析文本模板源,可以是表单字符串、或者模板文件,用于创建解析后的模板结构体。
  • 执行解析后的模板,传递 ResponseWriter 和变量数据,这样一来,模板引擎就可以基于模板和数据生成最终的 HTML 并将其传递给 ResponseWriter 发送给客户端。

下面是服务端处理器调用模版引擎渲染上述模板代码的示例:

package main
    
import (
    "html/template"
    "net/http"
)
    
func process(w http.ResponseWriter, r *http.Request)  {
    t, _ := template.ParseFiles("tmpl.html") // 解析模板,返回 Template
    t.Execute(w, "Hello World!")  // 执行模板,并将其传递给 w
}
    
func main()  {
    http.HandleFunc("/template", process)
    http.ListenAndServe(":8080", nil)
}

运行上述代码启动服务器,在终端窗口通过 curl 请求 /template 路由,返回结果如下:

-w767

表明 HTML 模板解析渲染成功,对应的 {{ . }} 也别替换成 Hello World!

解析模板

在上面的示例代码中,我们调用了 ParseFiles 方法解析模板文件并创建稍后执行的解析后的 Template。其底层分为两步,它可以接收一个或多个模板文件名称,传入多个模板文件名的时候,会以第一个文件名作为模板名称,后续其它模板通常是第一个模板或者其他模板嵌套的子模板。

此外,我们还可以通过 ParseGlob 方法解析模板,该方法传入的参数是模式匹配串,而不是文件名称:

t, _ := template.ParseFiles("tmpl.html")
t, _ := template.ParseGlob("*.html")

如果当前路径只有一个 tmpl.html 模板文件,上述代码的效果是一样的。

除了解析文件之外,还支持解析字符串,实际上,所有解析方法最终调用的都是 Parse 方法:

package main
    
import (
    "html/template"
    "net/http"
)
    
func parseFiles(w http.ResponseWriter, r *http.Request)  {
    t, _ := template.ParseFiles("tmpl.html")
    t.Execute(w, "Hello World!")
}
    
func parseString(w http.ResponseWriter, r *http.Request)  {
    tmpl := `<!DOCTYPE html> <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
            <title>Go Web Programming</title>
        </head>
        <body>
            {{ . }}
        </body> 
    </html>`
    
    t := template.New("tmpl.html")
    t.Parse(tmpl)
    t.Execute(w, "Hello World!")
}
    
func main()  {
    http.HandleFunc("/template", parseFiles)
    http.HandleFunc("/string", parseFiles)
    http.ListenAndServe(":8080", nil)
}

上面 parseString 方法和 parseFiles 方法实现的效果是一样的,实际上 template.ParseFiles("tmpl.html") 底层调用的代码正是:

t := template.New(filename)
t.Parse(string) // string 就是读取传入 file 的文本内容

在上面的代码中,我们忽略了 template.ParseFiles 返回的错误信息,不过,Go 官方建议我们对这个错误进行处理,为此,Go 还提供了更简洁的方式来处理模板解析过程中出现的错误:

t := template.Must(template.ParseFiles("tmpl.html"))

这种情况下,如果解析模板过程中出现问题,则抛出 panic(在 Go 语言中,panic 有点类似其它语言的异常,当函数内抛出 panic 时,会一直上溯到 main 入口,然后崩溃)。

执行模板

如果只解析一个模板文件的话,使用 Execute 方法就够了,如果要解析多个模板文件,也可以使用 Execute 方法,这个时候,会使用传入模板文件的第一个作为模板名称,并将其作为入口模板,如果要指定其它模板作为入口模板(或者称之为布局模板),需要调用 ExecuteTemplate 方法并将模板名作为第二个参数传递进去:

t, _ := template.ParseFiles("t1.html", "t2.html")
t.Execute(w, "Hello World!")
t.ExecuteTemplate(w, "t1.html", "Hello World!")

上面的 t.Executet.ExecuteTemplate 执行结果等效,如果你要从 t2.html 开始解析,需要这样指定入口文件:

t.ExecuteTemplate(w, "t2.html", "Hello World!")

点赞 取消点赞 收藏 取消收藏

<< 上一篇: 基于 gorilla/sessions 包在 Go 语言中启动和管理 Session

>> 下一篇: Go 视图模板篇(二):通过指令实现控制结构和模板引入