Go 模板處理

2022-05-13 17:43 更新

什么是模板

你一定聽說過一種叫做MVC的設(shè)計(jì)模式,Model處理數(shù)據(jù),View展現(xiàn)結(jié)果,Controller控制用戶的請(qǐng)求,至于View層的處理,在很多動(dòng)態(tài)語(yǔ)言里面都是通過在靜態(tài)HTML中插入動(dòng)態(tài)語(yǔ)言生成的數(shù)據(jù),例如JSP中通過插入?<%=....=%>?,PHP中通過插入?<?php.....?>?來(lái)實(shí)現(xiàn)的。

通過下面這個(gè)圖可以說明模板的機(jī)制


Web應(yīng)用反饋給客戶端的信息中的大部分內(nèi)容是靜態(tài)的,不變的,而另外少部分是根據(jù)用戶的請(qǐng)求來(lái)動(dòng)態(tài)生成的,例如要顯示用戶的訪問記錄列表。用戶之間只有記錄數(shù)據(jù)是不同的,而列表的樣式則是固定的,此時(shí)采用模板可以復(fù)用很多靜態(tài)代碼。

Go模板使用

在Go語(yǔ)言中,我們使用template包來(lái)進(jìn)行模板處理,使用類似?Parse?、?ParseFile?、?Execute?等方法從文件或者字符串加載模板,然后執(zhí)行類似上面圖片展示的模板的merge操作。請(qǐng)看下面的例子:

func handler(w http.ResponseWriter, r *http.Request) {
	t := template.New("some template") //創(chuàng)建一個(gè)模板
	t, _ = t.ParseFiles("tmpl/welcome.html")  //解析模板文件
	user := GetUser() //獲取當(dāng)前用戶信息
	t.Execute(w, user)  //執(zhí)行模板的merger操作
}

通過上面的例子我們可以看到Go語(yǔ)言的模板操作非常的簡(jiǎn)單方便,和其他語(yǔ)言的模板處理類似,都是先獲取數(shù)據(jù),然后渲染數(shù)據(jù)。

為了演示和測(cè)試代碼的方便,我們?cè)诮酉聛?lái)的例子中采用如下格式的代碼

  • 使用Parse代替ParseFiles,因?yàn)镻arse可以直接測(cè)試一個(gè)字符串,而不需要額外的文件
  • 不使用handler來(lái)寫演示代碼,而是每個(gè)測(cè)試一個(gè)main,方便測(cè)試
  • 使用?os.Stdout?代替?http.ResponseWriter?,因?yàn)?os.Stdout?實(shí)現(xiàn)了?io.Writer?接口

模板中如何插入數(shù)據(jù)?

上面我們演示了如何解析并渲染模板,接下來(lái)讓我們來(lái)更加詳細(xì)的了解如何把數(shù)據(jù)渲染出來(lái)。一個(gè)模板都是應(yīng)用在一個(gè)Go的對(duì)象之上,Go對(duì)象的字段如何插入到模板中呢?

字段操作

Go語(yǔ)言的模板通過{{}}來(lái)包含需要在渲染時(shí)被替換的字段,{{.}}表示當(dāng)前的對(duì)象,這和Java或者C++中的this類似,如果要訪問當(dāng)前對(duì)象的字段通過{{.FieldName}},但是需要注意一點(diǎn):這個(gè)字段必須是導(dǎo)出的(字段首字母必須是大寫的),否則在渲染的時(shí)候就會(huì)報(bào)錯(cuò),請(qǐng)看下面的這個(gè)例子:

package main

import (
	"html/template"
	"os"
)

type Person struct {
	UserName string
}

func main() {
	t := template.New("fieldname example")
	t, _ = t.Parse("hello {{.UserName}}!")
	p := Person{UserName: "Astaxie"}
	t.Execute(os.Stdout, p)
}

上面的代碼我們可以正確的輸出hello Astaxie,但是如果我們稍微修改一下代碼,在模板中含有了未導(dǎo)出的字段,那么就會(huì)報(bào)錯(cuò)

type Person struct {
	UserName string
	email	string  //未導(dǎo)出的字段,首字母是小寫的
}

t, _ = t.Parse("hello {{.UserName}}! {{.email}}")

上面的代碼就會(huì)報(bào)錯(cuò),因?yàn)槲覀冋{(diào)用了一個(gè)未導(dǎo)出的字段,但是如果我們調(diào)用了一個(gè)不存在的字段是不會(huì)報(bào)錯(cuò)的,而是輸出為空。

如果模板中輸出?{{.}}?,這個(gè)一般應(yīng)用于字符串對(duì)象,默認(rèn)會(huì)調(diào)用fmt包輸出字符串的內(nèi)容。

輸出嵌套字段內(nèi)容

上面我們例子展示了如何針對(duì)一個(gè)對(duì)象的字段輸出,那么如果字段里面還有對(duì)象,如何來(lái)循環(huán)的輸出這些內(nèi)容呢?我們可以使用?{{with …}}…{{end}}?和?{{range …}}{{end}}?來(lái)進(jìn)行數(shù)據(jù)的輸出。

  • {{range}} 這個(gè)和Go語(yǔ)法里面的range類似,循環(huán)操作數(shù)據(jù)
  • {{with}}操作是指當(dāng)前對(duì)象的值,類似上下文的概念

詳細(xì)的使用請(qǐng)看下面的例子:

package main

import (
	"html/template"
	"os"
)

type Friend struct {
	Fname string
}

type Person struct {
	UserName string
	Emails   []string
	Friends  []*Friend
}

func main() {
	f1 := Friend{Fname: "minux.ma"}
	f2 := Friend{Fname: "xushiwei"}
	t := template.New("fieldname example")
	t, _ = t.Parse(`hello {{.UserName}}!
			{{range .Emails}}
				an email {{.}}
			{{end}}
			{{with .Friends}}
			{{range .}}
				my friend name is {{.Fname}}
			{{end}}
			{{end}}
			`)
	p := Person{UserName: "Astaxie",
		Emails:  []string{"astaxie@beego.me", "astaxie@gmail.com"},
		Friends: []*Friend{&f1, &f2}}
	t.Execute(os.Stdout, p)
}

條件處理

在Go模板里面如果需要進(jìn)行條件判斷,那么我們可以使用和Go語(yǔ)言的if-else語(yǔ)法類似的方式來(lái)處理,如果pipeline為空,那么if就認(rèn)為是false,下面的例子展示了如何使用if-else語(yǔ)法:

package main

import (
	"os"
	"text/template"
)

func main() {
	tEmpty := template.New("template test")
	tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不會(huì)輸出. {{end}}\n"))
	tEmpty.Execute(os.Stdout, nil)

	tWithValue := template.New("template test")
	tWithValue = template.Must(tWithValue.Parse("不為空的 pipeline if demo: {{if `anything`}} 我有內(nèi)容,我會(huì)輸出. {{end}}\n"))
	tWithValue.Execute(os.Stdout, nil)

	tIfElse := template.New("template test")
	tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n"))
	tIfElse.Execute(os.Stdout, nil)
}

通過上面的演示代碼我們知道if-else語(yǔ)法相當(dāng)?shù)暮?jiǎn)單,在使用過程中很容易集成到我們的模板代碼中。

注意:if里面無(wú)法使用條件判斷,例如.Mail=="astaxie@gmail.com",這樣的判斷是不正確的,if里面只能是bool值

pipelines

Unix用戶已經(jīng)很熟悉什么是pipe了,ls | grep "beego"類似這樣的語(yǔ)法你是不是經(jīng)常使用,過濾當(dāng)前目錄下面的文件,顯示含有"beego"的數(shù)據(jù),表達(dá)的意思就是前面的輸出可以當(dāng)做后面的輸入,最后顯示我們想要的數(shù)據(jù),而Go語(yǔ)言模板最強(qiáng)大的一點(diǎn)就是支持pipe數(shù)據(jù),在Go語(yǔ)言里面任何{{}}里面的都是pipelines數(shù)據(jù),例如我們上面輸出的email里面如果還有一些可能引起XSS注入的,那么我們?nèi)绾蝸?lái)進(jìn)行轉(zhuǎn)化呢?

{{. | html}}

在email輸出的地方我們可以采用如上方式可以把輸出全部轉(zhuǎn)化html的實(shí)體,上面的這種方式和我們平常寫Unix的方式是不是一模一樣,操作起來(lái)相當(dāng)?shù)暮?jiǎn)便,調(diào)用其他的函數(shù)也是類似的方式。

模板變量

有時(shí)候,我們?cè)谀0迨褂眠^程中需要定義一些局部變量,我們可以在一些操作中申明局部變量,例如with``range``if過程中申明局部變量,這個(gè)變量的作用域是{{end}}之前,Go語(yǔ)言通過申明的局部變量格式如下所示:

$variable := pipeline

詳細(xì)的例子看下面的:

{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}

模板函數(shù)

模板在輸出對(duì)象的字段值時(shí),采用了?fmt?包把對(duì)象轉(zhuǎn)化成了字符串。但是有時(shí)候我們的需求可能不是這樣的,例如有時(shí)候我們?yōu)榱朔乐估]件發(fā)送者通過采集網(wǎng)頁(yè)的方式來(lái)發(fā)送給我們的郵箱信息,我們希望把?@?替換成?at?例如:?astaxie at beego.me?,如果要實(shí)現(xiàn)這樣的功能,我們就需要自定義函數(shù)來(lái)做這個(gè)功能。

每一個(gè)模板函數(shù)都有一個(gè)唯一值的名字,然后與一個(gè)Go函數(shù)關(guān)聯(lián),通過如下的方式來(lái)關(guān)聯(lián)

type FuncMap map[string]interface{}

例如,如果我們想要的email函數(shù)的模板函數(shù)名是emailDeal,它關(guān)聯(lián)的Go函數(shù)名稱是EmailDealWith,那么我們可以通過下面的方式來(lái)注冊(cè)這個(gè)函數(shù)

t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})

EmailDealWith這個(gè)函數(shù)的參數(shù)和返回值定義如下:

func EmailDealWith(args …interface{}) string

我們來(lái)看下面的實(shí)現(xiàn)例子:

package main

import (
	"fmt"
	"html/template"
	"os"
	"strings"
)

type Friend struct {
	Fname string
}

type Person struct {
	UserName string
	Emails   []string
	Friends  []*Friend
}

func EmailDealWith(args ...interface{}) string {
	ok := false
	var s string
	if len(args) == 1 {
		s, ok = args[0].(string)
	}
	if !ok {
		s = fmt.Sprint(args...)
	}
	// find the @ symbol
	substrs := strings.Split(s, "@")
	if len(substrs) != 2 {
		return s
	}
	// replace the @ by " at "
	return (substrs[0] + " at " + substrs[1])
}

func main() {
	f1 := Friend{Fname: "minux.ma"}
	f2 := Friend{Fname: "xushiwei"}
	t := template.New("fieldname example")
	t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
	t, _ = t.Parse(`hello {{.UserName}}!
				{{range .Emails}}
					an emails {{.|emailDeal}}
				{{end}}
				{{with .Friends}}
				{{range .}}
					my friend name is {{.Fname}}
				{{end}}
				{{end}}
				`)
	p := Person{UserName: "Astaxie",
		Emails:  []string{"astaxie@beego.me", "astaxie@gmail.com"},
		Friends: []*Friend{&f1, &f2}}
	t.Execute(os.Stdout, p)
}

上面演示了如何自定義函數(shù),其實(shí),在模板包內(nèi)部已經(jīng)有內(nèi)置的實(shí)現(xiàn)函數(shù),下面代碼截取自模板包里面

var builtins = FuncMap{
	"and":      and,
	"call":     call,
	"html":     HTMLEscaper,
	"index":    index,
	"js":       JSEscaper,
	"len":      length,
	"not":      not,
	"or":       or,
	"print":    fmt.Sprint,
	"printf":   fmt.Sprintf,
	"println":  fmt.Sprintln,
	"urlquery": URLQueryEscaper,
}

Must操作

模板包里面有一個(gè)函數(shù)Must,它的作用是檢測(cè)模板是否正確,例如大括號(hào)是否匹配,注釋是否正確的關(guān)閉,變量是否正確的書寫。接下來(lái)我們演示一個(gè)例子,用Must來(lái)判斷模板是否正確:

package main

import (
	"fmt"
	"text/template"
)

func main() {
	tOk := template.New("first")
	template.Must(tOk.Parse(" some static text /* and a comment */"))
	fmt.Println("The first one parsed OK.")

	template.Must(template.New("second").Parse("some static text {{ .Name }}"))
	fmt.Println("The second one parsed OK.")

	fmt.Println("The next one ought to fail.")
	tErr := template.New("check parse error with Must")
	template.Must(tErr.Parse(" some static text {{ .Name }"))
}

將輸出如下內(nèi)容

The first one parsed OK.
The second one parsed OK.
The next one ought to fail.
panic: template: check parse error with Must:1: unexpected "}" in command

嵌套模板

我們平常開發(fā)Web應(yīng)用的時(shí)候,經(jīng)常會(huì)遇到一些模板有些部分是固定不變的,然后可以抽取出來(lái)作為一個(gè)獨(dú)立的部分,例如一個(gè)博客的頭部和尾部是不變的,而唯一改變的是中間的內(nèi)容部分。所以我們可以定義成header、content、footer三個(gè)部分。Go語(yǔ)言中通過如下的語(yǔ)法來(lái)申明

{{define "子模板名稱"}}內(nèi)容{{end}}

通過如下方式來(lái)調(diào)用:

{{template "子模板名稱"}}

接下來(lái)我們演示如何使用嵌套模板,我們定義三個(gè)文件,header.tmpl、content.tmplfooter.tmpl文件,里面的內(nèi)容如下

//header.tmpl
{{define "header"}}
<html>
<head>
	<title>演示信息</title>
</head>
<body>
{{end}}

//content.tmpl
{{define "content"}}
{{template "header"}}
<h1>演示嵌套</h1>
<ul>
	<li>嵌套使用define定義子模板</li>
	<li>調(diào)用使用template</li>
</ul>
{{template "footer"}}
{{end}}

//footer.tmpl
{{define "footer"}}
</body>
</html>
{{end}}

演示代碼如下:

package main

import (
	"fmt"
	"os"
	"text/template"
)

func main() {
	s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")
	s1.ExecuteTemplate(os.Stdout, "header", nil)
	fmt.Println()
	s1.ExecuteTemplate(os.Stdout, "content", nil)
	fmt.Println()
	s1.ExecuteTemplate(os.Stdout, "footer", nil)
	fmt.Println()
	s1.Execute(os.Stdout, nil)
}

通過上面的例子我們可以看到通過template.ParseFiles把所有的嵌套模板全部解析到模板里面,其實(shí)每一個(gè)定義的{{define}}都是一個(gè)獨(dú)立的模板,他們相互獨(dú)立,是并行存在的關(guān)系,內(nèi)部其實(shí)存儲(chǔ)的是類似map的一種關(guān)系(key是模板的名稱,value是模板的內(nèi)容),然后我們通過ExecuteTemplate來(lái)執(zhí)行相應(yīng)的子模板內(nèi)容,我們可以看到header、footer都是相對(duì)獨(dú)立的,都能輸出內(nèi)容,content 中因?yàn)榍短琢薶eader和footer的內(nèi)容,就會(huì)同時(shí)輸出三個(gè)的內(nèi)容。但是當(dāng)我們執(zhí)行s1.Execute,沒有任何的輸出,因?yàn)樵谀J(rèn)的情況下沒有默認(rèn)的子模板,所以不會(huì)輸出任何的東西。

同一個(gè)集合類的模板是互相知曉的,如果同一模板被多個(gè)集合使用,則它需要在多個(gè)集合中分別解析

總結(jié)

通過上面對(duì)模板的詳細(xì)介紹,我們了解了如何把動(dòng)態(tài)數(shù)據(jù)與模板融合:如何輸出循環(huán)數(shù)據(jù)、如何自定義函數(shù)、如何嵌套模板等等。通過模板技術(shù)的應(yīng)用,我們可以完成MVC模式中V的處理,接下來(lái)的章節(jié)我們將介紹如何來(lái)處理M和C。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)