這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go 官方庫提供了兩個模板庫: text/template 和 html/template。這兩個庫類似,只不過 html/template對html格式做了特別的處理,當需要輸出html格式的代碼時需要使用html/template。
使用模版,可以協助我們寫一些通用的代碼,或者提供清晰的檔案布局, 或者提供一個代碼產生器。
官方文檔提供了很好的模版的使用方法, 其中 text/template提供了基礎的模版的使用方法,比如 Action、 Argument、Pipeline、Variable、Function、模版嵌套的介紹, html/template對 Context 進行了介紹。 本文假定你已經瞭解了這些基礎知識。如果你還不清楚,或者還沒有用過模版,可以參考文末的參考文檔進行學習。
雖然text/template官方文檔對模版嵌套簡單了介紹,但是對於如何使用嵌套模版進行實際開發,以及注意事項並沒有詳細的介紹,所以本文著重介紹嵌套模版的使用。
雖然本文名稱為嵌套模版的最佳實務,但是準確的講,這隻是最佳實務之一。如果讀者有其它的實踐方案,或者更好的處理模版嵌套的方案,歡迎討論。
在我們開發web應用程式的時候,不可避免的要使用到模版。
一般一個網站包含很多的頁面,比如新聞頁面、註冊頁面、文章列表等。 不同的新聞可能使用同一個新聞布局模版,不同的文章詳細頁也可能使用同一個文章布局頁面, 同時新聞布局模版和文章布局頁面也可能有一些公用的東西,比如header、footer、導覽列等。如何將這些公用的東西抽取成統一的模版呢?
本文將逐步介紹這些技術, 首先介紹嵌套檔案的使用。
Parse vs ParseFiles vs ParseBlob
首先我們看一下這三個檔案有何不同。 事實上是五個檔案:
12345 |
func ParseFiles(filenames ...string) (*Template, error)func ParseGlob(pattern string) (*Template, error)func (t *Template) Parse(text string) (*Template, error)func (t *Template) ParseFiles(filenames ...string) (*Template, error)func (t *Template) ParseGlob(pattern string) (*Template, error) |
Parse用來解析一個字串,字串代表模版的內容,並且嵌套的模版也會和這個模版進行關聯。
123 |
tmpl, err := template.New("name").Parse(...)// Error checking elidederr = tmpl.Execute(out, data) |
和
1234 |
import "html/template"...t, err := template.New("foo").Parse(`\{\{define "T"\}\}Hello, \{\{.\}\}!\{\{end\}\}`)err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>") |
ParseFiles用來解析一組命名的檔案模版,當模版定義在不同的檔案中的時候,使用這個方法可以產生一個可執行檔模版,模版執行的時候不會出錯。
ParseGlob與ParseFiles類似,它使用filepath.Glob模式比對的方式遍曆檔案,將這些檔案產生模版。
這兩個函數還可以作為 *Template的方法使用。作為函數使用的時候,它返回模版的名字是第一個檔案的名字,模版以第一個檔案作為base模版。
同時後面的檔案也會產生模版作為這個模版的關聯模板,你可以通過Lookup方法尋找到這個模版,因為每個模版都儲存著它的關聯模版:
1 |
set map[string]*Template |
我們以一個例子示範一下嵌套模版的使用。
當前檔案夾下有兩個檔案。
header.html
12 |
Title is \{\{.Title\}\}\{\{template "footer"\}\} |
footer.html
1 |
\{\{define "footer"\}\}Body is \{\{.Body\}\}\{\{end\}\} |
測試程式:
12345678910111213141516171819202122 |
package mainimport ("fmt""html/template""net/http")func handler(w http.ResponseWriter, r *http.Request) {t, _ := template.ParseFiles("header.html", "footer.html")err := t.Execute(w, map[string]string{"Title": "My title", "Body": "Hi this is my body"})if err != nil {panic(err)}}func main() {http.HandleFunc("/", handler)http.ListenAndServe(":8080", nil)} |
瀏覽器訪問 http://localhost:8080/會得到渲染的結果:
12 |
Title is My titleBody is |
可以看到結果基本符合預期。header.html嵌套了footer模版,渲染的時候以header.html為主模版顯示,嵌套渲染了footer.html。
但是上面的結果顯示有一點點問題,就是footer渲染的時候並沒有顯示body的結果,這是因為data傳給了主模版,嵌套模版如果要使用這個資料,需要在嵌套的地方把data傳遞給它。我們可以修改header.html:
12 |
Title is \{\{.Title\}\}\{\{template "footer" <div class=""></div>\}\} |
這樣footer.html就可以使用傳入的data渲染了:
12 |
Title is My titleBody is Hi this is my body |
在使用ParseFiles、ParseGlob 函數 的時候,預設以檔案的路徑的最後一個部分作為模版名稱, 比如檔案a/foo的模版名稱為foo,但是如果參數中不同的檔案夾下有相同的檔案名稱時,最後那個同名的模版檔案會"覆蓋"前面的重名檔案模版,官方庫的實現中不能儲存重名的模版檔案。
Execute vs ExecuteTemplate
上面的例子顯示了一個簡單的嵌套模版的使用,但是如果我們把兩個檔案的順序交換一下,如下所示:
12345678 |
func handler(w http.ResponseWriter, r *http.Request) {t, _ := template.ParseFiles("footer.html", "header.html")err := t.Execute(w, map[string]string{"Title": "My title", "Body": "Hi this is my body"})if err != nil {panic(err)}} |
運行後瀏覽器訪問一下,會發現沒報錯,但是也沒有渲染任何東西。
這是因為`template.ParseFiles`返回的模版以第一個為主模版。在這種情況下footer.html沒有可渲染的東西。
在這種情況下,我們就需要顯示的指定要渲染的模版:
123456789 |
func handler(w http.ResponseWriter, r *http.Request) {t, _ := template.ParseFiles("footer.html", "header.html")//err := t.Execute(w, map[string]string{"Title": "My title", "Body": "Hi this is my body"})err := t.ExecuteTemplate(w, "header.html", map[string]string{"Title": "My title", "Body": "Hi this is my body"})if err != nil {panic(err)}} |
使用ExecuteTemplate可以選擇渲染t關聯的模版作為渲染的主模版。上面的例子中我們選擇header.html作為主模版,所以它可以正常渲染。
或者, 通過下面的方法也能正常渲染,它和上面的代碼基本是等價的(只有escape細微的差別)。
123456789 |
func handler(w http.ResponseWriter, r *http.Request) {t, _ := template.ParseFiles("footer.html", "header.html")t = t.Lookup("header.html")err := t.Execute(w, map[string]string{"Title": "My title", "Body": "Hi this is my body"})//err := t.ExecuteTemplate(w, "header.html", map[string]string{"Title": "My title", "Body": "Hi this is my body"})if err != nil {panic(err)}} |
所以可以看到Execute和ExecuteTemplate的功能的差別,當你想用指定的關聯的模版渲染時,就使用ExecuteTemplate。
最佳實務
有了上面的基礎,我們就可以解決文章開始提出的問題: 如何規劃一個網站的模版?
假設有三個檔案: hello.html、header.html、footer.html,其中hello.html是一個欄目的統一布局, header.html是所有的布局中需要嵌套的html的首部,比如網站描述、css、導覽列等,footer.html包含全域的javascript檔案的引入、著作權聲明等等。
我們建立兩個檔案夾:layouts、widgets,將hello.html放入到layouts檔案夾中,header.html、footer.html放入到widgets檔案夾中,
我們就以這三個檔案作為例子,看看嵌套模版在網站中的應用。
但是在進一步介紹之前,我需要介紹上面代碼中的一個問題。可以看到,上面的每個請求都會讀模數版檔案再進行解析,這會嚴重影響程式的效能,解決方案就是預先讀取這些檔案並產生Template對象,這樣請求來了就直接使用解析好的template對象即可。
12345678910111213141516171819202122232425 |
var templates map[string]*template.Templatefunc init() {if templates == nil {templates = make(map[string]*template.Template)}//templatesDir := "./templates/" templatesDir := "./"layouts, err := filepath.Glob(templatesDir + "layouts/*.html")if err != nil {log.Fatal(err)}widgets, err := filepath.Glob(templatesDir + "widgets/*.html")if err != nil {log.Fatal(err)}for _, layout := range layouts {files := append(widgets, layout)templates[filepath.Base(layout)] = template.Must(template.ParseFiles(files...))}} |
通過一個map對象,我們儲存了每一個布局對應的template對象。
然後提供一個模版渲染的方法:
123456789 |
func renderTemplate(w http.ResponseWriter, name string, data interface{}) error {tmpl, ok := templates[name]if !ok {return fmt.Errorf("The template %s does not exist.", name)}w.Header().Set("Content-Type", "text/html; charset=utf-8")return tmpl.ExecuteTemplate(w, name, data)} |
我在為rpcx開發web GUI的時候使用了這個方法:rpcx-ui。
當然通過一些變換也可以實現其它的模版設計, 比如一個base.html模版,裡面定義好了header,footer,還嵌套了body模版,通過不同的body模版實現不同模版布局,比如news.html定義了顯示news的body, music.html定義了music的body,這也是一個不錯的模版布局。
參考文檔
- https://golang.org/pkg/text/template/
- https://golang.org/pkg/html/template/
- https://elithrar.github.io/article/approximating-html-template-inheritance/
- https://groups.google.com/forum/#!topic/golang-nuts/EweRbwa_tks
- http://stackoverflow.com/questions/12224436/multiple-files-using-template-parsefiles-in-golang
- https://gohugo.io/templates/go-templates/
- https://astaxie.gitbooks.io/build-web-application-with-golang/content/en/07.4.html