# 7.4 テンプレートの処理 ## テンプレートとは何か おそらくあなたはMVCのデザインパターンについて聞いたことがあると思います。Modelはデータを処理を、Viewは表示結果を、Controllerはユーザのリクエストの制御を行います。Viewレイヤーの処理では、多くの動的な言語ではどれも静的なHTMLの中に動的言語が生成したデータを挿入します。例えばJSPでは`<%=....=%>`を挿入することで、PHPでは``を挿入することで実現します。 下の図でテンプレートのメカニズムについてご紹介します ![](images/7.4.template.png?raw=true) 図7.1 テンプレートのメカニズム図 Webアプリケーションがクライアントに返すフィードバックの情報の中の大部分の内容は静的で不変です。また少ない部分でユーザのリクエストによって動的に生成されるものがあります。例えばユーザのアクセスログリストを表示したい場合、ユーザ間ではログデータが異なるのみで、リストのスタイルは固定です。この時テンプレートを用いることで多くの静的なコードを使いまわすことができます。 ## Goのテンプレートの使用 Go言語では、`template`パッケージを使用してテンプレートの処理を行います。`Parse`、`ParseFile`、`Execute`といった方法を使ってファイルや文字列からテンプレートをロードします。その後植えの図で示したテンプレートのmerge操作のようなものを実行します。下の例をご覧ください: func handler(w http.ResponseWriter, r *http.Request) { t := template.New("some template") //テンプレートを新規に作成する。 t, _ = t.ParseFiles("tmpl/welcome.html", nil) //テンプレートファイルを解析 user := GetUser() //現在のユーザの情報を取得する。 t.Execute(w, user) //テンプレートのmerger操作を実行する。 } 上の例で、Go言語のテンプレート操作は非常に簡単で便利だとおわかりいただけるかと思います。その他の言語のテンプレート処理に似ていて、まずデータを取得した後データを適用します。 デモとテストコードの簡便のため、以降の例では以下の形式のコードを採用します。 - ParseFilesの代わりにParseを使用します。Parseは直接文字列をテストでき、外部のファイルを必要としないためです。 - handlerを使ってデモコードを書くことはせず、それぞれひとつのmainをテストします。便利なテストです。 - `http.ResponseWriter`の代わりに`os.Stdout`を使用します。`os.Stdout`は`io.Writer`インターフェースを実装しているからです。 ## どのようにしてテンプレートの中にデータを挿入するのか? 上においてどのように解析とテンプレートの適用するかデモを行いました。以降ではさらに詳しくどのようにデータを適用していくのか理解していきましょう。テンプレートはすべてGoのオブジェクト上で適用されます。Goオブジェクトのフィールドはどのようにしてテンプレートの中に挿入されるのでしょうか? ### フィールドの操作 Go言語のテンプレートは`{{}}`を通して適用時に置換する必要のあるフィールドを含めます。`{{.}}`は現在のオブジェクトを示しています。これはJavaやC++の中のthisに似たものです。もし現在のオブジェクトのフィールドにアクセスしたい場合は`{{.FieldName}}`というようにします。ただし注意してください:このフィールドは必ずエクスポートされたものとなります(頭文字が大文字になります)、さもなければ適用時にエラーを発生させます。下の例をご覧ください: 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`と出力されます。しかしもしコードに修正を加え、テンプレートにエクスポートされていないフィールドを含むと、エラーを発生させます。 type Person struct { UserName string email string //エクスポートされていないフィールド、頭文字が小文字です。 } t, _ = t.Parse("hello {{.UserName}}! {{.email}}") 上のコードはエラーを発生させます。なぜならエクスポートされていないフィールドをコールしたためです。しかしもし存在しないフィールドをコールした場合はエラーを発生させず、空文字列を出力します。 テンプレートで`{{.}}`を出力すると、一般的には文字列オブジェクトに対して適用されます。デフォルトでfmtパッケージがコールされ文字列の内容が出力されます。 ### ネストしたフィールドの内容の出力 上の例でどのようにひとつのオブジェクトのフィールドを出力するか示しました。もしフィールドの中にまたオブジェクトがある場合は、どのようにループしてこれらの内容を出力するのでしょうか?ここでは`{{with ...}}...{{end}}`と`{{range ...}}{{end}}`によってデータを出力することができます。 - {{range}} はGo言語の中のrangeに似ています。ループしてデータを操作します - {{with}}操作是指当前对象的值,类似上下文的概念 - {{with}}操作は現在のオブジェクトの値を指します。コンテキストの概念に似ています。 詳細な使用方法は以下の例をご覧ください: 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`文を使用するか示します: 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インジェクションを引き起こす可能性があるとすると、どのように変換するのでしょうか? {{. | html}} emailが出力される場所では上のような方法で出力をすべてhtmlの実体に変換することができます。上のような方法は我々が普段書いているUnixの方法とまったく一緒ではないですか。とても簡単に操作することができます。他の関数をコールする場合も似たような方法となります。 ### テンプレート変数 ときどき、テンプレートを使っていてローカル変数を定義したい場合があります。操作の中でローカル変数を宣言することができます。例えば`with``range``if`プロセスではローカル変数を宣言します。この変数のスコープは`{{end}}`の前です。Go言語で宣言されたローカル変数の形式は以下のとおりです: $variable := pipeline 詳細な例は以下をご覧ください: {{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`,如果要实现这样的功能,我们就需要自定义函数来做这个功能。 テンプレートがオブジェクトのフィールドの値を出力する際、`fmt`パッケージを採用してオブジェクトを文字列に変換します。しかしときどき我々はこうしたくはないときもあります。 每一个模板函数都有一个唯一值的名字,然后与一个Go函数关联,通过如下的方式来关联 type FuncMap map[string]interface{} 例如,如果我们想要的email函数的模板函数名是`emailDeal`,它关联的Go函数名称是`EmailDealWith`,n那么我们可以通过下面的方式来注册这个函数 t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) `EmailDealWith`这个函数的参数和返回值定义如下: func EmailDealWith(args ...interface{}) string 我们来看下面的实现例子: 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) } 上面演示了如何自定义函数,其实,在模板包内部已经有内置的实现函数,下面代码截取自模板包里面 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来判断模板是否正确: 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语言中通过如下的语法来申明 {{define "子模板名称"}}内容{{end}} 通过如下方式来调用: {{template "子模板名称"}} 接下来我们演示如何使用嵌套模板,我们定义三个文件,`header.tmpl`、`content.tmpl`、`footer.tmpl`文件,里面的内容如下 //header.tmpl {{define "header"}} 演示信息 {{end}} //content.tmpl {{define "content"}} {{template "header"}}

演示嵌套

{{template "footer"}} {{end}} //footer.tmpl {{define "footer"}} {{end}} 演示代码如下: 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都是相对独立的,都能输出内容,contenrt中因为嵌套了header和footer的内容,就会同时输出三个的内容。但是当我们执行`s1.Execute`,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。 >同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析 ## 总结 通过上面对模板的详细介绍,我们了解了如何把动态数据与模板融合:如何输出循环数据、如何自定义函数、如何嵌套模板等等。通过模板技术的应用,我们可以完成MVC模式中V的处理,接下来的章节我们将介绍如何来处理M和C。 ## links * [目录]() * 上一节: [正则处理](<07.3.md>) * 下一节: [文件操作](<07.5.md>)