【Golang】Golang Template實例

golang提供了兩個標準庫用來處理模板text/template和html/template。我們使用html/template格式化html字符。

模板引擎

模板引擎很多,Python的jinja,nodejs的jade等都很好。所謂模板引擎,則將模板和數據進行渲染的輸出格式化後的字符程序。對於go,執行這個流程大概需要三步。

  • 創建模板對象
  • 加載模板字串
  • 執行渲染模板
warming up

go提供的標準庫html/template提供了很多處理模板的接口。我們的項目結構爲:

├── main.go
└── templates
    ├── index.html
    └── layout.html

templates文件夾有兩個文件,分別爲模板文件。 layout.html文件如下:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    template data: {{ . }}
  </body>
</html>

我們可以使用ParseFiles方法加載模板,該方法會返回一個模板對象和錯誤,接下來就可以使用模板對象執行模板,注入數據對象。go的提供了一些模板標籤,稱之爲action,.也是一種action,更多的action稍後解釋。

func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html")
    fmt.Println(t.Name())
    t.Execute(w, "Hello world")
}

我們打印了t模板對象的Name方法,實際上,每一個模板,都有一個名字,如果不顯示指定這個名字,go將會把文件名(包括擴展名當成名字)本例則是layout.html。訪問之後可以看見返回的html字串:

:curl -i http://127.0.0.1:8000/
HTTP/1.1 200 OK
Date: Fri, 09 Dec 2016 09:04:36 GMT
Content-Length: 223
Content-Type: text/html; charset=utf-8

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    template data: Hello world
  </body>
</html>

go不僅可以解析模板文件,也可以直接解析模板字串,這就是標準的處理,新建-加載-執行三部曲:

func templateHandler(w http.ResponseWriter, r *http.Request){
    tmpl := `<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Web Programming</title>
    </head>
    <body>
        {{ . }}
    </body>
</html>`

    t := template.New("layout.html")
    t, _ = t.Parse(tmpl)
    fmt.Println(t.Name())
    t.Execute(w, "Hello World")

}

實際開發中,最終的頁面很可能是多個模板文件的嵌套結果。go的ParseFiles也支持加載多個模板文件,不過模板對象的名字則是第一個模板文件的文件名。

func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html", "templates/index.html")
    fmt.Println(t.Name())
    t.Execute(w, "Hello world")
}

可見打印的還是 layout.html的名字,執行的模板的時候,並沒有index.html的模板內容。

此外,還有ParseGlob方法,可以通過glob通配符加載模板。

模板命名

前文已經提及,模板對象是有名字的,可以在創建模板對象的時候顯示命名,也可以讓go自動命名。可是涉及到嵌套模板的時候,該如何命名模板呢,畢竟模板文件有好幾個?

go提供了ExecuteTemplate方法,用於執行指定名字的模板。例如加載layout.html模板的時候,可以指定layout.html:

func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html")
    fmt.Println(t.Name())
    t.ExecuteTemplate(w, "layout", "Hello world")
}

似乎和Execute方法沒有太大的差別。下面修改一下layout.html文件:

{{ define "layout" }}

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    template data: {{ . }}
  </body>
</html>

{{ end }}

在模板文件中,使用了define這個action給模板文件命名了。雖然我們ParseFiles方法返回的模板對象t的名字還是layout.html, 但是ExecuteTemplate執行的模板卻是html文件中定義的layout。

不僅可以通過define定義模板,還可以通過template action引入模板,類似jinja的include特性。修改 layout.html 和 index.html

{{ define "layout" }}
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    template data: {{ . }}

    {{ template "index" }}
  </body>
</html>
{{ end }}

index.html:

{{ define "index" }}

<div style="background: yellow">
    this is index.html
</div>

{{ end }}

go的代碼也需要修改,使用ParseFiles加載需要渲染的模板文件:

func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html", "templates/index.html")
    t.ExecuteTemplate(w, "layout", "Hello world")
}

訪問可以看到 index被layout模板include了:

:curl http://127.0.0.1:8000/
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    template data: Hello world

 <div style="background: yellow">
     this is index.html
 </div>
  </body>
</html>
單文件嵌套

總而言之,創建模板對象後和加載多個模板文件,執行模板文件的時候需要指定base模板(layout),在base模板中可以include其他命名的模板。無論點.,define,template這些花括號包裹的東西都是go的action(模板標籤)。

Action是go模板中用於動態執行一些邏輯和展示數據的形式。大致分爲下面幾種:

  • 條件語句
  • 迭代
  • 封裝
  • 引用
條件判斷的語法很簡單:
{{ if arg }}
  some content
{{ end }}

{{ if arg }}
  some content
{{ else }}
  other content
{{ end }}

arg 可以是基本數據結構,也可以是表達式:if-end包裹的內容爲條件爲真的時候展示。與if語句一樣,模板也可以有else語句。

func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html")
    rand.Seed(time.Now().Unix())
    t.ExecuteTemplate(w, "layout", rand.Intn(10) > 5)
}

{{ define "layout" }}
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    template data: {{ . }}

    {{ if . }}
      Number is greater than 5!
    {{ else }}
      Number is 5 or less!
    {{ end }}
  </body>
</html>
{{ end }}

此時就能看見,當.的值爲true的時候顯示if的邏輯,否則顯示else的邏輯。

迭代

對於一些數組,切片或者是map,可以使用迭代的action,與go的迭代類似,使用range進行處理:

func templateHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("templates/layout.html"))
    daysOfWeek := []string{"Mon", "Tue", "Wed", "Ths", "Fri", "Sat", "Sun"}
    t.ExecuteTemplate(w, "layout", daysOfWeek)
}

{{ define "layout" }}
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    template data: {{ . }}

    {{ range . }}
        <li>{{ . }}</li>
    {{ end }}
  </body>
</html>
{{ end }}

可以看見輸出了一堆li列表。迭代的時候,還可以使用$設置循環變量:

{{ range $key, $value := . }}
    <li>key: {{ $key }}, value: {{ $value }}</li>
{{ else }}
    empty
{{ end }}

可以看見和迭代切片很像。range也可以使用else語句:

func templateHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("templates/layout.html"))
    daysOfWeek := []string{}
    t.ExecuteTemplate(w, "layout", daysOfWeek)
}

{{ range . }}
    <li>{{ . }}</li>
{{ else }}
 empty
{{ end }}

當range的結構爲空的時候,則會執行else分支的邏輯。

with封裝

with語言在Python中可以開啓一個上下文環境。對於go模板,with語句類似,其含義就是創建一個封閉的作用域,在其範圍內,可以使用.action,而與外面的.無關,只與with的參數有關:

{{ with arg }}
    此時的點 . 就是arg
{{ end }}
{{ define "layout" }}
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    template data: {{ . }}

     {{ with "world"}}
        Now the dot is set to {{ . }}
     {{ end }}
  </body>
</html>
{{ end }}

訪問結果如下:

:curl http://127.0.0.1:8000/
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    template data: [Mon Tue Wed Ths Fri Sat Sun]

        Now the dot is set to world

  </body>
</html>

可見 with語句的.與其外面的.是兩個不相關的對象。with語句也可以有else。else中的.則和with外面的.一樣,畢竟只有with語句內纔有封閉的上下文:

{{ with ""}}
 Now the dot is set to {{ . }}
{{ else }}
 {{ . }}
{{ end }}

訪問效果爲:

:curl http://127.0.0.1:8000/
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    template data: [Mon Tue Wed Ths Fri Sat Sun]

        [Mon Tue Wed Ths Fri Sat Sun]

  </body>
</html>
引用

我們已經介紹了模板嵌套引用的技巧。引用除了模板的include,還包括參數的傳遞。

func templateHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("templates/layout.html", "templates/index.html"))
    daysOfWeek := []string{"Mon", "Tue", "Wed", "Ths", "Fri", "Sat", "Sun"}
    t.ExecuteTemplate(w, "layout", daysOfWeek)
}

修改 layout.html, layout中引用了 index模板:

{{ define "layout" }}
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    layout template data: ({{ . }})

    {{ template "index" }}

  </body>
</html>
{{ end }}

index.html模板的內容也打印了 .:

{{ define "index" }}

<div style="background: yellow">
    this is index.html ({{ . }})
</div>

{{ end }}

訪問的效果如下,index.html 中的點並沒有數據。

:curl http://127.0.0.1:8000/
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    layout template data: ([Mon Tue Wed Ths Fri Sat Sun])

<div style="background: yellow">
    this is index.html ()
</div>

  </body>
</html>

我們可以修改引用語句{{ template “index” . }},把參數傳給子模板,再次訪問,就能看見index.html模板也有數據啦。

<div style="background: yellow">
    this is index.html ([Mon Tue Wed Ths Fri Sat Sun])
</div>
參數,變量和管道

模板的參數可以是go中的基本數據類型,如字串,數字,布爾值,數組切片或者一個結構體。在模板中設置變量可以使用 $variable := value。我們在range迭代的過程使用了設置變量的方式。

go還有一個特性就是模板的管道函數,熟悉django和jinja的開發者應該很熟悉這種手法。通過定義函數過濾器,實現模板的一些簡單格式化處理。並且通過管道哲學,這樣的處理方式可以連成一起。

{{ p1 | p2 | p3 }}

例如 模板內置了一些函數,比如格式化輸出:

{{ 12.3456 | printf "%.2f" }}
函數

既然管道符可以成爲模板中的過濾器,那麼除了內建的函數,能夠自定義函數可以擴展模板的功能。幸好go的模板提供了自定義模板函數的功能。

想要創建一個定義函數只需要兩步:

  • 創建一個FuncMap類型的map,key是模板函數的名字,value是其函數的定義。
  • 將 FuncMap注入到模板中。
func templateHandler(w http.ResponseWriter, r *http.Request) {
    funcMap := template.FuncMap{"fdate": formDate}
    t := template.New("layout").Funcs(funcMap)
    t = template.Must(t.ParseFiles("templates/layout.html", "templates/index.html"))
    t.ExecuteTemplate(w, "layout", time.Now())
}

然後在模板中使用{{ . | fdate }},當然也可以不適用管道過濾器,而是使用正常的函數調用形式,{{ fdate . }}

注意,函數的注入,必須要在parseFiles之前,因爲解析模板的時候,需要先把函數編譯註入。

智能上下文

上面所介紹的特性,基本上是大多數模板引擎都具有的功能。go還提供了一個更有意思的特性。那就是根據上下文顯示模板的內容。例如字符的轉義,會根據所顯示的上下文環境而智能變化。比如同樣的html標籤,在Js和html環境中,其轉義的內容是不一樣的:

func templateHandler(w http.ResponseWriter, r *http.Request){
    t, _ :=template.ParseFiles("templates/layout.html")
    content := `I asked: <i>What's up?</i>`
    t.ExecuteTemplate(w, "layout", content)
}

模板文件:

{{ define "layout" }}
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>layout</title>
  </head>
  <body>
    <h3>This is layout</h3>
    layout template data: ({{ . }})

    <div><a href="/{{ . }}">Path</a></div>
    <div><a href="/?q={{ . }}">Query</a></div>
    <div><a onclick="f('{{ . }}')">Onclick</a></div>

  </body>
</html>
{{ end }}

訪問結果:

layout template data: (I asked: <i>What's up?</i>)

    <div><a href="/I%20asked:%20%3ci%3eWhat%27s%20up?%3c/i%3e">Path</a></div>
    <div><a href="/?q=I%20asked%3a%20%3ci%3eWhat%27s%20up%3f%3c%2fi%3e">Query</a></div>
    <div><a οnclick="f('I asked: \x3ci\x3eWhat\x27s up?\x3c\/i\x3e')">Onclick</a></div>

可以看見go會自動爲我們處理html標籤的轉義。這對web安全具有重要作用。避免了一些XSS攻擊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章