GO 語言標準庫 http-template

【導讀】web 項目中,遇到後端渲染頁面的需求要如何處理?本文介紹的基礎庫 http/template 可以實現這個功能。

html/template包實現了數據驅動的模板, 用於申城可防止代碼注入的安全的 HTML 內容. 它提供了和text/template包相同的接口, Go 語言中輸出 HTML 的場景都應使用 html/templant 這個包.

模板與渲染

在一些前後端分離的 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) *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 >
    <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 >
        <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>

同理, 當我們傳入的變量是 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關鍵字進行遍歷, 有以下兩種寫法, 其中pipelien的值必須是數組, 切片, 字典或者通道.

{{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.tmpl中按照如下方式使用我們自定義的kua函數了

{{kua .Name}}

嵌套 template

我們可以在 template 中嵌套其他的 template. 這個 template 可以是單獨的文件, 也可以是通過define定義的 templage.

舉個例子: t.tmpl文件內容如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta >
    <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.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 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模板, 我們可以通過下面兩種方法來解決.

  1. 在模板文件開頭使用 {{define 模板名}} 的語句顯式的爲模板命名.

  2. 可以把模板文件存放在 templates 文件夾下面的不同目錄中, 然後使用template.ParseGlob(“templates/**/*.tmpl”)解析模板

修改默認標識符

Go 標準庫的模板引擎使用的花括號{{}}作爲標識,而許多前端框架(如VueAngularJS)也使用{{}}作爲標識符,所以當我們同時使用 Go 語言模板引擎和以上前端框架時就會出現衝突,這個時候我們需要修改標識符,修改前端的或者修改 Go 語言的。這裏演示如何修改 Go 語言模板引擎默認的標識符:

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

text/template 與 html/template 的區別

html/template針對的是需要返回 HTML 內容的場景, 在模板渲染過程中會對一些有風險的內容進行轉義, 以此來防範跨站腳本攻擊.

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

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta >
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
    {{.}}
</body>
</html>

這個時候傳入一段 JS 代碼並使用html/template去渲染該文件,會在頁面上顯示出轉義後的 JS 內容。alert('嘿嘿嘿') 這就是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 }}

轉自:

zhuanlan.zhihu.com/p/299048675

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qvqH45HOtHc6BdORcSOXqw