html/template包實作了資料驅動的模板,用于生成可防止代碼注入的安全的HTML內容,它提供了和text/template包相同的介面,Go語言中輸出HTML的場景都應使用html/template這個包,
模板與渲染
在一些前后端不分離的Web架構中,我們通常需要在后端將一些資料渲染到HTML檔案中,從而實作動態的網頁(網頁的布局和樣式大致一樣,但展示的內容并不一樣)效果,
我們這里說的模板可以理解為事先定義好的HTML檔案檔案,模板渲染的作用機制可以簡單理解為文本替換操作–使用相應的資料去替換HTML檔案中事先準備好的標記,
很多編程語言的Web框架中都使用各種模板引擎,比如Python語言中Flask框架中使用的jinja2模板引擎,
Go語言的模板引擎
Go語言內置了文本模板引擎text/template和用于HTML檔案的html/template,它們的作用機制可以簡單歸納如下:
- 模板檔案通常定義為
.tmpl和.tpl為后綴(也可以使用其他的后綴),必須使用UTF8編碼, - 模板檔案中使用
{{和}}包裹和標識需要傳入的資料, - 傳給模板這樣的資料就可以通過點號(
.)來訪問,如果資料是復雜型別的資料,可以通過{ { .FieldName }}來訪問它的欄位, - 除
{{和}}包裹的內容外,其他內容均不做修改原樣輸出,
模板引擎的使用
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) *Template函式創建一個名為name的模板,然后對其呼叫上面的方法去決議模板字串或模板檔案,
模板渲染
渲染模板簡單來說就是使用資料去填充模板,當然實際上可能會復雜很多,
func (t *Template) Execute(wr io.Writer, data interface{}) error func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
基本示例
定義模板檔案
我們按照Go模板語法定義一個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 {{.}}</p>
</body>
</html>
決議和渲染模板檔案
然后我們創建一個main.go檔案,在其中寫下HTTP server端代碼如下:
// 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.go檔案編譯執行,然后使用瀏覽器訪問http://127.0.0.1:9090就能看到頁面上顯示了“Hello 沙河小王子”, 這就是一個最簡單的模板渲染的示例,Go語言模板引擎詳細用法請往下閱讀,
模板語法
{{.}}
模板語法都包含在{{和}}中間,其中{{.}}中的點表示當前物件,
當我們傳入一個結構體物件時,我們可以根據.來訪問結構體的對應欄位,例如:
// 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>年齡:{{.Name}}</p>
</body>
</html>
同理,當我們傳入的變數是map時,也可以在模板檔案中通過.根據key來取值,
注釋
{{/* a comment */}}
注釋,執行時會忽略,可以多行,注釋不能嵌套,并且必須緊貼分界符始止,
pipeline
pipeline是指產生資料的操作,比如{{.}}、{{.Name}}等,Go的模板語法中支持使用管道符號|鏈接多個命令,用法和unix下的管道類似:|前面的命令會將運算結果(或回傳值)傳遞給后一個命令的最后一個位置,
注意:并不是只有使用了|才是pipeline,Go的模板語法中,pipeline的概念是傳遞資料,只要能產生資料的,都是pipeline,
變數
我們還可以在模板中宣告變數,用來保存傳入模板的資料或其他陳述句生成的結果,具體語法如下:
$obj := {{.}}
其中$obj是變數的名字,在后續的代碼中就可以使用該變數了,
移除空格
有時候我們在使用模板語法的時候會不可避免的引入一下空格或者換行符,這樣模板最終渲染出來的內容可能就和我們想的不一樣,這個時候可以使用{{-語法去除模板內容左側的所有空白符號, 使用-}}去除模板內容右側的所有空白符號,
例如:
{{- .Name -}}
注意:-要緊挨{{和}},同時與模板值之間需要使用空格分隔,
條件判斷
Go模板語法中的條件判斷有以下幾種:
{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
range
Go的模板語法中使用range關鍵字進行遍歷,有以下兩種寫法,其中pipeline的值必須是陣列、切片、字典或者通道,
{{range pipeline}} T1 {{end}} 如果pipeline的值其長度為0,不會有任何輸出 {{range pipeline}} T1 {{else}} T0 {{end}} 如果pipeline的值其長度為0,則會執行T0,
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
回傳與其引數的文本表示形式等效的轉義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,模板執行會中斷并回傳給呼叫模板執行者該錯誤;
比較函式
布爾函式會將任何型別的零值視為假,其余視為真,
下面是定義為函式的二元比較運算的集合:
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)
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}}
嵌套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后進行決議,
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 >
{{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")決議模板,
修改默認的識別符號
Go標準庫的模板引擎使用的花括號{{和}}作為標識,而許多前端框架(如Vue和 AngularJS)也使用{{和}}作為識別符號,所以當我們同時使用Go語言模板引擎和以上前端框架時就會出現沖突,這個時候我們需要修改識別符號,修改前端的或者修改Go語言的,這里演示如何修改Go語言模板引擎默認的識別符號:
template.New("test").Delims("{[", "]}").ParseFiles("./t.tmpl")
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函式就可以了,
{{ . | safe }}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/45079.html
標籤:Go
上一篇:Web框架之Gin介紹及使用
