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