## 4.6. 文本和HTML模闆 前面的例子,隻是最簡單的格式化,使用Printf是完全足夠的。但是有時候會需要複雜的打印格式,這時候一般需要將格式化代碼分離出來以便更安全地脩改。這寫功能是由text/template和html/template等模闆包提供的,它們提供了一個將變量值填充到一個文本或HTML格式的模闆的機製。 一個模闆是一個字符串或一個文件,里面包含了一個或多個由雙花括號包含的`{{action}}`對象。大部分的字符串隻是按面值打印,但是對於actions部分將觸發其它的行爲。每個actions都包含了一個用模闆語言書寫的表達式,一個action雖然簡短但是可以輸出複雜的打印值,模闆語言包含通過選擇結構體的成員、調用函數或方法、表達式控製流if-else語句和range循環語句,還有其它實例化模闆等諸多特性。下面是一個簡單的模闆字符串: {% raw %} ```Go gopl.io/ch4/issuesreport const templ = `{{.TotalCount}} issues: {{range .Items}}---------------------------------------- Number: {{.Number}} User: {{.User.Login}} Title: {{.Title | printf "%.64s"}} Age: {{.CreatedAt | daysAgo}} days {{end}}` ``` {% endraw %} 這個模闆先打印匹配到的issue總數,然後打印每個issue的編號、創建用戶、標題還有存在的時間。對於每一個action,都有一個當前值的概念,對應點操作符,寫作“.”。當前值“.”最初被初始化爲調用模闆是的參數,在當前例子中對應github.IssuesSearchResult類型的變量。模闆中`{{.TotalCount}}`對應action將展開爲結構體中TotalCount成員以默認的方式打印的值。模闆中`{{range .Items}}`和`{{end}}`對應一個循環action,因此它們直接的內容可能會被展開多次,循環每次迭代的當前值對應當前的Items元素的值。 在一個action中,`|`操作符表示將前一個表達式的結果作爲後一個函數的輸入,類似於UNIX中管道的概念。在Title這一行的action中,第二個操作是一個printf函數,是一個基於fmt.Sprintf實現的內置函數,所有模闆都可以直接使用。對於Age部分,第二個動作是一個叫daysAgo的函數,通過time.Since函數將CreatedAt成員轉換爲過去的時間長度: ```Go func daysAgo(t time.Time) int { return int(time.Since(t).Hours() / 24) } ``` 需要註意的是CreatedAt的參數類型是time.Time,併不是字符串。以同樣的方式,我們可以通過定義一些方法來控製字符串的格式化(§2.5),一個類型同樣可以定製自己的JSON編碼和解碼行爲。time.Time類型對應的JSON值是一個標準時間格式的字符串。 生成模闆的輸出需要兩個處理步驟。第一步是要分析模闆併轉爲內部表示,然後基於指定的輸入執行模闆。分析模闆部分一般隻需要執行一次。下面的代碼創建併分析上面定義的模闆templ。註意方法調用鏈的順序:template.New先創建併返迴一個模闆;Funcs方法將daysAgo等自定義函數註冊到模闆中,併返迴模闆;最後調用Parse函數分析模闆。 ```Go report, err := template.New("report"). Funcs(template.FuncMap{"daysAgo": daysAgo}). Parse(templ) if err != nil { log.Fatal(err) } ``` 因爲模闆通常在編譯時就測試好了,如果模闆解析失敗將是一個致命的錯誤。template.Must輔助函數可以簡化這個致命錯誤的處理:它接受一個模闆和一個error類型的參數,檢測error是否爲nil(如果不是nil則發出panic異常),然後返迴傳入的模闆。我們將在5.9節再討論這個話題。 一旦模闆已經創建、註冊了daysAgo函數、併通過分析和檢測,我們就可以使用github.IssuesSearchResult作爲輸入源、os.Stdout作爲輸出源來執行模闆: ```Go var report = template.Must(template.New("issuelist"). Funcs(template.FuncMap{"daysAgo": daysAgo}). Parse(templ)) func main() { result, err := github.SearchIssues(os.Args[1:]) if err != nil { log.Fatal(err) } if err := report.Execute(os.Stdout, result); err != nil { log.Fatal(err) } } ``` 程序輸出一個純文本報告: ``` $ go build gopl.io/ch4/issuesreport $ ./issuesreport repo:golang/go is:open json decoder 13 issues: ---------------------------------------- Number: 5680 User: eaigner Title: encoding/json: set key converter on en/decoder Age: 750 days ---------------------------------------- Number: 6050 User: gopherbot Title: encoding/json: provide tokenizer Age: 695 days ---------------------------------------- ... ``` 現在讓我們轉到html/template模闆包。它使用和text/template包相同的API和模闆語言,但是增加了一個將字符串自動轉義特性,這可以避免輸入字符串和HTML、JavaScript、CSS或URL語法産生衝突的問題。這個特性還可以避免一些長期存在的安全問題,比如通過生成HTML註入攻擊,通過構造一個含有惡意代碼的問題標題,這些都可能讓模闆輸出錯誤的輸出,從而讓他們控製頁面。 下面的模闆以HTML格式輸出issue列表。註意import語句的不同: {% raw %} ```Go gopl.io/ch4/issueshtml import "html/template" var issueList = template.Must(template.New("issuelist").Parse(`
# | State | User | Title |
---|---|---|---|
{{.Number}} | {{.State}} | {{.User.Login}} | {{.Title}} |
A: {{.A}}
B: {{.B}}
` t := template.Must(template.New("escape").Parse(templ)) var data struct { A string // untrusted plain text B template.HTML // trusted HTML } data.A = "Hello!" data.B = "Hello!" if err := t.Execute(os.Stdout, data); err != nil { log.Fatal(err) } } ``` {% endraw %} 圖4.6顯示了出現在瀏覽器中的模闆輸出。我們看到A的黑體標記被轉義失效了,但是B沒有。 ![](../images/ch4-06.png) 我們這里隻講述了模闆繫統中最基本的特性。一如旣往,如果想了解更多的信息,請自己査看包文檔: ``` $ go doc text/template $ go doc html/template ``` **練習 4.14:** 創建一個web服務器,査詢一次GitHub,然後生成BUG報告、里程碑和對應的用戶信息。