Go 語言 文本和HTML模板

2023-03-14 16:53 更新

原文鏈接:https://gopl-zh.github.io/ch4/ch4-06.html


4.6. 文本和HTML模板

前面的例子,只是最簡(jiǎn)單的格式化,使用Printf是完全足夠的。但是有時(shí)候會(huì)需要復(fù)雜的打印格式,這時(shí)候一般需要將格式化代碼分離出來以便更安全地修改。這些功能是由text/template和html/template等模板包提供的,它們提供了一個(gè)將變量值填充到一個(gè)文本或HTML格式的模板的機(jī)制。

一個(gè)模板是一個(gè)字符串或一個(gè)文件,里面包含了一個(gè)或多個(gè)由雙花括號(hào)包含的{{action}}對(duì)象。大部分的字符串只是按字面值打印,但是對(duì)于actions部分將觸發(fā)其它的行為。每個(gè)actions都包含了一個(gè)用模板語言書寫的表達(dá)式,一個(gè)action雖然簡(jiǎn)短但是可以輸出復(fù)雜的打印值,模板語言包含通過選擇結(jié)構(gòu)體的成員、調(diào)用函數(shù)或方法、表達(dá)式控制流if-else語句和range循環(huán)語句,還有其它實(shí)例化模板等諸多特性。下面是一個(gè)簡(jiǎn)單的模板字符串:

gopl.io/ch4/issuesreport

const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User:   {{.User.Login}}
Title:  {{.Title | printf "%.64s"}}
Age:    {{.CreatedAt | daysAgo}} days
{{end}}`

這個(gè)模板先打印匹配到的issue總數(shù),然后打印每個(gè)issue的編號(hào)、創(chuàng)建用戶、標(biāo)題還有存在的時(shí)間。對(duì)于每一個(gè)action,都有一個(gè)當(dāng)前值的概念,對(duì)應(yīng)點(diǎn)操作符,寫作“.”。當(dāng)前值“.”最初被初始化為調(diào)用模板時(shí)的參數(shù),在當(dāng)前例子中對(duì)應(yīng)github.IssuesSearchResult類型的變量。模板中{{.TotalCount}}對(duì)應(yīng)action將展開為結(jié)構(gòu)體中TotalCount成員以默認(rèn)的方式打印的值。模板中{{range .Items}}{{end}}對(duì)應(yīng)一個(gè)循環(huán)action,因此它們之間的內(nèi)容可能會(huì)被展開多次,循環(huán)每次迭代的當(dāng)前值對(duì)應(yīng)當(dāng)前的Items元素的值。

在一個(gè)action中,|操作符表示將前一個(gè)表達(dá)式的結(jié)果作為后一個(gè)函數(shù)的輸入,類似于UNIX中管道的概念。在Title這一行的action中,第二個(gè)操作是一個(gè)printf函數(shù),是一個(gè)基于fmt.Sprintf實(shí)現(xiàn)的內(nèi)置函數(shù),所有模板都可以直接使用。對(duì)于Age部分,第二個(gè)動(dòng)作是一個(gè)叫daysAgo的函數(shù),通過time.Since函數(shù)將CreatedAt成員轉(zhuǎn)換為過去的時(shí)間長(zhǎng)度:

func daysAgo(t time.Time) int {
    return int(time.Since(t).Hours() / 24)
}

需要注意的是CreatedAt的參數(shù)類型是time.Time,并不是字符串。以同樣的方式,我們可以通過定義一些方法來控制字符串的格式化(§2.5),一個(gè)類型同樣可以定制自己的JSON編碼和解碼行為。time.Time類型對(duì)應(yīng)的JSON值是一個(gè)標(biāo)準(zhǔn)時(shí)間格式的字符串。

生成模板的輸出需要兩個(gè)處理步驟。第一步是要分析模板并轉(zhuǎn)為內(nèi)部表示,然后基于指定的輸入執(zhí)行模板。分析模板部分一般只需要執(zhí)行一次。下面的代碼創(chuàng)建并分析上面定義的模板templ。注意方法調(diào)用鏈的順序:template.New先創(chuàng)建并返回一個(gè)模板;Funcs方法將daysAgo等自定義函數(shù)注冊(cè)到模板中,并返回模板;最后調(diào)用Parse函數(shù)分析模板。

report, err := template.New("report").
    Funcs(template.FuncMap{"daysAgo": daysAgo}).
    Parse(templ)
if err != nil {
    log.Fatal(err)
}

因?yàn)槟0逋ǔT诰幾g時(shí)就測(cè)試好了,如果模板解析失敗將是一個(gè)致命的錯(cuò)誤。template.Must輔助函數(shù)可以簡(jiǎn)化這個(gè)致命錯(cuò)誤的處理:它接受一個(gè)模板和一個(gè)error類型的參數(shù),檢測(cè)error是否為nil(如果不是nil則發(fā)出panic異常),然后返回傳入的模板。我們將在5.9節(jié)再討論這個(gè)話題。

一旦模板已經(jīng)創(chuàng)建、注冊(cè)了daysAgo函數(shù)、并通過分析和檢測(cè),我們就可以使用github.IssuesSearchResult作為輸入源、os.Stdout作為輸出源來執(zhí)行模板:

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)
    }
}

程序輸出一個(gè)純文本報(bào)告:

$ 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
----------------------------------------
...

現(xiàn)在讓我們轉(zhuǎn)到html/template模板包。它使用和text/template包相同的API和模板語言,但是增加了一個(gè)將字符串自動(dòng)轉(zhuǎn)義特性,這可以避免輸入字符串和HTML、JavaScript、CSS或URL語法產(chǎn)生沖突的問題。這個(gè)特性還可以避免一些長(zhǎng)期存在的安全問題,比如通過生成HTML注入攻擊,通過構(gòu)造一個(gè)含有惡意代碼的問題標(biāo)題,這些都可能讓模板輸出錯(cuò)誤的輸出,從而讓他們控制頁面。

下面的模板以HTML格式輸出issue列表。注意import語句的不同:

gopl.io/ch4/issueshtml

import "html/template"

var issueList = template.Must(template.New("issuelist").Parse(`
<h1>{{.TotalCount}} issues</h1>
<table>
<tr style='text-align: left'>
  <th>#</th>
  <th>State</th>
  <th>User</th>
  <th>Title</th>
</tr>
{{range .Items}}
<tr>
  <td><a href='{{.HTMLURL}}'>{{.Number}}</a></td>
  <td>{{.State}}</td>
  <td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
  <td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
</tr>
{{end}}
</table>
`))

下面的命令將在新的模板上執(zhí)行一個(gè)稍微不同的查詢:

$ go build gopl.io/ch4/issueshtml
$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html

圖4.4顯示了在web瀏覽器中的效果圖。每個(gè)issue包含到Github對(duì)應(yīng)頁面的鏈接。


圖4.4中issue沒有包含會(huì)對(duì)HTML格式產(chǎn)生沖突的特殊字符,但是我們馬上將看到標(biāo)題中含有&<字符的issue。下面的命令選擇了兩個(gè)這樣的issue:

$ ./issueshtml repo:golang/go 3133 10535 >issues2.html

圖4.5顯示了該查詢的結(jié)果。注意,html/template包已經(jīng)自動(dòng)將特殊字符轉(zhuǎn)義,因此我們依然可以看到正確的字面值。如果我們使用text/template包的話,這2個(gè)issue將會(huì)產(chǎn)生錯(cuò)誤,其中“&lt;”四個(gè)字符將會(huì)被當(dāng)作小于字符“<”處理,同時(shí)“<link>”字符串將會(huì)被當(dāng)作一個(gè)鏈接元素處理,它們都會(huì)導(dǎo)致HTML文檔結(jié)構(gòu)的改變,從而導(dǎo)致有未知的風(fēng)險(xiǎn)。

我們也可以通過對(duì)信任的HTML字符串使用template.HTML類型來抑制這種自動(dòng)轉(zhuǎn)義的行為。還有很多采用類型命名的字符串類型分別對(duì)應(yīng)信任的JavaScript、CSS和URL。下面的程序演示了兩個(gè)使用不同類型的相同字符串產(chǎn)生的不同結(jié)果:A是一個(gè)普通字符串,B是一個(gè)信任的template.HTML字符串類型。


gopl.io/ch4/autoescape

func main() {
    const templ = `<p>A: {{.A}}</p><p>B: {{.B}}</p>`
    t := template.Must(template.New("escape").Parse(templ))
    var data struct {
        A string        // untrusted plain text
        B template.HTML // trusted HTML
    }
    data.A = "<b>Hello!</b>"
    data.B = "<b>Hello!</b>"
    if err := t.Execute(os.Stdout, data); err != nil {
        log.Fatal(err)
    }
}

圖4.6顯示了出現(xiàn)在瀏覽器中的模板輸出。我們看到A的黑體標(biāo)記被轉(zhuǎn)義失效了,但是B沒有。


我們這里只講述了模板系統(tǒng)中最基本的特性。一如既往,如果想了解更多的信息,請(qǐng)自己查看包文檔:

$ go doc text/template
$ go doc html/template

練習(xí) 4.14: 創(chuàng)建一個(gè)web服務(wù)器,查詢一次GitHub,然后生成BUG報(bào)告、里程碑和對(duì)應(yīng)的用戶信息。



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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)