阅读 112

Go Web之模板引擎

这是我参与8月更文挑战的第28天,活动详情查看:8月更文挑战

文章内容来自本人学习《Go Web编程实战派》及自己的实践总结

模版引擎

Go语言中通用的模版引擎库text/template用于处理任意格式的文本。另外,Go语言还单独提供了html/template包,用于生成可对抗代码注入的安全HTML文档

模板原理

模板和模板引擎

在给予MVC模型的Web架构中,我们常将不变的部分提出来成为模版,而那些可变部分有后端程序提供数据,借助模版引擎渲染来生成动态网页

模板可以被理解为事先定义好的HTML文档。模板渲染可以被简单理解为文本替换操作——是后端用相应的数据去替换HTML文档中实现准备好的标记

模板的诞生是为了将显示于数据分离(即前后端分离);模板技术多种多样,但本质是将模板文件和数据通过模板引擎生成最终的HTML文档。模版引擎有很多,比如:Node.js的ejsnunjucks等等

Go语言的模板引擎

Go语言内置了文本模板引擎text/template包,以及用于生成HTML文档的html/template包,它们的使用基本类似,大致总结为以下几点:

  • 模板文件的后缀通常是.tmpl.tpl(也可以使用其他的后缀),必须使用UTF-8编码
  • 模板文件中使用{{}}来包裹和表示需要传入的数据
  • 传给模板的数据可以通过点号.来访问。如果是符合类型的数据,则可以通过{{.FieldName}}来访问它的字段
  • {{}}包裹外,其他的内容均不做任何处理,原样输出

Go语言模版引擎的使用分为:定义模板文件解析模板文件和渲染文件

  1. 定义模板文件

    定义模板文件是指按照相应的语法规则去定义模板文件

  2. 解析模板文件

    html/template包提供了以下方法来解析模板文件、获取模板对象;可以通过New()函数来创建模板对象,并为其挺假一个模板名称。New()函数的定义如下:

    func New(name string) *Template
    复制代码

    可以使用Parse()函数来创建模板对象并完成解析模版内容。Parse()定义方法如下:

    func (t *Template) Parse(src string) (*Template, err)
    复制代码

    如果要解析模板文件,则可以使用ParseFile()函数,该函数会返回模板对象。该函数定义如下:

    func ParseFiles(filenames ...string) (*Template, error)
    复制代码

    如果要批量解析文件,则可以使用ParseGlob()函数。该函数的定义如下:

    func ParseGlob(pattern string) (*Template, error)
    复制代码

    可以使用ParseGlob()函数来进行正则匹配,比如在当前解析目录下有以a开头的模板文件,则使用template.ParseGlob("a*")

  3. 渲染模板文件

    html/template包提供了Execute()ExecteTemplate()方法来渲染模板

    func (t *template) Execute(wr Writer, data interface{}) error {}
    
    func (t *template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error {}
    复制代码

    在创建New()函数时就为模板对象添加了一个模板名称,执行`Execute()方法后会默认去寻找该名称进行数据融合

    使用ParseFile()函数可以一次加载多个模板,此时不可以使用Execute()来执行数据融合,可以通过ExecuteTemplate()方法指定模板名称来执行数据融合

使用html/template包

创建模板

在Go语言中,可以通过将模板应用于一个数据结构(即把该数据结构作为模板的参数)来执行并输出HTML文档

模板在执行时会遍历数据结构,并将指针指向运行中的数据结构中的.的当前位置

用作模板的输入文本必须是UTF-8编码的文本;Action是数据运算和控制单位,Action有{{}}界定;在Action之外的所有文本都不做修改的复制到输出中。Action内部不能有换行,但注释可以有换行

接下来就创建并实践模板吧

  • 新建一个项目example,然后在example中分别创建examp/template/index.tmpl和main.go

  • 创建模板文件

    // example/template/index.tmpl
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>模板</title>
    </head>
    <body>
        <p>加油,少年!{{.}}</p>
    </body>
    </html>
    复制代码
  • 创建用于创建和渲染模板的文件(main.go)

    package main
    
    import (
    	"fmt"
    	"html/template"
    	"net/http"
    )
    
    func helloHandleFunc(w http.ResponseWriter, r *http.Request) {
    
    	// 解析模板
    	tpl, err := template.ParseFiles("./template/index.tmpl")
    	if err != nil {
    		fmt.Println("template parsefile failed, err:", err)
    		return
    	}
    
    	// 渲染模版
    	name := "learn Go"
    	tpl.Execute(w, name)
    }
    func main() {
    	http.HandleFunc("/", helloHandleFunc)
    	http.ListenAndServe(":8082", nil)
    }
    复制代码
  • 在命令行终端输入启动命令(注意,一定是在文件所在路径下)

    go run main.go
    复制代码
  • 在浏览器中访问http://localhost:8082/,最终效果图如下所示:

image-20210809065158135.png

Go语言模板语法

模板语法都包含在{{}}中间,其中.表示当前对象,在传入一个结构体对象时,可以根据.来访问结构体的对应字段,例如来写一个个人信息的模板渲染:

// example/template/user.tpml
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>模板</title>
  </head>
  <body>
    <table border="1">
      <thead>
          <th rowspan="2">个人信息</th>
      </thead>
      <tr>
        <td>nickname</td>
        <td>{{.Nickname}}</td>
      </tr>
      <tr>
        <td>age</td>
        <td>{{.Age}}</td>
      </tr>
      <tr>
        <td>gender</td>
        <td>{{.Gender}}</td>
      </tr>
      <tr>
        <td>email</td>
        <td>{{.Email}}</td>
      </tr>
    </table>
  </body>
</html>
复制代码

完善解析模板的文件及数据

// example/main.go
type User struct {
	Nickname string
	Gender string
	Age int
	Email string
}

func handleSayHi(w http.ResponseWriter, r *http.Request){
	// 解析模板
	tpl, err := template.ParseFiles("./template/user.tmpl")
	if err != nil {
		fmt.Println("create template failed, err:", err)
		return
	}

	// 利用给定数据渲染模版,并将结果写入w
	user := User{
		Nickname: "Forest",
		Gender: "man",
		Age: 22,
		Email: "767425412@qq.com",
	}
	tpl.Execute(w, user)
}
复制代码

重新运行命令启动程序,然后在浏览器中访问:http://localhost:8082/say-hi,效果图如下:

image-20210809071300780.png

在传入的变量时map时,也可以在模板文件中通过.来访问值

常用模板语法

  • 注释

    在Go语言中,HTML模板的注释结构如下:

    {{/* 这是一个注释,不会被解析 */}}
    复制代码
  • 管道(pipeline)

    管道是指产生数据的操作;比如:{{.}}{{.Nickname}}等,Go的模板语法中支持用管道符|来链接多个命令,用法和UNIX下的管道类似:|前面的命令会将晕眩结果(或返回值)传递给后一个命令的最后一个位置

    ::: tip 注意

    并不是只有使用|才是管道,在Go的模板语法中,pipeline的概念时传递数据,只要能产生数据的结构,都是pipeline

    :::

  • 变量

    在Action里可以初始化一个变量来捕获管道的执行结果。初始化语法如下:

    $variable := pipline
    复制代码

    其中$variable是变量的名字。声明变量的Action不会产生任何输出

  • 条件判断

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

    {{if pipline}} T1 {{end}}
    {{if pipline}} T1 {{else}} T0 {{end}}
    {{if pipline}} T1 {{else if pipline}} T0 {{end}}
    复制代码
  • renge关键字

    在Go的模板语法中,使用range关键字进行遍历,其中pipline的值必须是数组、切片、map或者channel。其语法以{{range pipline}}开头,以{{end}}结尾,形式如下:

    {{range pipline}} T1 {{end}}
    复制代码

    如果pipline的值值其长度为0,则不会有任何输出。中间也可以有{{else}},如下:

    {{range pipline}} T1 {{else}} T0 {{end}}
    复制代码

    如果pipline的值其长度为0则会执行T0

    示例如下:

    package main
    
    import (
    	"html/template"
    	"log"
    	"net/http"
    )
    
    func handleRange(w http.ResponseWriter, r *http.Request) {
    	rangeTemplate := `<p>{{if .Kind}}</p>
    	<p>{{range $i, $v := .MapContent}}</p>
    	<p>{{$i}} => {{$v}},{{$.OutsideContent}}</p>
    	<p>{{end}}</p>
    	<p>{{else}}</p>
    	<p>{{range .MapContent}}</p>
    	<p>{{.}}, {{$.OutsideContent}}</p>
    	<p>{{end}}</p>
    	<p>{{end}}</p>`
    
    	str1 := []string{"first range", "use index and value"}
    	str2 := []string{"second range", "not use index and value"}
    
    	type Content struct {
    		MapContent     []string
    		OutsideContent string
    		Kind           bool
    	}
    
    	var content = []Content{
    		{str1, "第一次外面的内容", true},
    		{str2, "第二次外面的内容", false},
    	}
    
    	// 创建模板并将字符解析进去
    	t := template.Must(template.New("range").Parse(rangeTemplate))
    	fmt.Println("t:", t)
    	for _, c := range content {
    		err := t.Execute(w, c)
    		if err != nil {
    			log.Println("executing template:", err)
    		}
    	}
    }
    
    func main() {
    	http.HandleFunc("/range", handleRange)
    	http.ListenAndServe(":8082", nil)
    }
    复制代码

    重新运行程序后,访问浏览器效果图如下:

    image-20210811072640545.png

  • with关键字

    在Go的模板语法中,with关键字和if关键字有点类似,{{with}}操作尽在传递的管道部位空时有条件地执行其主体。如下:

    {{with pipline}} T1 {end}
    复制代码

    如果pipline为空,则不产生输出。中间也可以{{else}};例如:

    {{with pipline}} T1 {{else}} T0 {{end}}
    复制代码

    如果pipline为空,则不改变.并执行T0;否则将.设为pipline的值并执行T1

  • 比较函数

    布尔函数会将任何类型的零值是为假,将其余视为真。例如下面常用的二元比较运算符:

    运算符说明
    eq相等判断;例如:arg1 == arg2
    ne不相等判断;例如:arg1 != arg2
    lt小于判断;例如:arg1 < arg2
    le小于等于判断;例如:arg1 <= arg2
    gt大于判断;例如:arg1 > arg2
    ge大于等于判断;例如:arg1 >= arg2

    为简化多参数相等检测,eq可以接受2个或者多个参数,它将第一个参数和其余参数依次比较,如下:

    {{eq arg1 arg2 arg3 ... argn}}
    复制代码

    即只能做如下比较

    arg1 ==arg2 || arg1 == arg2 || ... || arg1 == argn
    复制代码

    比较函数只适用基本类型(或在重定义的基本类型,如"type Balance float32")。但整数和浮点数不能互相比较

  • 预定义函数

    预定义函数是模板库中定义好的函数,可以直接在{{}}中使用,预定义函数名及其功能见下表:

    函数名说明
    and函数返回其第1个空参数或者最后一个参数,即:"and x y"等价于"if x then y else x"。所有参数都会执行
    or返回第一个非空参数或者最后一个参数,即"or x y"等价于"if x then x else y"。所有参数都会执行
    not返回其单个参数的布尔值“不是”
    len返回其参数的整数类型长度
    index执行结果为index()函数后第一个参数以第一个参数后面剩下的参数为索引指向的位置。每个被索引的主体必须是数组、切片、map
    printfmt.Sprint
    printffmt.Sprintf
    printlnfmt.Sprintln
    html返回其参数文本表示的HTML逸码等价表示
    urlquery返回其参数文本表示的可嵌入URL查询的逸码等价表示
    js返回其参数文本表示的JavaScript译码等价表示
    call执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数
  • 自定义函数

    Go语言的模板支持自定义函数。自定义函数通过调用Funcs()方法实现,其定义如下:

    func (t *Template) Funcs(funcMap FuncMap) *Template
    复制代码

    Funcs()方法向模板对象的函数字典里加入参数funcMap内的键值对。如果funcMap的某个键值对的值不是函数类型,或者返回值不符合要求,则会报panic错误,但可以对模板对象的函数列表的成员进行重写。方法返回模板对象以便进行链式调用。FuncMap的定义如下:

    type FuncMap map[string]interface{}
    复制代码

    FuncMap类型定义了函数名字符串到函数的映射,每个函数都必须有1个或者2个返回值。如果有两个返回值,则后一个必须是error接口类型;如果有2个返回值的方法返回errornil,则模板执行会中断并返回该错误给调用者

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

文章分类
后端
文章标签