一、模板與渲染
在一些前后端不分離的Web架構中,我們通常需要在后端將一些數據渲染到HTML文檔中,從而實現動態的網頁(網頁的布局和樣式大致一樣,但展示的內容並不一樣)效果。
我們這里說的模板可以理解為事先定義好的HTML文檔文件,模板渲染的作用機制可以簡單理解為文本替換操作–使用相應的數據去替換HTML文檔中事先准備好的標記。
很多編程語言的Web框架中都使用各種模板引擎,比如Python語言中Flask框架中使用的jinja2模板引擎。
二、Go語言的模板引擎
Go語言內置了文本模板引擎text/template
和用於HTML文檔的html/template
。它們的作用機制可以簡單歸納如下:
-
模板文件通常定義為
.tmpl
和.tpl
為后綴(也可以使用其他的后綴),必須使用UTF8
編碼。 -
模板文件中使用
{{
和}}
包裹和標識需要傳入的數據。 -
傳給模板這樣的數據就可以通過點號(
.
)來訪問,如果數據是復雜類型的數據,可以通過{ { .FieldName }}來訪問它的字段。 -
除
{{
和}}
包裹的內容外,其他內容均不做修改原樣輸出。
三、模板引擎的使用
Go語言模板引擎的使用可以分為三部分:定義模板文件、解析模板文件和模板渲染.
1. 定義模板文件
其中,定義模板文件時需要我們按照相關語法規則去編寫,后文會詳細介紹。
2. 解析模板文件
上面定義好了模板文件之后,可以使用下面的常用方法去解析模板文件,得到模板對象:
func (t *Template) Parse(src string) (*Template, error) func ParseFiles(filenames ...string) (*Template, error) func ParseGlob(pattern string) (*Template, error)
當然,你也可以使用func New(name string) *Template
函數創建一個名為name
的模板,然后對其調用上面的方法去解析模板字符串或模板文件。
渲染模板簡單來說就是使用數據去填充模板,當然實際上可能會復雜很多。
func (t *Template) Execute(wr io.Writer, data interface{}) error func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
4. 基本示例
4.1 定義模板文件
我們按照Go模板語法定義一個hello.tmpl
的模板文件,內容如下:
<!doctype html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Hello</title> </head> <body> <p>Hello {{ . }}</p> </body> </html>
4.2 解析和渲染模板文件
然后我們創建一個main.go
文件,在其中寫下HTTP server端代碼如下:
package main import ( "html/template" "log" "net/http" ) func sayHello(w http.ResponseWriter, r *http.Request) { // 解析模板 t,err := template.ParseFiles("./hello.tmpl") if err!=nil { log.Println("Parse template failed, err%v", err) return } // 渲染模板 name := "飛哥哥" err = t.Execute(w, name) if err!=nil { log.Println("render template failed, err%v", err) return } } func main() { http.HandleFunc("/", sayHello) err := http.ListenAndServe(":9000", nil) if err!=nil { log.Println("http server start failed,err:%v",err) } }
將上面的main.go
文件編譯執行,然后使用瀏覽器訪問http://127.0.0.1:9000
就能看到頁面上顯示了“Hello 沙河小王子”。 這就是一個最簡單的模板渲染的示例,Go語言模板引擎詳細用法請往下閱讀。
四、模板語法
1. {{.}}
模板語法都包含在
{{
和}}
中間,其中{{.}}
中的點表示當前對象。當我們傳入一個結構體對象時,我們可以根據.
來訪問結構體的對應字段。例如:package main import ( "html/template" "log" "net/http" ) type User struct { Name string Gender string Age int } func sayHello(w http.ResponseWriter, r *http.Request) { // 解析模板 t,err := template.ParseFiles("./hello.tmpl") if err!=nil { log.Println("Parse template failed, err%v", err) return } // 渲染模板 // 渲染字符串 name := "飛哥哥" //err = t.Execute(w, name) // 渲染結構體 user := User{ Name:name, Gender:"男", Age:23, } err = t.Execute(w, user) if err!=nil { log.Println("render template failed, err%v", err) return } } func main() { http.HandleFunc("/", sayHello) err := http.ListenAndServe(":9000", nil) if err!=nil { log.Println("http server start failed,err:%v",err) } }模板文件
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>年齡:{{.Name}}</p> </body> </html>渲染map類型
func sayHello(w http.ResponseWriter, r *http.Request) { // 解析模板 t,err := template.ParseFiles("./hello.tmpl") if err!=nil { log.Println("Parse template failed, err%v", err) return } // 渲染模板 // 渲染字符串 name := "飛哥哥" //err = t.Execute(w, name) // 渲染結構體 user := User{ Name:name, Gender:"男", Age:23, } //err = t.Execute(w, user) // 渲染map m := map[string]interface{}{ "name": name, "gender": "男", "age": 24, } //err = t.Execute(w, m) hobbyList := []string{ "籃球", "足球", "雙色球", } err = t.Execute(w, map[string]interface{}{ "m": m, "user": user, "hobby": hobbyList, }) if err!=nil { log.Println("render template failed, err%v", err) return } }hello.tmpl
<p>Hello {{ .user.Name }}</p> <p>年齡 {{ .user.Age }}</p> <p>性別 {{ .user.Gender }}</p> <p>Hello {{ .m.name }}</p> <p>年齡 {{ .m.age }}</p> <p>性別 {{ .m.gender }}</p> <hr> {{ range $idx, $hobby := .hobby}} <p>{{$idx}} - {{$hobby}}</p>
2. 注釋
{{/* a comment */}} 注釋,執行時會忽略。可以多行。注釋不能嵌套,並且必須緊貼分界符始止。
3. pipeline
pipeline
是指產生數據的操作。比如{{.}}
、{{.Name}}
等。Go的模板語法中支持使用管道符號|
鏈接多個命令,用法和unix下的管道類似:|
前面的命令會將運算結果(或返回值)傳遞給后一個命令的最后一個位置。
注意:並不是只有使用了|
才是pipeline。Go的模板語法中,pipeline的
概念是傳遞數據,只要能產生數據的,都是pipeline
。
4. 變量
我們還可以在模板中聲明變量,用來保存傳入模板的數據或其他語句生成的結果。具體語法如下:
$obj := {{.}}
其中$obj
是變量的名字,在后續的代碼中就可以使用該變量了。
5. 移除空格
有時候我們在使用模板語法的時候會不可避免的引入一下空格或者換行符,這樣模板最終渲染出來的內容可能就和我們想的不一樣,這個時候可以使用{{-
語法去除模板內容左側的所有空白符號, 使用-}}
去除模板內容右側的所有空白符號。
例如:
{{- .Name -}}
注意:-
要緊挨{{
和}}
,同時與模板值之間需要使用空格分隔。
6. 條件判斷
Go模板語法中的條件判斷有以下幾種:
{{if pipeline}} T1 {{end}} {{if pipeline}} T1 {{else}} T0 {{end}} {{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
7. range
Go的模板語法中使用range
關鍵字進行遍歷,有以下兩種寫法,其中pipeline
的值必須是數組、切片、字典或者通道。
{{range pipeline}} T1 {{end}} 如果pipeline的值其長度為0,不會有任何輸出 {{range pipeline}} T1 {{else}} T0 {{end}} 如果pipeline的值其長度為0,則會執行T0。
8. with
{{with pipeline}} T1 {{end}} 如果pipeline為empty不產生輸出,否則將dot設為pipeline的值並執行T1。不修改外面的dot。 {{with pipeline}} T1 {{else}} T0 {{end}} 如果pipeline為empty,不改變dot並執行T0,否則dot設為pipeline的值並執行T1。
9. 預定義函數
執行模板時,函數從兩個函數字典中查找:首先是模板函數字典,然后是全局函數字典。一般不在模板內定義函數,而是使用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 返回與其參數的文本表示形式等效的轉義HTML。 這個函數在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,模板執行會中斷並返回給調用模板執行者該錯誤;
10. 比較函數
布爾函數會將任何類型的零值視為假,其余視為真。
下面是定義為函數的二元比較運算的集合:
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”)。但是,整數和浮點數不能互相比較。
11. 自定義函數
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) return } // 自定義一個誇人的模板函數 kua := func(arg string) (string, error) { return arg + "真帥", nil } // 采用鏈式操作在Parse之前調用Funcs添加自定義的kua函數 tmpl, err := template.New("hello").Funcs(template.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.tmpl
中按照如下方式使用我們自定義的kua
函數了。
{{kua .Name}}
12. 嵌套template
我們可以在template中嵌套其他的template。這個template可以是單獨的文件,也可以是通過define
定義的template。
舉個例子: 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"}} <hr> {{template "ol.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>
我們注冊一個templDemo
路由處理函數.
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: 18, } tmpl.Execute(w, user) }
注意:在解析模板時,被嵌套的模板一定要在后面解析,例如上面的示例中t.tmpl
模板中嵌套了ul.tmpl
,所以ul.tmpl
要在t.tmpl
后進行解析。
13. 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.tmpl
,”繼承”base.tmpl
:
{{template "base.tmpl" .}} {{define "content"}} <div>Hello world!</div> {{end}}
然后使用template.ParseGlob
按照正則匹配規則解析模板文件,然后通過ExecuteTemplate
渲染指定的模板:
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
模板,我們可以通過下面兩種方法來解決。
-
在模板文件開頭使用
{{define 模板名}}
語句顯式的為模板命名。 -
可以把模板文件存放在
templates
文件夾下面的不同目錄中,然后使用template.ParseGlob("templates/**/*.tmpl")
解析模板。
14. 修改默認的標識符
Go標准庫的模板引擎使用的花括號{{
和}}
作為標識,而許多前端框架(如Vue
和 AngularJS
)也使用{{
和}}
作為標識符,所以當我們同時使用Go語言模板引擎和以上前端框架時就會出現沖突,這個時候我們需要修改標識符,修改前端的或者修改Go語言的。這里演示如何修改Go語言模板引擎默認的標識符:
template.New("index3.tmpl").Delims("{[","]}").ParseFiles("templates/index3.tmpl")
<h1>hello {[ . ]}</h1>
五、text/template與html/tempalte的區別
html/template
針對的是需要返回HTML內容的場景,在模板渲染過程中會對一些有風險的內容進行轉義,以此來防范跨站腳本攻擊。
例如,我定義下面的模板文件:
<!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>
這個時候傳入一段JS代碼並使用html/template
去渲染該文件,會在頁面上顯示出轉義后的JS內容。<script>alert('嘿嘿嘿')</script>
這就是html/template
為我們做的事。
但是在某些場景下,我們如果相信用戶輸入的內容,不想轉義的話,可以自行編寫一個safe函數,手動返回一個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函數就可以了。