Files
build-web-application-with-…/zh-tw/07.4.md
2019-02-26 01:40:54 +08:00

375 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 7.4 範本處理
## 什麼是範本
你一定聽說過一種叫做MVC的設計模式Model處理資料View展現結果Controller控制使用者的請求至於View層的處理在很多動態語言裡面都是透過在靜態HTML中插入動態語言產生的資料例如JSP中透過插入`<%=....=%>`PHP中透過插入`<?php.....?>`來實現的。
透過下面這個圖可以說明範本的機制
![](images/7.4.template.png?raw=true)
圖7.1 範本機制圖
Web應用反饋給客戶端的資訊中的大部分內容是靜態的不變的而另外少部分是根據使用者的請求來動態產生的例如要顯示使用者的訪問記錄列表。使用者之間只有記錄資料是不同的而列表的樣式則是固定的此時採用範本可以複用很多靜態程式碼。
## Go範本使用
在Go語言中我們使用`template`套件來進行範本處理,使用類似`Parse``ParseFile``Execute`等方法從檔案或者字串載入範本然後執行類似上面圖片展示的範本的merge操作。請看下面的例子
```Go
func handler(w http.ResponseWriter, r *http.Request) {
t := template.New("some template") //建立一個範本
t, _ = t.ParseFiles("tmpl/welcome.html") //解析範本檔案
user := GetUser() //取得當前使用者資訊
t.Execute(w, user) //執行範本的merger操作
}
```
透過上面的例子我們可以看到Go語言的範本操作非常的簡單方便和其他語言的範本處理類似都是先取得資料然後渲染資料。
為了示範和測試程式碼的方便,我們在接下來的例子中採用如下格式的程式碼
- 使用Parse代替ParseFiles因為Parse可以直接測試一個字串而不需要額外的檔案
- 不使用handler來寫示範程式碼而是每個測試一個main方便測試
- 使用`os.Stdout`代替`http.ResponseWriter`,因為`os.Stdout`實現了`io.Writer`介面
## 範本中如何插入資料?
上面我們示範瞭如何解析並渲染範本接下來讓我們來更加詳細的瞭解如何把資料渲染出來。一個範本都是應用在一個Go的物件之上Go物件的欄位如何插入到範本中呢
### 欄位操作
Go語言的範本透過`{{}}`來包含需要在渲染時被替換的欄位,`{{.}}`表示當前的物件這和Java或者C++中的this類似如果要訪問當前物件的欄位透過`{{.FieldName}}`,但是需要注意一點:這個欄位必須是匯出的(欄位首字母必須是大寫的),否則在渲染的時候就會報錯,請看下面的這個例子:
```Go
package main
import (
"html/template"
"os"
)
type Person struct {
UserName string
}
func main() {
t := template.New("fieldname example")
t, _ = t.Parse("hello {{.UserName}}!")
p := Person{UserName: "Astaxie"}
t.Execute(os.Stdout, p)
}
```
上面的程式碼我們可以正確的輸出`hello Astaxie`,但是如果我們稍微修改一下程式碼,在範本中含有了未匯出的欄位,那麼就會報錯
```Go
type Person struct {
UserName string
email string //未匯出的欄位,首字母是小寫的
}
t, _ = t.Parse("hello {{.UserName}}! {{.email}}")
```
上面的程式碼就會報錯,因為我們呼叫了一個未匯出的欄位,但是如果我們呼叫了一個不存在的欄位是不會報錯的,而是輸出為空。
如果範本中輸出`{{.}}`這個一般應用於字串物件預設會呼叫fmt套件輸出字串的內容。
### 輸出巢狀欄位內容
上面我們例子展示瞭如何針對一個物件的欄位輸出,那麼如果欄位裡面還有物件,如何來迴圈的輸出這些內容呢?我們可以使用`{{with …}}…{{end}}``{{range …}}{{end}}`來進行資料的輸出。
- {{range}} 這個和Go語法裡面的range類似迴圈操作資料
- {{with}}操作是指當前物件的值,類似上下文的概念
詳細的使用請看下面的例子:
```Go
package main
import (
"html/template"
"os"
)
type Friend struct {
Fname string
}
type Person struct {
UserName string
Emails []string
Friends []*Friend
}
func main() {
f1 := Friend{Fname: "minux.ma"}
f2 := Friend{Fname: "xushiwei"}
t := template.New("fieldname example")
t, _ = t.Parse(`hello {{.UserName}}!
{{range .Emails}}
an email {{.}}
{{end}}
{{with .Friends}}
{{range .}}
my friend name is {{.Fname}}
{{end}}
{{end}}
`)
p := Person{UserName: "Astaxie",
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
Friends: []*Friend{&f1, &f2}}
t.Execute(os.Stdout, p)
}
```
### 條件處理
在Go範本裡面如果需要進行條件判斷那麼我們可以使用和Go語言的`if-else`語法類似的方式來處理如果pipeline為空那麼if就認為是false下面的例子展示瞭如何使用`if-else`語法:
```Go
package main
import (
"os"
"text/template"
)
func main() {
tEmpty := template.New("template test")
tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不會輸出. {{end}}\n"))
tEmpty.Execute(os.Stdout, nil)
tWithValue := template.New("template test")
tWithValue = template.Must(tWithValue.Parse("不為空的 pipeline if demo: {{if `anything`}} 我有內容,我會輸出. {{end}}\n"))
tWithValue.Execute(os.Stdout, nil)
tIfElse := template.New("template test")
tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n"))
tIfElse.Execute(os.Stdout, nil)
}
```
透過上面的示範程式碼我們知道`if-else`語法相當的簡單,在使用過程中很容易整合到我們的範本程式碼中。
> 注意if裡面無法使用條件判斷例如.Mail=="astaxie@gmail.com"這樣的判斷是不正確的if裡面只能是bool值
### pipelines
Unix使用者已經很熟悉什麼是`pipe`了,`ls | grep "beego"`類似這樣的語法你是不是經常使用,過濾當前目錄下面的檔案,顯示含有"beego"的資料表達的意思就是前面的輸出可以當做後面的輸入最後顯示我們想要的資料而Go語言範本最強大的一點就是支援pipe資料在Go語言裡面任何`{{}}`裡面的都是pipelines資料例如我們上面輸出的email裡面如果還有一些可能引起XSS注入的那麼我們如何來進行轉化呢
```Go
{{. | html}}
```
在email輸出的地方我們可以採用如上方式可以把輸出全部轉化html的實體上面的這種方式和我們平常寫Unix的方式是不是一模一樣操作起來相當的簡便呼叫其他的函式也是類似的方式。
### 範本變數
有時候,我們在範本使用過程中需要定義一些區域性變數,我們可以在一些操作中申明區域性變數,例如`with``range``if`過程中申明區域性變數,這個變數的作用域是`{{end}}`之前Go語言透過申明的區域性變數格式如下所示
```Go
$variable := pipeline
```
詳細的例子看下面的:
```Go
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
```
### 範本函式
範本在輸出物件的欄位值時,採用了`fmt`套件把物件轉化成了字串。但是有時候我們的需求可能不是這樣的,例如有時候我們為了防止垃圾郵件傳送者透過採集網頁的方式來發送給我們的郵箱資訊,我們希望把`@`替換成`at`例如:`astaxie at beego.me`,如果要實現這樣的功能,我們就需要自訂函式來做這個功能。
每一個範本函式都有一個唯一值的名字然後與一個Go函式關聯透過如下的方式來關聯
```Go
type FuncMap map[string]interface{}
```
例如如果我們想要的email函式的範本函式名是`emailDeal`它關聯的Go函式名稱是`EmailDealWith`,那麼我們可以透過下面的方式來註冊這個函式
```Go
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
```
`EmailDealWith`這個函式的引數和返回值定義如下:
```Go
func EmailDealWith(args interface{}) string
```
我們來看下面的實現例子:
```Go
package main
import (
"fmt"
"html/template"
"os"
"strings"
)
type Friend struct {
Fname string
}
type Person struct {
UserName string
Emails []string
Friends []*Friend
}
func EmailDealWith(args ...interface{}) string {
ok := false
var s string
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
s = fmt.Sprint(args...)
}
// find the @ symbol
substrs := strings.Split(s, "@")
if len(substrs) != 2 {
return s
}
// replace the @ by " at "
return (substrs[0] + " at " + substrs[1])
}
func main() {
f1 := Friend{Fname: "minux.ma"}
f2 := Friend{Fname: "xushiwei"}
t := template.New("fieldname example")
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
t, _ = t.Parse(`hello {{.UserName}}!
{{range .Emails}}
an emails {{.|emailDeal}}
{{end}}
{{with .Friends}}
{{range .}}
my friend name is {{.Fname}}
{{end}}
{{end}}
`)
p := Person{UserName: "Astaxie",
Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"},
Friends: []*Friend{&f1, &f2}}
t.Execute(os.Stdout, p)
}
```
上面示範瞭如何自訂函式,其實,在範本套件內部已經有內建的實現函式,下面程式碼擷取自範本套件裡面
```Go
var builtins = FuncMap{
"and": and,
"call": call,
"html": HTMLEscaper,
"index": index,
"js": JSEscaper,
"len": length,
"not": not,
"or": or,
"print": fmt.Sprint,
"printf": fmt.Sprintf,
"println": fmt.Sprintln,
"urlquery": URLQueryEscaper,
}
```
## Must操作
範本套件裡面有一個函式`Must`它的作用是檢測範本是否正確例如大括號是否匹配註釋是否正確的關閉變數是否正確的書寫。接下來我們示範一個例子用Must來判斷範本是否正確
```Go
package main
import (
"fmt"
"text/template"
)
func main() {
tOk := template.New("first")
template.Must(tOk.Parse(" some static text /* and a comment */"))
fmt.Println("The first one parsed OK.")
template.Must(template.New("second").Parse("some static text {{ .Name }}"))
fmt.Println("The second one parsed OK.")
fmt.Println("The next one ought to fail.")
tErr := template.New("check parse error with Must")
template.Must(tErr.Parse(" some static text {{ .Name }"))
}
```
將輸出如下內容
```
The first one parsed OK.
The second one parsed OK.
The next one ought to fail.
panic: template: check parse error with Must:1: unexpected "}" in command
```
## 巢狀範本
我們平常開發Web應用的時候經常會遇到一些範本有些部分是固定不變的然後可以抽取出來作為一個獨立的部分例如一個部落格的頭部和尾部是不變的而唯一改變的是中間的內容部分。所以我們可以定義成`header``content``footer`三個部分。Go語言中透過如下的語法來申明
```Go
{{define "子範本名稱"}}內容{{end}}
```
透過如下方式來呼叫:
```Go
{{template "子範本名稱"}}
```
接下來我們示範如何使用巢狀範本,我們定義三個檔案,`header.tmpl``content.tmpl``footer.tmpl`檔案,裡面的內容如下
```html
//header.tmpl
{{define "header"}}
<html>
<head>
<title>示範資訊</title>
</head>
<body>
{{end}}
//content.tmpl
{{define "content"}}
{{template "header"}}
<h1>示範巢狀</h1>
<ul>
<li>巢狀使用define定義子範本</li>
<li>呼叫使用template</li>
</ul>
{{template "footer"}}
{{end}}
//footer.tmpl
{{define "footer"}}
</body>
</html>
{{end}}
```
示範程式碼如下:
```Go
package main
import (
"fmt"
"os"
"text/template"
)
func main() {
s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")
s1.ExecuteTemplate(os.Stdout, "header", nil)
fmt.Println()
s1.ExecuteTemplate(os.Stdout, "content", nil)
fmt.Println()
s1.ExecuteTemplate(os.Stdout, "footer", nil)
fmt.Println()
s1.Execute(os.Stdout, nil)
}
```
透過上面的例子我們可以看到透過`template.ParseFiles`把所有的巢狀範本全部解析到範本裡面,其實每一個定義的{{define}}都是一個獨立的範本他們相互獨立是並行存在的關係內部其實儲存的是類似map的一種關係(key是範本的名稱value是範本的內容),然後我們透過`ExecuteTemplate`來執行相應的子範本內容我們可以看到header、footer都是相對獨立的都能輸出內容content 中因為嵌套了header和footer的內容就會同時輸出三個的內容。但是當我們執行`s1.Execute`,沒有任何的輸出,因為在預設的情況下沒有預設的子範本,所以不會輸出任何的東西。
>同一個集合類別的範本是互相知曉的,如果同一範本被多個集合使用,則它需要在多個集合中分別解析
## 總結
透過上面對範本的詳細介紹我們瞭解瞭如何把動態資料與範本融合如何輸出迴圈資料、如何自訂函式、如何巢狀範本等等。透過範本技術的應用我們可以完成MVC模式中V的處理接下來的章節我們將介紹如何來處理M和C。
## links
* [目錄](<preface.md>)
* 上一節: [正則處理](<07.3.md>)
* 下一節: [檔案操作](<07.5.md>)