Go開發Web應用(3):模板

語言: CN / TW / HK

前言:Web模板就是預先設計好的html頁面,Go的模板引擎會通過模板和傳入的資料來建立html頁面返回給客戶端。Go的標準庫text/templatehtml\template提供了預設的模板引擎。其實大多時候Go都是作為後端語言來使用,並且大家基本上也都是使用前後端分離的模式搭建應用。但是這也不妨礙我們瞭解下Go中的模板的用法。

Go的模板引擎

GO的模板就是一個文字檔案(常為html檔案),其中嵌入一些action。如果Go伺服器使用模板生成頁面,那麼流程簡單描述:Handler呼叫模板引擎,將一個或多個模板檔案傳入給模板引擎,在傳入模板需要的動態資料;模板在收到這些資料後會生成相應的html檔案,並將這些html檔案寫入到ResponseWriter中最終返回給客戶端。可以大致描述為下圖。 Go伺服器使用模板生成頁面

使用模板

使用模板要用到text/template包中的template結構。首先呼叫template載入相應的模板或者模板集合,隨後解析模板並根據傳入的資料生成html寫入響應。主要涉及到的兩個方法是載入模板和執行模板(生成響應)。
template使用ParseFiles方法讀取和解析模板(集合),方法返回指向解析後的template.Template的指標與一個可為空的錯誤。接著使用Execute方法執行模板將資料應用到模板中生成html。
比如下面的info.html就是一個模板檔案。和真正的html檔案的區別就是其中有使用{{ }}包住的內容。{{ }}包住的內容區域就是前面所說的Action,模板引擎在執行模板是會使用一個值去替換這個Action本身。如果你用過使用過其他語言進行過動態網頁的開發應該很熟悉這種模式,對於我來說聯想到的就是Razor頁面。 info.html模板檔案如下

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Info</title>
</head>

<body>
    {{.}}
</body>

</html>

這個模板的Action只有一個".",表示模板引擎在執行模板時,使用一個值去替換這個Action本身。有了模板檔案,載入並執行也並不複雜。如下所示

【示例1】使用模板
package main

import (
	"net/http"
	"text/template"
)

func main() {
	server := http.Server{
		Addr: "localhost:8080",
	}

	http.HandleFunc("/info", func(w http.ResponseWriter, r *http.Request) {
		//**********************************************************
		t, _ := template.ParseFiles("templateFile/info.html") //使用了相對路徑
		_ = t.Execute(w, "hello gopher")    //使用字串"hello gopher"來替換
		//**********************************************************
	})

	_ = server.ListenAndServe()
}

template.ParseFiles是包提供的獨立函式,其背後是呼叫了Template結構的ParseFiles方法。

t, _ := template.ParseFiles("tmpl.html")

這和下面的寫法是等價的

t := template.New("tmpl.html")
t, _ := t.ParseFiles("tmpl.html")

ParseFiles方法可以接收多個引數,其函式簽名如下

func parseFiles(t *Template, readFile func(string) (string, []byte, error), filenames ...string) (*Template, error) {
	......
}

無論接收幾個檔名做引數,都只返回一個模板。並且這個模板的名稱就是傳入的第一個檔案的檔名(帶字尾)。但是如果傳入的是多個模板檔案,那麼會將這些模板檔案放在一個集合裡儲存。
除了ParseFiles函式,另外還有一個ParseGlob函式也可對模板進行語法分析。ParseGlob函式傳入的引數是帶萬用字元的檔名。比如t.ParseGlob("templateFile/*.html)就是將templateFile資料夾內所有後綴為.html的檔案一起傳入進行分析。並且可以通過Lookup方法根據模板名找出需要的模板進行執行。

templateCollection := template.ParseGlob("templateFile/*.html")
tmpl := templateCollection.Lookup("a.html")
tmpl.Execute(w, nil)

另外,template.Must函式提供了一種處理錯誤的機制。template.Must可以包裹一個待執行的函式,待執行的函式返回的是一個指向模板的指標和一個可為空的錯誤。如果這個錯誤不是nil,那麼template.Must函式將會引發panic。如果沒有錯誤,返回模板指標。

t := template.Must(template.ParseGlob("templateFile/*.html))

action

在模板檔案中使用雙大括號包裹的內容就稱為Action,單純的點“.”就是一個簡單的Action。當然除了“.”還有其他的Action。

條件Action

條件Action根據引數值來進行分支操作。

{{if arg}}
...
{{else}}
...
{{end}}

當然,可以沒有“else”分支。一個簡單的示例即可說明用法。

【示例2】條件Action的使用
func main() {
	server := http.Server{
		Addr: "localhost:8080",
	}

	http.HandleFunc("/action", action)

	_ = server.ListenAndServe()
}

func action(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles("templateFile/ifelse.html")
	//產生隨機數
	rand.Seed(time.Now().Unix())
	scope := 10
	i := rand.Intn(scope)
	//執行模板
	t.Execute(w, i > scope/2)
}

模板檔案如下,只包含一層條件Action。由於傳入的引數是由隨機數控制,所以不停訪問會隨機返回兩種相應。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>IF-ELSE</title>
</head>

<body>
    {{if .}}
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere sit aut quos, natus iure alias dolore quam! Eum hic,
    fuga quasi eos impedit distinctio nam iusto commodi nemo odit libero.
    {{else}}
    襟三江而帶五湖,控蠻荊而引甌越。物華天寶,龍光射牛鬥之墟;人傑地靈,徐孺下陳蕃之榻。雄州霧列,俊採星馳。臺隍枕夷夏之交,賓主盡東南之美。
    {{end}}
</body>

</html>

迭代Action

可以遍歷array,slice,map和channel等。結構如下,其中迭代內部的點(.)會被賦予被迭代的元素。

{{range array}}
Dot is set to the element{{.}}
{{else}}
Nothing to show
{{end}}

如果加上{{else}}段,那麼傳入的集合為nil時,則會進入{{else}}段。

【示例3】迭代Action的使用
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Range</title>
</head>

<body>
    <ul>
    {{range .}}
    <li>{{.}}</li>
    {{else}}
    <li>nothing</li>
    {{end}}
</ul>
</body>

</html>

Handler中向模板中傳入一個string陣列進行迭代

func rangeAction(t *template.Template, w http.ResponseWriter, r *http.Request) {
	daysOfWeek := []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sta", "Sun"}
	t.Execute(w, daysOfWeek)
}

設定Action

設定Action允許使用者在指定範圍內為點(.)設定值。就是在{{with %你想設定的值%}}與{{end}}之間,點(.)不再是傳入的值,而是你設定的值。

{{with arg}}
 Dot is set to arg
{{end}}

用個例子說明。

【示例4】設定Action的用法
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>With </title>
</head>

<body>
<div>點表示: {{.}}</div>
<div>
    {{with "gopher"}}
    點表示: {{.}}
    {{end}}
</div>
</body>

</html>
//設定Action
func withAction(t *template.Template, w http.ResponseWriter, r *http.Request) {
	t.Execute(w, "hello")
}

包含Action

包含Action可以讓模板實現巢狀。寫法是下面這樣

{{template "name"}}
或
{{template "name" arg}}

其中name就是要包含的模板的名稱。下面這個模板"t1.html"中嵌套了"t2.html"。前面說過模板檔案的名稱會被用作為模板的名。

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>t1</title>
</head>
	<body>
		<div>This is t1.html before</div>
		<div>This is the value of the dot in t1.html - {{.}}</div>
		<hr/>
		{{template "t2.html"}}
		<hr/>
		<div>This is t1.html after</div>
	</body>
</html>

而"t2.html"模板如下

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>t2</title>
</head>
<body>
	<div style="background-color: dodgerblue">
		This is t2.html
		This is the value of the dot in t2.html - {{.}}
	</div>
</body>
</html>

訪問 http://localhost:8080/action/t1 能看到下面的介面。
包含Action

需要注意的是,載入巢狀的模板需要對所有涉及到的模板進行分析,就是在呼叫ParseFiles函式或者ParseGlob函式時要將涉及到的模板都傳入。上面模板t1.html中的點(.)被傳入的字串所替換,但是t2.html中的點(.)卻沒有。如果將t1.html中的Action改寫成{{template "name" arg}}這樣便可以將變數傳遞到巢狀的模板中,如下

{{template "t2.html" .}}

現在訪問 http://localhost:8080/action/t1 的介面如下。
包含Action

引數、變數和管道

引數和變數

前面一直在模板中使用的點(.)就是一個引數,表示的是Handler向模板傳遞的資料。除了點(.),引數還可以是bool、int、string等字面量,也可以是結構或者方法,但是方法只能有一個返回值或者一個返回值加一個可為空的錯誤。
使用者還可以在模板中定義變數,使用$符號開頭,如下

{{range $key, $value := .}}
The key is {{$key}} and the value is {{$value}}
{{end}}

這樣,從Handler傳給模板一個map時,便可以進行遍歷了。

管道

模板中的管道是多個有序的串聯起來的引數、函式或者方法。工作方式和Linux中的管道有點類似。

{{p1 | p2 | p3}

在管道中,p1的輸出作為p2的輸入,依次下去。舉個簡單的例子

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

上面的管道,浮點數字面量作為引數輸入到模板的內建函式printf中,並使用指定的格式符,最終輸出12.35.

函式

Go的函式可以作為引數輸入模板,並且Go模板引擎也內建了一些函式,這些函式都有限制:只能有一個返回值或者一個返回值加一個可為空的錯誤。
使用者建立自定義的模板函式的步驟:
(1)建立一個FuncMap的對映,並將對映的鍵設定為函式的名字,值設定成實際函式 (2)將FuncMap與模板進行繫結 【示例5】自定義模板函式

func main() {
	server := http.Server{
		Addr: "localhost:8080",
	}
	http.HandleFunc("/process", process)
	_ = server.ListenAndServe()
}

//模板函式的使用
func process(tw http.ResponseWriter, r *http.Request) {
	//step1: 建立FuncMap對映
	funcMap := template.FuncMap{
		"fdate": formatDate,
	}
	//step2: 將FuncMap對映與模板關聯
	t := template.New("tmpl.html").Funcs(funcMap)

	t, _ = t.ParseFiles("tmpl.html")
	t.Execute(w, time.Now())
}

//自定義模板函式:格式化日期
func formatDate(t time.Time) string {
	layout := "2006-01-02"
	return t.Format(layout)
}

模板檔案如下

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Process</title>
</head>
<body>
	<div>The date time is {{ . | fdate }}</div>
	<!--或者-->
	<div>The date time is {{ fdate . }}</div>
</body>
</html>

但是,綜合來說,管道還是會比函式要更強大和靈活,並更加易讀。

佈局Layout

佈局頁(layout)在很多其他的Web框架中也經常見到,比如ASP.NET(Core)中也有layout的使用。使用{{template "name" .}}可以在模板中實現巢狀,如果使用這樣的方法來實現佈局頁,那麼每個頁面都需要有一個自己的佈局頁檔案,那麼意義就不是很大了。我們需要的是一個公共的佈局頁。這裡,Go還提供了定義Action來幫助實現佈局頁。
【示例6】使用佈局頁

{{define "layout"}}

<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Go Layout</title>
</head>
<body>
    <div>This is Layout - begin</div>
    <div>this is value - {{.}}</div>
    {{template "content"}}
    <div>This is Layout - end</div>
</body>
</html>

{{end}}
{{define "content"}}

<h1 style="color: red">Hello world</h1>

{{end}}
{{define "content"}}

<h1 style="color: blue">Hello world</h1>

{{end}}

下面是後臺程式碼

func main() {
	server := http.Server{
		Addr: "localhost:8080",
	}
	http.HandleFunc("/layout", layout)
	_ = server.ListenAndServe()
}

func layout(w http.ResponseWriter, r *http.Request) {
	rand.Seed(time.Now().Unix())
	var t *template.Template
	if rand.Intn(10) > 5 {
		t, _ = t.ParseFiles("layout.html", "redHello.html")
	} else {
		t, _ = t.ParseFiles("layout.html", "blueHello.html")
	}
	_ = t.ExecuteTemplate(w, "layout", "")
}

和之前不一樣的是,這裡使用的執行模板的方法是ExecuteTemplate,並把待執行的模板名傳入。這樣的寫法便實現了佈局頁。