25

【go语言学习】标准库之template

 3 years ago
source link: https://studygolang.com/articles/31178
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、模板和模板引擎

在基于MVC的web架构中,我们常常将不变的部分提出成为模板,可变部分通过后端程序提供数据,借助模板引擎渲染来生成动态网页。

模板可以理解为事先定义好的HTML文档文件,模板渲染的作用机制可以简单理解为文本替换操作—使用相应的数据去替换HTML文档中事先准备好的标记。

模板的诞生是为了将显示与数据分离(即前后端分离),模板技术多种多样,但其本质是将模板文件和数据通过模板引擎生成最终的HTML代码。

模板引擎很多,Python的jinja,nodejs的jade等都很好。

二、go语言模板引擎

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

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

三、模板引擎的使用

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

1、定义模板

按照相应的语法规则去编写模板

2、解析模板

template包提供了以下方法解析模板,获得模板对象

// 创建模板对象,并为其添加一个模板名称
func New(name string) *Template {}
// 解析字符串 
// 可以使用template.New("name").Parse(src string) 
// 来创建模板对象,并完成解析模板内容。
func (t *Template) Parse(src string) (*Template, error) {}
// ParseFiles 方法可以解析模板文件,并得到模板对象
func ParseFiles(filenames ...string) (*Template, error) {}
// ParseGlob方法用于批量解析文件
// 比如在当前目录下有以h开头的模板10个
// 使用template.ParseGlob("h*")即可页将10个模板文件一起解析出来
func ParseGlob(pattern string) (*Template, error) {}

3、模板渲染

template包提供了以下方法用于渲染模板。

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

使用New在创建时就为其添加一个模板名称,并且执行t.Execute()会默认去寻找该名称进行数据融合。

使用ParseFiles一次指定多个文件加载多个模板进来,就不可以使用t.Execute()来执行数据融合,可以通过t.ExecuteTemplate()方法指定模板名称来执行数据融合。

4、基本示例

定义模板

// go_web/index.tmpl

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
   <p>hello {{ . }} </p> 
</body>
</html>

解析和渲染模板

// go_web/main.go

package main

import (
    "fmt"
    "html/template"
    "net/http"
)

func helloHandleFunc(w http.ResponseWriter, r *http.Request) {
    // 2. 解析模板
    t, err := template.ParseFiles("./index.tmpl")
    if err != nil {
        fmt.Println("template parsefile failed, err:", err)
        return
    }
    // 3.渲染模板
    name := "ruby"
    t.Execute(w, name)
}

func main() {
    http.HandleFunc("/", helloHandleFunc)
    http.ListenAndServe(":8080", nil)
}

四、模板语法

{{}} 包裹的内容统称为 action,分为两种类型:

  • 数据求值(data evaluations)
  • 控制结构(control structures)

action 求值的结果会直接复制到模板中,控制结构和我们写 Go 程序差不多,也是条件语句、循环语句、变量、函数调用等等…

1、注释

{{/* a comment */}}
// 注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止。

2、移除空格

{{ 符号的后面加上短横线并保留一个或多个空格来去除它前面的空白(包括换行符、制表符、空格等),即 {{- xxxx

}} 的前面加上一个或多个空格以及一个短横线 - 来去除它后面的空白,即 xxxx -}}

<p>{{ 20 }} < {{ 40 }}</p> // 20 < 40
<p>{{ 20 -}} < {{- 40 }}</p> // 20<40

3、管道pipeline

pipeline是指产生数据的操作。比如{{.}}、{{.Name}}、funcname args等。

可以使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令将运算结果(或返回值)传递给后一个命令的最后一个位置。

{{"put" | printf "%s%s" "out" | printf "%q"}}  // "output"

4、变量

在golang渲染template的时候,可以接受一个 interface{} 类型的变量,我们在模板文件中可以读取变量内的值并渲染到模板里。

{{}} 中间的 . 代表传入的变量(数据),其代表 当前作用域的当前对象 ,变量(数据)不同渲染不同。

有两个常用的传入变量的类型。一个是 struct ,在模板内可以读取该 struct 的字段(对外暴露的属性)来进行渲染。还有一个是 map[string]interface{} ,在模板内可以使用 key 获取对应的 value 来进行渲染。

示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
   <p>姓名:{{ .Name }}</p> 
   <p>年龄:{{ .Age }}</p>
   <p>性别:{{ .Gender }}</p>
   <p>语文成绩:{{ .Score.yuwen}}</p>
   <p>数学成绩:{{ .Score.shuxue}}</p>
   <p>英语成绩:{{ .Score.yingyu}}</p>
</body>
</html>
package main

import (
    "fmt"
    "html/template"
    "net/http"
)

// User 结构体
type User struct {
    Name   string
    Age    int
    Gender string
    Score  map[string]float64
}

func indexHandleFunc(w http.ResponseWriter, r *http.Request) {
    t, err := template.ParseFiles("./index.tmpl")
    if err != nil {
        fmt.Println("template parsefiles failed, err:", err)
        return
    }
    user := User{
        Name:   "ruby",
        Age:    20,
        Gender: "female",
        Score: map[string]float64{
            "yuwen":  98,
            "shuxue": 100,
            "yingyu": 94,
        },
    }
    t.Execute(w, user)
}

func main() {
    http.HandleFunc("/", indexHandleFunc)
    http.ListenAndServe(":8080", nil)
}

自定义变量

{{ $obj := "jack" }}
{{ $obj }} // 输出:jack

5、函数

golang的模板其实功能很有限,很多复杂的逻辑无法直接使用模板语法来表达,所以只能使用模板函数来实现。

首先,template包创建新的模板的时候,支持.Funcs方法来将自定义的函数集合导入到该模板中,后续通过该模板渲染的文件均支持直接调用这些函数。

该函数集合的定义为:

type FuncMap map[string]interface{}

key为方法的名字,value则为函数。这里函数的参数个数没有限制,但是对于返回值有所限制。有两种选择,一种是只有一个返回值,还有一种是有两个返回值,但是第二个返回值必须是error类型的。这两种函数的区别是第二个函数在模板中被调用的时候,假设模板函数的第二个参数的返回不为空,则该渲染步骤将会被打断并报错。

  • 内置模板函数:
var builtins = FuncMap{
    // 返回第一个为空的参数或最后一个参数。可以有任意多个参数。
    // "and x y"等价于"if x then y else x"
    "and": and,
    // 显式调用函数。第一个参数必须是函数类型,且不是template中的函数,而是外部函数。
    // 例如一个struct中的某个字段是func类型的。
    // "call .X.Y 1 2"表示调用dot.X.Y(1, 2),Y必须是func类型,函数参数是1和2。
    // 函数必须只能有一个或2个返回值,如果有第二个返回值,则必须为error类型。
    "call": call,
    // 返回与其参数的文本表示形式等效的转义HTML。
    // 这个函数在html/template中不可用。
    "html": HTMLEscaper,
    // 对可索引对象进行索引取值。第一个参数是索引对象,后面的参数是索引位。
    // "index x 1 2 3"代表的是x[1][2][3]。
    // 可索引对象包括map、slice、array。
    "index": index,
    // 返回与其参数的文本表示形式等效的转义JavaScript。
    "js": JSEscaper,
    // 返回参数的length。
    "len": length,
    // 布尔取反。只能一个参数。
    "not": not,
    // 返回第一个不为空的参数或最后一个参数。可以有任意多个参数。
    // "or x y"等价于"if x then x else y"。
    "or":      or,
    "print":   fmt.Sprint,
    "printf":  fmt.Sprintf,
    "println": fmt.Sprintln,
    // 以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。
    // 这个函数在html/template中不可用。
    "urlquery": URLQueryEscaper,
}
  • 比较函数:
eq arg1 arg2:
    arg1 == arg2时为true
ne arg1 arg2:
    arg1 != arg2时为true
lt arg1 arg2:
    arg1 < arg2时为true
le arg1 arg2:
    arg1 <= arg2时为true
gt arg1 arg2:
    arg1 > arg2时为true
ge arg1 arg2:
    arg1 >= arg2时为true
  • 自定义模板函数
t = t.Funcs(template.FuncMap{"handleFieldName": HandleFunc})
  • 函数调用
{{funcname .arg1 .arg2}}

6、条件判断

{{ if pipeline }} T1 {{ end }}
{{ if pipeline }} T1 {{ else }} T2 {{ end }}
{{ if pipeline }} T1 {{ else if pipeline }} T2 {{ end }}

7、循环遍历

{{ range pipeline }} T1 {{ end }}
// 如果 pipeline 的长度为 0 则输出 else 中的内容
{{ range pipeline }} T1 {{ else }} T2 {{ end }}

range可以遍历slice、数组、map或channel。遍历的时候,会设置 . 为当前正在遍历的元素。

对于第一个表达式,当遍历对象的值为0值时,则range直接跳过,就像if一样。对于第二个表达式,则在遍历到0值时执行else。

range的参数部分是pipeline,所以在迭代的过程中是可以进行赋值的。但有两种赋值情况:

{{ range $value := pipeline }} T1 {{ end }}
{{ range $key, $value := pipeline }} T1 {{ end }}

如果range中只赋值给一个变量,则这个变量是当前正在遍历元素的值。如果赋值给两个变量,则第一个变量是索引值(array/slice是数值,map是key),第二个变量是当前正在遍历元素的值。

8、with...end

{{ with pipeline }} T1 {{ end }}
{{ with pipeline }} T1 {{ else }} T0 {{ end }}

对于第一种格式,当pipeline不为0值的时候,将 . 设置为pipeline运算的值,否则跳过。

对于第二种格式,当pipeline为0值时,执行else语句块T0,否则 . 设置为pipeline运算的值,并执行T1。

9、模板嵌套

  • define

define可以直接在待解析内容中定义一个模板

// 定义名称为name的template
{{ define "name" }} T {{ end }}
  • template

使用template来执行模板

// 执行名为name的template
{{ template "name" }}
{{ template "name"  pipeline }}
  • block
{{ block "name" pipeline }} T {{ end }}

block等价于define定义一个名为name的模板,并在"有需要"的地方执行这个模板,执行时将 . 设置为pipeline的值。

等价于:先 {{ define "name" }} T {{ end }} 再执行 {{ template "name" pipeline }}

代码示例:

<!-- index.tmpl -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
   {{ template "content"}} 
</body>
</html>
<!-- red.tmpl -->
{{ define "content" }} 
    <div style="color:red"><h3>hello world</h3></div>
{{ end }}
<!-- blue.tmpl -->
{{ define "content" }} 
    <div style="color:blue"><h3>hello world</h3></div>
{{ end }}
// main.go
package main

import (
    "html/template"
    "math/rand"
    "net/http"
    "time"
)

func indexHandleFunc(w http.ResponseWriter, r *http.Request) {
    t := template.New("index.tmpl")
    rand.Seed(time.Now().UnixNano())
    if rand.Intn(100) > 50 {
        t, _ = template.ParseFiles("./index.tmpl", "./red.tmpl")
    } else {
        t, _ = template.ParseFiles("./index.tmpl", "./blue.tmpl")
    }
    t.Execute(w, "")
}
func main() {
    http.HandleFunc("/", indexHandleFunc)
    http.ListenAndServe(":8080", nil)
}

如果使用block,那么可以设置默认的content模板。

修改index.tmpl

<!-- index.tmpl -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    {{ block "content" . }}
    <div style="color:yellow"><h3>hello world</h3></div>
    {{ end }}
</body>
</html>

修改后端程序:

// main.go
package main

import (
    "html/template"
    "math/rand"
    "net/http"
    "time"
)

func indexHandleFunc(w http.ResponseWriter, r *http.Request) {
    t := template.New("index.tmpl")
    rand.Seed(time.Now().UnixNano())
    if rand.Intn(100) > 75 {
        t, _ = template.ParseFiles("./index.tmpl", "./red.tmpl")
    } else if rand.Intn(100) > 25 {
        t, _ = template.ParseFiles("./index.tmpl", "./blue.tmpl")
    } else {
        t, _ = template.ParseFiles("./index.tmpl")
    }
    t.Execute(w, "")
}
func main() {
    http.HandleFunc("/", indexHandleFunc)
    http.ListenAndServe(":8080", nil)
}

10、修改默认的标识符

Go标准库的模板引擎使用的花括号 {{}} 作为标识,而许多前端框架(如Vue和 AngularJS)也使用 {{}} 作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符,修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符:

template.New("test").Delims("{[", "]}").ParseFiles("./t.tmpl")

有疑问加站长微信联系

iiUfA3j.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK