request用法_Go 语言 Web 应用开发 第 04 课:高级模板用法
在上一節(jié)課中,我們學(xué)習(xí)了標(biāo)準(zhǔn)庫(kù)中?text/template?包提供的文本模板引擎的邏輯控制、集合對(duì)象迭代和空白符號(hào)處理的用法。這節(jié)課,我們將學(xué)習(xí)標(biāo)準(zhǔn)庫(kù)模板引擎中的一些高級(jí)概念和使用方法,并將渲染結(jié)果轉(zhuǎn)換為 HTML。
模板中的作用域
和程序代碼中的作用域相似,在?text/template?包提供的文本模板引擎中也有作用域的概念。其實(shí)在上節(jié)課當(dāng)中,我們就已經(jīng)接觸過(guò) with 語(yǔ)句了,而這個(gè)語(yǔ)句就是模板作用域的最直接體現(xiàn)。
示例文件?template.go
package mainimport ("fmt""log""net/http""text/template")func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 創(chuàng)建模板對(duì)象并解析模板內(nèi)容tmpl, err := template.New("test").Parse(`{{$name1 := "alice"}}name1: {{$name1}}{{with true}} {{$name1 = "alice2"}} {{$name2 := "bob"}} name2: {{$name2}}{{end}}name1 after with: {{$name1}}`)if err != nil {
fmt.Fprintf(w, "Parse: %v", err)return
}// 調(diào)用模板對(duì)象的渲染方法
err = tmpl.Execute(w, nil)if err != nil {
fmt.Fprintf(w, "Execute: %v", err)return
}
})
log.Println("Starting HTTP server...")
log.Fatal(http.ListenAndServe("localhost:4000", nil))
}
在運(yùn)行這段代碼之前,我們首先需要注意幾點(diǎn):
模板變量?name1?是在模板的全局作用域中定義的
模板變量?name1?在 with 代碼塊中進(jìn)行的是單純的賦值操作,即?=?不是?:=
模板變量?name2?是在 with 代碼塊的作用域中定義的
嘗試運(yùn)行以上代碼可以在終端獲得以下結(jié)果:
? curl http://localhost:4000name1: alice
name2: bob
name1 after with: alice2
可以看到,在進(jìn)入 with 代碼塊之前,name1?的值為 “alice”,但在 with 代碼塊中被修改成為了?alice2,這個(gè)賦值操作直接修改了在模板全局作用域中定義的模板變量?name1?的值。
接下來(lái),我們對(duì)模板內(nèi)容做出如下修改(末尾追加了一行):
示例文件?template_2.go
...tmpl, err := template.New("test").Parse(`{{$name1 := "alice"}}name1: {{$name1}}{{with true}} {{$name1 = "alice2"}} {{$name2 := "bob"}} name2: {{$name2}}{{end}}name1 after with: {{$name1}}name2 after with: {{$name2}}`)...
為了縮減篇幅并更好地專注于有變動(dòng)的部分,部分未改動(dòng)的代碼塊使用了 “…” 進(jìn)行替代
如果嘗試運(yùn)行以上代碼,將在終端獲得以下錯(cuò)誤:
? curl http://localhost:4000Parse: template: test:10: undefined variable "$name2"
模板引擎在解析階段就發(fā)現(xiàn)名為?$name2?的模板變量在 with 代碼塊之外是屬于未定義的,這和在程序代碼中操作一個(gè)超出作用域的變量是一致的。
最后,我們?cè)賮?lái)觀察一下在作用域的規(guī)則下,對(duì)模板變量使用?=?和?:=?的區(qū)別(注意?{{$name1 := "alice2"}}?這一行):
示例文件?template_3.go
...tmpl, err := template.New("test").Parse(`{{$name1 := "alice"}}name1: {{$name1}}{{with true}} {{$name1 := "alice2"}} {{$name2 := "bob"}} name1 in with: {{$name1}} name2: {{$name2}}{{end}}name1 after with: {{$name1}}`)...
為了縮減篇幅并更好地專注于有變動(dòng)的部分,部分未改動(dòng)的代碼塊使用了 “…” 進(jìn)行替代
嘗試運(yùn)行以上代碼可以在終端獲得以下結(jié)果:
? curl http://localhost:4000name1: alice
name1 in with: alice2
name2: bob
name1 after with: alice
我們看到,當(dāng)我們?cè)谀0逯惺褂?:=?的時(shí)候,模板引擎會(huì)在當(dāng)前作用域內(nèi)新建一個(gè)同名的模板變量(等同于程序代碼中本地變量和全局變量的區(qū)別),在同個(gè)作用域內(nèi)對(duì)這個(gè)模板變量的操作都不會(huì)影響到其它作用域。
除了 with 語(yǔ)句之外,if 語(yǔ)句和 range 語(yǔ)句也會(huì)在各自的代碼塊中形成一個(gè)局部的作用域,感興趣的同學(xué)可以基于示例代碼進(jìn)行修改和嘗試。
模板函數(shù)
模板函數(shù),顧名思義,就是像在程序代碼中的函數(shù)那樣,用于在運(yùn)行時(shí)調(diào)用的數(shù)據(jù)結(jié)構(gòu)。其實(shí)在上一節(jié)課中,我們就已經(jīng)介紹并使用過(guò)部分內(nèi)置模板函數(shù)了,還記得等式與不等式的判斷語(yǔ)句嗎?eq、ne?和?lt?等等,本質(zhì)上就是模板函數(shù),只是?text/template?包的文本模板引擎將它們內(nèi)置罷了。
如果想要自定義模板函數(shù)并加入到模板對(duì)象中,可以通過(guò)?Funcs?方法:
示例文件?template_func.go
package mainimport ("fmt""log""net/http""text/template")func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 創(chuàng)建模板對(duì)象并添加自定義模板函數(shù)tmpl := template.New("test").Funcs(template.FuncMap{"add": func(a, b int) int {return a + b
},
})// 解析模板內(nèi)容_, err := tmpl.Parse(`result: {{add 1 2}}`)if err != nil {
fmt.Fprintf(w, "Parse: %v", err)return
}// 調(diào)用模板對(duì)象的渲染方法
err = tmpl.Execute(w, nil)if err != nil {
fmt.Fprintf(w, "Execute: %v", err)return
}
})
log.Println("Starting HTTP server...")
log.Fatal(http.ListenAndServe("localhost:4000", nil))
}
Funcs?方法接受一個(gè)?template.FuncMap?類型的參數(shù),其用法和我們上節(jié)課講到的 map 類型根對(duì)象有異曲同工之妙,底層也是?map[string]interface{}?類型。
在上面的代碼中,我們添加了一個(gè)名為?add?的函數(shù),其接受兩個(gè)?int?類型的參數(shù),返回相加后的結(jié)果。
嘗試運(yùn)行以上代碼可以在終端獲得以下結(jié)果:
? curl http://localhost:4000result: 3
通過(guò)這種方法,就可以向模板對(duì)象中添加更多的函數(shù)以滿足開發(fā)需要。標(biāo)準(zhǔn)庫(kù)的模板引擎還有許多其它用途的內(nèi)置模板函數(shù),可以通過(guò)用戶文檔查看。
模板中的管道操作
使用過(guò)類 Unix 操作系統(tǒng)的同學(xué)一定對(duì)管道操作(Pipeline)不會(huì)陌生,而這種便利的用法在?text/template?包的文本模板引擎中也可以實(shí)現(xiàn),連語(yǔ)法也是一模一樣的。
示例文件?template_pipeline.go
...tmpl := template.New("test").Funcs(template.FuncMap{"add2": func(a int) int {return a + 2},
})// 解析模板內(nèi)容_, err := tmpl.Parse(`result: {{add2 0 | add2 | add2}}`)
...
為了縮減篇幅并更好地專注于有變動(dòng)的部分,部分未改動(dòng)的代碼塊使用了 “…” 進(jìn)行替代
在這里,我們添加了一個(gè)名為?add2?的模板函數(shù),其作用就是返回?int?參數(shù)加 2 之后的結(jié)果。
嘗試運(yùn)行以上代碼可以在終端獲得以下結(jié)果:
? curl http://localhost:4000result: 6
我們?cè)谀0逯姓{(diào)用了三次?add2?函數(shù),其中兩次是通過(guò)管道操作,因此返回的結(jié)果為?0 + 2 + 2 + 2 = 6。
有同學(xué)可能就會(huì)問了,這個(gè)?add2?函數(shù)只接受一個(gè)參數(shù),那如果模板函數(shù)接受兩個(gè)或者更多的參數(shù)還可以進(jìn)行管道操作嗎?答案當(dāng)然是肯定的。
示例文件?template_pipeline_2.go
...tmpl := template.New("test").Funcs(template.FuncMap{"add": func(a, b int) int {return a + b},
})// 解析模板內(nèi)容_, err := tmpl.Parse(`result: {{add 1 3 | add 2 | add 2}}`)
...
為了縮減篇幅并更好地專注于有變動(dòng)的部分,部分未改動(dòng)的代碼塊使用了 “…” 進(jìn)行替代
嘗試運(yùn)行以上代碼可以在終端獲得以下結(jié)果:
? curl http://localhost:4000result: 8
感興趣的同學(xué)可以嘗試讓一個(gè)模板函數(shù)接收或者返回更多數(shù)量的參數(shù),看看是否仍舊可以進(jìn)行管道操作呢?
模板復(fù)用
當(dāng)程序代碼逐漸變得復(fù)雜的時(shí)候,就會(huì)希望通過(guò)抽象成獨(dú)立的函數(shù)或者方法來(lái)復(fù)用一部分代碼邏輯,在模板中也是一樣的道理。這一小節(jié),我們就來(lái)學(xué)習(xí)如何在?text/template?包的文本模板引擎中實(shí)現(xiàn)模板的復(fù)用。
示例文件?template_reuse.go
package mainimport ("fmt""log""net/http""strings""text/template")func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 創(chuàng)建模板對(duì)象并添加自定義模板函數(shù)tmpl := template.New("test").Funcs(template.FuncMap{"join": strings.Join,
})// 解析模板內(nèi)容_, err := tmpl.Parse(`{{define "list"}} {{join . ", "}}{{end}}Names: {{template "list" .names}}`)if err != nil {
fmt.Fprintf(w, "Parse: %v", err)return
}// 調(diào)用模板對(duì)象的渲染方法
err = tmpl.Execute(w, map[string]interface{}{"names": []string{"Alice", "Bob", "Cindy", "David"},
})if err != nil {
fmt.Fprintf(w, "Execute: %v", err)return
}
})
log.Println("Starting HTTP server...")
log.Fatal(http.ListenAndServe("localhost:4000", nil))
}
閱讀以上代碼需要注意這幾點(diǎn):
通過(guò)?Funcs?方法添加了名為?join?模板函數(shù),其實(shí)際上就是調(diào)用?strings.Join
通過(guò)?define ""?的語(yǔ)法定義了一個(gè)非常簡(jiǎn)單的局部模板,即以根對(duì)象?.?作為參數(shù)調(diào)用?join?模板函數(shù)
通過(guò)?template "" ?的語(yǔ)法,調(diào)用名為?list?的局部模板,并將?.names?作為參數(shù)傳遞進(jìn)去(傳遞的參數(shù)會(huì)成為局部模板的根對(duì)象)
嘗試運(yùn)行以上代碼可以在終端獲得以下結(jié)果:
? curl http://localhost:4000Names: Alice, Bob, Cindy, David
這個(gè)例子雖然簡(jiǎn)單,但也使用到了模板復(fù)用最核心的概念:定義、使用和傳參。
從本地文件加載模板
到目前為止,我們使用的模板內(nèi)容都是硬編碼在程序代碼中的,每次修改都需要重新編譯和運(yùn)行程序,這種方式不僅麻煩,而且當(dāng)模板數(shù)量特別多的時(shí)候也不利于進(jìn)行管理。因此,我們可以將模板內(nèi)容保存到本地文件,然后在程序中加載對(duì)應(yīng)的模板后進(jìn)行渲染,最后輸出結(jié)果到客戶端。
示例文件?template_local.go
package mainimport ("fmt""log""net/http""text/template")func main() {// 創(chuàng)建模板對(duì)象并解析模板內(nèi)容tmpl, err := template.ParseFiles("template_local.tmpl")if err != nil {
log.Fatalf("Parse: %v", err)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 調(diào)用模板對(duì)象的渲染方法
err = tmpl.Execute(w, map[string]interface{}{"names": []string{"Alice", "Bob", "Cindy", "David"},
})if err != nil {
fmt.Fprintf(w, "Execute: %v", err)return
}
})
log.Println("Starting HTTP server...")
log.Fatal(http.ListenAndServe("localhost:4000", nil))
}
在這里,我們主要用到的函數(shù)是?template.ParseFiles,我們?cè)谕瑐€(gè)目錄創(chuàng)建一個(gè)名為?template_local.tmpl?的模板文件(文件后綴可以是任意的,一般在使用標(biāo)準(zhǔn)庫(kù)的模板引擎時(shí)習(xí)慣性地將文件后綴命名為?.tmpl?或?.tpl):
{{range .names}}- {{.}}
{{end}}
嘗試運(yùn)行以上代碼可以在終端獲得以下結(jié)果:
? curl http://localhost:4000- Alice
- Bob
- Cindy
- David
值得注意的是,template.ParseFiles?接受的是變長(zhǎng)的參數(shù),因此我們可以同時(shí)指定一個(gè)或者多個(gè)模板文件。那么,怎么才能讓模板引擎知道我們想要進(jìn)行渲染的模板文件是哪一個(gè)呢?
示例文件?template_local_2.go
...// 渲染指定模板的內(nèi)容err = tmpl.ExecuteTemplate(w, "template_local.tmpl", map[string]interface{}{"names": []string{"Alice", "Bob", "Cindy", "David"},
})
...
非常簡(jiǎn)單,只需要將?Execute?方法改成?ExecuteTemplate?就可以了,后者允許通過(guò)模板文件的名稱來(lái)指定具體渲染哪一個(gè)模板文件。在本例中,我們是通過(guò)本地文件加載模板的,因此模板的名稱就是文件名本身。
html/template?與?text/template?的關(guān)聯(lián)與不同
在 Web 應(yīng)用的開發(fā)過(guò)程中,服務(wù)端經(jīng)常需要向客戶端(通常為瀏覽器)輸出 HTML 內(nèi)容以構(gòu)成用戶可交互的頁(yè)面,我們依舊可以使用?text/template?包的模板引擎達(dá)到這個(gè)目的:
示例文件?template_html.go
package mainimport ("fmt""log""net/http""text/template")func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 創(chuàng)建模板對(duì)象并解析模板內(nèi)容tmpl, err := template.New("test").Parse(`
Heading 2
Paragraph
`)if err != nil {fmt.Fprintf(w, "Parse: %v", err)return
}// 調(diào)用模板對(duì)象的渲染方法
err = tmpl.Execute(w, nil)if err != nil {
fmt.Fprintf(w, "Execute: %v", err)return
}
})
log.Println("Starting HTTP server...")
log.Fatal(http.ListenAndServe("localhost:4000", nil))
}
運(yùn)行以上代碼并通過(guò)瀏覽器訪問即可看到渲染后的 HTML 頁(yè)面:
既然?text/template?包就可以達(dá)到渲染 HTML 頁(yè)面的目的,那為什么標(biāo)準(zhǔn)庫(kù)還要另外提供一個(gè)?html/template?包呢?按照官方的說(shuō)法,html/template?本身是一個(gè)?text/template?包的一層封裝,并在此基礎(chǔ)上專注于提供安全保障。作為使用者來(lái)說(shuō),最直觀的變化就是對(duì)所有的文本變量都進(jìn)行了轉(zhuǎn)義處理。
怎么理解呢?我們來(lái)看下面這個(gè)例子。
示例文件?template_xss.go
package mainimport ("fmt""log""net/http""text/template")func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 創(chuàng)建模板對(duì)象并解析模板內(nèi)容tmpl, err := template.New("test").Parse(`
{{.content}}
`)if err != nil {fmt.Fprintf(w, "Parse: %v", err)return
}// 調(diào)用模板對(duì)象的渲染方法
err = tmpl.Execute(w, map[string]interface{}{"content": "",
})if err != nil {
fmt.Fprintf(w, "Execute: %v", err)return
}
})
log.Println("Starting HTTP server...")
log.Fatal(http.ListenAndServe("localhost:4000", nil))
}
有一定 Web 開發(fā)基礎(chǔ)的同學(xué)肯定馬上就能看出來(lái),如果我們運(yùn)行這段代碼,將會(huì)導(dǎo)致俗稱的跨站腳本攻擊(Cross-site scripting, XSS),是最常見的 Web 應(yīng)用安全漏洞之一。
如果想要避免此類攻擊,只需要將導(dǎo)入的包從?text/template?改成?html/template?就可以了。修改完成后,再運(yùn)行程序的話,我們只會(huì)看到被轉(zhuǎn)義之后的 JavaScript 腳本內(nèi)容,成功地避免了此類安全漏洞。
反轉(zhuǎn)義
我們剛剛學(xué)到,在渲染 HTML 內(nèi)容時(shí),正確的姿勢(shì)是使用?html/template?包進(jìn)行渲染操作,因?yàn)檫@個(gè)包可以為我們對(duì)可疑的內(nèi)容進(jìn)行轉(zhuǎn)義。這是一個(gè)優(yōu)點(diǎn),但從另一個(gè)角度講也是缺點(diǎn),因?yàn)樵谀承r(shí)候我們確實(shí)需要?jiǎng)討B(tài)地生成 HTML 內(nèi)容然后作為變量通過(guò)模板引擎進(jìn)行渲染。這時(shí),我們可以借助模板函數(shù),將我們確信安全的文本轉(zhuǎn)換為一個(gè)特殊類型?template.HTML,這樣模板引擎就知道不需要對(duì)其進(jìn)行轉(zhuǎn)義。
示例文件?template_safe.go
package mainimport ("fmt""html/template""log""net/http")func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 創(chuàng)建模板對(duì)象并添加自定義模板函數(shù)tmpl := template.New("test").Funcs(template.FuncMap{"safe": func(s string) template.HTML {return template.HTML(s)
},
})// 解析模板內(nèi)容_, err := tmpl.Parse(`
{{.content | safe}}
`)if err != nil {fmt.Fprintf(w, "Parse: %v", err)return
}// 調(diào)用模板對(duì)象的渲染方法
err = tmpl.Execute(w, map[string]interface{}{"content": "Hello world!",
})if err != nil {
fmt.Fprintf(w, "Execute: %v", err)return
}
})
log.Println("Starting HTTP server...")
log.Fatal(http.ListenAndServe("localhost:4000", nil))
}
這里的核心部分就是?safe?模板函數(shù),其主要作用就是將?string?類型的字符串?s?轉(zhuǎn)換類型為?template.HTML?并返回。
嘗試運(yùn)行以上代碼后,可以在瀏覽器獲得以下頁(yè)面:
在一些 Web 應(yīng)用,我們確實(shí)會(huì)遇到需要將用戶輸入的內(nèi)容渲染為 HTML 格式,怎么樣才可以將任意文本安全地渲染成 HTML 且避免跨站腳本攻擊呢?幸運(yùn)地是,Go 語(yǔ)言社區(qū)已經(jīng)有人開源了一個(gè)名為?bluemonkey?的工具包,它可以幫助我們?cè)阡秩?HTML 時(shí)過(guò)濾掉所有潛在的不安全內(nèi)容,而非無(wú)腦地對(duì)所有字符進(jìn)行轉(zhuǎn)義。
示例文件?template_bluemonkey.go
package mainimport ("fmt""html/template""log""net/http""github.com/microcosm-cc/bluemonday")func main() {p := bluemonday.UGCPolicy()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 創(chuàng)建模板對(duì)象并添加自定義模板函數(shù)tmpl := template.New("test").Funcs(template.FuncMap{"sanitize": func(s string) template.HTML {return template.HTML(p.Sanitize(s))
},
})// 解析模板內(nèi)容_, err := tmpl.Parse(`
{{.content | sanitize}}
`)if err != nil {fmt.Fprintf(w, "Parse: %v", err)return
}// 調(diào)用模板對(duì)象的渲染方法
err = tmpl.Execute(w, map[string]interface{}{"content": `Google`,
})if err != nil {
fmt.Fprintf(w, "Execute: %v", err)return
}
})
log.Println("Starting HTTP server...")
log.Fatal(http.ListenAndServe("localhost:4000", nil))
}
嘗試運(yùn)行以上代碼后,可以在瀏覽器獲得以下頁(yè)面:
從上圖中無(wú)法看出?bluemonkey?具體做了什么,我們可以通過(guò)終端查看:
? curl http://localhost:4000不難發(fā)現(xiàn),onblur="alert(secret)"?已經(jīng)被過(guò)濾掉了。這個(gè)工具包的功能非常強(qiáng)大,感興趣的同學(xué)可以自行查看文檔做更深入的研究。
修改分隔符
在本節(jié)課的最后,我們來(lái)快速學(xué)習(xí)一下如何修改模板的分隔符,因?yàn)闃?biāo)準(zhǔn)庫(kù)的模板引擎使用的花括號(hào)?{{?和?}}?和許多流行的前端框架有沖突(如 VueJS 和 AngularJS),所以知道怎么修改它們是非常有用的。
示例文件?template_delims.go
package mainimport ("fmt""log""net/http""text/template")func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {// 創(chuàng)建模板對(duì)象并解析模板內(nèi)容tmpl, err := template.New("test").Delims("[[", "]]").Parse(`[[.content]]`)if err != nil {
fmt.Fprintf(w, "Parse: %v", err)return
}// 調(diào)用模板對(duì)象的渲染方法
err = tmpl.Execute(w, map[string]interface{}{"content": "Hello world!",
})if err != nil {
fmt.Fprintf(w, "Execute: %v", err)return
}
})
log.Println("Starting HTTP server...")
log.Fatal(http.ListenAndServe("localhost:4000", nil))
}
在這里,我們通過(guò)?Delims?方法將它們分別修改為方括號(hào)?[[?和?]]。
嘗試運(yùn)行以上代碼可以在終端獲得以下結(jié)果:
? curl http://localhost:4000Hello world!
小結(jié)
這節(jié)課,我們主要學(xué)習(xí)了標(biāo)準(zhǔn)庫(kù)中?text/template?包提供的文本模板引擎的作用域、管道操作、模板函數(shù)和模板復(fù)用,以及如何安全地渲染 HTML 內(nèi)容。
下節(jié)課,我們將學(xué)習(xí)如何接收和處理 HTML 表單數(shù)據(jù)。
???
點(diǎn)擊原文鏈接可以到 Go 語(yǔ)言中文網(wǎng)參與討論
總結(jié)
以上是生活随笔為你收集整理的request用法_Go 语言 Web 应用开发 第 04 课:高级模板用法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: clover configurator_
- 下一篇: 伪代码的简单例子_使用策略+工厂模式彻底