html/templatetext/template

模板与渲染

在一些前后端分离的Web架构中, 我们通常需要在后端将一些数据渲染到HTML文档中, 从而实现动态的网页(网页的布局和样式大致一样, 但展示的内容并不一样) 效果.

我们这里说的模板可以理解为事先定义好的HTML文档文件, 模板渲染的左右机制可以简单理解为文本替换操作---使用相应的数据去替换HTML文档中实现准备好的标记.

很多编程语言的Web框架中都使用各种模板引擎, 比如Python语言中Flask框架中使用jinja2模板引擎.


Go语言的模板引擎

Go语言内置了文本模板引擎text/template和用于HTML文档的html/templant. 它们的作用机制可以简单归纳如下:

  1. 模板文件通常定义为.tmpl和.tpl为后缀(也可以使用其他后缀), 必须使用utf-8编码.
  2. 模板文件中使用{{和}}包裹和标识需要传入的数据.
  3. 传给模板这样的数据就可以通过点号(.)来访问,如果数据是复杂类型的数据, 可以通过{{.FieldName}}来访问它的字段.
  4. 除{{和}}包裹的内容外, 其他内容均不做修改原样输出.

模板引擎的使用

Go语言模板引擎的使用可以分文三部分: 定义模板文件/解析模板文件/渲染模板文件

定义模板文件

其中, 定义模板文件时需要我们按照相关语法规则去编写, 后文会详细介绍.

解析模板文件

上面定义好了模板文件之后,可以使用下面的常用方法去解析模板文件, 得到模板对象:

func (t *Template) Parse(src string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)
func ParseGlob(pattern string) (*Template, error)
func New(name string) *Templatename
模板渲染

渲染模板简单来说就是使用数据去填充模板, 当然实际上可能会复杂很多.

func(t *Template) Execute(wr io.Writer, data interface) error
func(t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
基本示例

定义模板文件

hello.tmpl
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=devie-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
    <p>Hello {{.}}<p>
</body>
<html>

解析模板和渲染模板文件

main.go
// main.go
func sayHello(w http.ResponseWriter, r *http.Request) {
    // 解析指定文件生成模板对象
    tmpl, err := template.ParseFiles("./hello.tmpl")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    // 利用给定数据渲染模板, 并将结果写入w
    tmpl.Execute(w, "小明")
}

func main() {
    http.HandleFunc("/", sayHello)
    err := http.ListenAndServe(":9090", nil)
    if err != nil {
        fmt.Println("HTTP SERVER failed,err:", err)
        return
    }
}
main.gohttp://127.0.0.1:9090"Hello 小明"

模板语法

{{.}}
{{}}{{.}}
.
// main.go
type UserInfo struct {
    Name string
    Gender string
    Age int
}

func sayHello(w http.ResponseWriter, r *http.Request) {
    // 解析指定文件生成模板对象
    tmpl, err := template.ParseFiles("./hello.tmpl")
    if err != nil {
        fmt.Println("create template failed,err:", err)
        return
    }
    // 利用给定数据渲染模板, 并将结果写入w
    user := UserInfo{
        Name: "小明",
        Gender: "男",
        Age: 18,
    }
    tmpl.Execute(w, user)
}
hello.tmpl
<!DOCTYPE html>
<html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Hello</title>
    </head>
    <body>
        <p>Hello {{.Name}}</p>
        <p>性别:{{.Gender}}</p>
        <p>年龄:{{.Age}}</p>
    </body>
</html>
.
注释
{{/* a comment */}}
注释, 执行时会忽略. 可以多行. 只是不能嵌套, 并且必须紧贴分界符始止.
pipeline
{{.}}{{.Name}}||
|
变量

我们还可以在模板中声明变量, 用来保存传入模板的数据或其他语句生成的结构. 具体语法如下:

$obj := {{.}}
$obj
移除空格​
{{--}}

例如:

{{- .Name -}}
-{{}}
条件判断

Go模板语法中的条件判断有以下几种:

{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
range
rangepipelien
{{range pipeline}}T1{{end}}
如果pipeline的值其长度为0, 不会有任何输出
{{range pipeline}}T1{{else}}T0{{end}}
如果pipeline为empty, 不改变dot并执行T0,否则dot设置为pipeline的值并执行T1.
with
{{with pipeline}}T1{{end}}
如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1.不修改外面的dot.
{{with pipeline}}T1{{else}}T0{{end}}
如果pipeline为empty, 不改变dot并执行T0,否则dot设为pipeline的值并执行T1.
预定义函数

执行模板时, 函数从两个函数字典中查找: 首先是模板函数字典, 然后是全局函数字典. 一般不在模板内定义函数, 而是使用Funcs方法添加函数到模板里.

预定义的全局函数如下:

and
    函数返回它的第一个empty参数或者最后一个参数;
    就是说`and x y`等价于`if x then y else x`;所有参数都会执行;
or
    返回第一个非empty参数或者最后一个参数;
    即`or x y`等价于`if x then x else y`; 所有参数都会执行;
not
    返回它的单个参数的布尔值的否定
len 
    返回它的参数的整数类型长度
index
    执行结果为第一个参数以剩下的参数为索引/键指向的值;
    如`index x 1 2 3`返回x[1][2][3]的值; 每个被索引的主体必须是数组,切片,或者字典.
print
    即fmt.Sprint
printf
    即fmt.Sprintf
println
    即fmt.Sprintln
html
    返回与其参数的文本表示形式等效的转移HEML.
    这个函数在html/template中不可用
urlquery
    以适合嵌套到网址查询中的形式返回其参数的文本表示的转义值.
    这个函数在html/template中不可用
js
    返回与其参数的文本表示形式等效的转义javaScript.
call
    执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
    如`call .X.Y 1 2`等价于Go语言里的dot.X.Y(1,2);
    其中Y是函数类型的字段或者字典的值,或者是其他类似情况;
    call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
    该函数类型值必须有1到2个返回值,如果有2个返回值则后一个必须是error接口类型;
    如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板的执行者该错误;
比较函数

布尔函数会将任何类型的零值视为假, 其余视为真.

下面是定义为函数的二元比较运算的集合;

eq      如果arg1 == arg2则返回真
ne      如果arg1 != arg2则返回真
lt      如果arg1 < arg2则返回真
le      如果arg1 <= arg2则返回真
gt      如果arg1 > arg2则返回真
ge      如果arg1 >= arg2则返回真

为了简化多参数相等检测,eq(只有eq) 可以接受2个或更多个参数, 它会将第一个参数和其余参数依次比较,返回下式的结果:

{{eq arg1 arg2 arg3}}

比较函数只适用于基本类型(或重定义的基本类型, 如`type Celsius float32). 但是,整数和浮点数不能互相比较.

自定义函数

Go的模板支持自定义函数.

func sayHello(w http.ResponseWriter, r *http.Request) {
    htmlByte, err := ioutil.ReadFile("./hello.tmpl")
    if err != nil {
        fmt.println("read html failed,err:",err)
    }
    // 自定义一个夸人的模板函数
    kua := func(arg string)(string,error) {
        return arg + "真厉害",nil
    }
    // 采用链式操作在Parse之前调用Func添加自定义的kua函数
    tmpl,err := templage.New("hello").
                Funcs(temolate.FuncMap{"kua":kua}).
                Parse(string(htmlByte))
    if err != nil {
        fmt.println("create template failed,err:",err)
        return
    }
    user := UserInfo{
        Name: "小明",
        Gender: "男",
        Age: 18,
    }
    // 使用user渲染模板,并将结果写入w
    tmpl.Execute(w,user)
}
hello.tmplkua
{{kua .Name}}
嵌套template
define
t.tmpl
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>tmpl test</title>
</head>
<body>

    <h1>测试嵌套template语法</h1>
    <hr>
    {{template "ul.tmpl"}}
</body>
</html>

{{ define "ol.tmpl"}}
<ol>
    <li>吃饭</li>
    <li>睡觉</li>
    <li>打豆豆</li>
</ol>
{{end}}
ul.tmpl
<ul>
    <li>注释</li>
    <li>日志</li>
    <li>测试</li>
</ul>
tmplDemo
http.HandleFunc("/tmpl", tmplDemo)
tmplDemo
func tmplDemo(w http.ResponseWriter, r *http.Request) {
    tmpl, err := template.ParseFiles("./t.tmpl", "./ul.tmpl")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    user := UserInfo {
        Name: "小明",
        Gender: "男",
        Age: 12,
    }
    tmpl.Execute(w, user)
}
t.tmplul.tmplul.tmplt.tmpl
block
{{block "name" pipeline}} T1 {{end}}
block{{define "name”}} T1 {{end}}{{template “name” pipeline}}
templates/base.tmpl
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Go Templates</title>
</head>
<body>
<div class="container-fluid">
    {{block "content" . }}{{end}}
</div>
</body>
</html>
templates/index.tmplbase.tmpl
{{template "base.tmpl"}}

{{define "content"}}
    <div>Hello world!</div>
{{end}}
template.ParseGlobExecuteTemplate
func index(w http.ResponseWriter, r *http.Request){
    tmpl, err := template.ParseGlob("templates/*.tmpl")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    err = tmpl.ExecuteTemplate(w, "index.tmpl", nil)
    if err != nil {
        fmt.Println("render template failed, err:", err)
        return
    }
}
index.tmpl
template.ParseGlob(“templates/**/*.tmpl”)
修改默认标识符
{{}}VueAngularJS{{}}
template.New("test").Delims("{[", "]}").ParseFiles("./t.tmpl")

text/template与html/template的区别

html/template

例如,我定义下面的模板文件:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
    {{.}}
</body>
</html>
html/templatealert('嘿嘿嘿')html/template
template.HTML
func xss(w http.ResponseWriter, r *http.Request){
    tmpl,err := template.New("xss.tmpl").Funcs(template.FuncMap{
        "safe": func(s string)template.HTML {
            return template.HTML(s)
        },
    }).ParseFiles("./xss.tmpl")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    jsStr := `<script>alert('嘿嘿嘿')</script>`
    err = tmpl.Execute(w, jsStr)
    if err != nil {
        fmt.Println(err)
    }
}

这样我们只需要在模板文件不需要转义的内容后面使用我们定义好的safe函数就可以了。

{{ . | safe }}