html/templateメモ
目次
html/templateの概要
htmlのテンプレートにこちらのプログラムで処理したデータを埋め込み返す。webサービスには必須の技術だが、それをgolangを実現するにはテンプレートエンジンであるhtml/templateを使う
正確には汎用テンプレートエンジンであるtext/template標準ライブラリと、さらにHTML専用のテンプレートエンジンであるhtml/templateライブラリの二種類があるが、ここでは後者を扱う
templateとは
-
template(テンプレート) とは予め用意されたhtmlのひな型
-
テンプレートエンジン とはテンプレートとデータ 1を組み込んで、クライアントに返す最終的なhtmlを生成するプログラム
- 通常、ハンドラがテンプレートエンジンを呼び出してデータをテンプレートに組み込み、できあがったHTMLをクライアントに返す
イメージ図12
基本文法
-
Go言語のテンプレートはテキストドキュメントであり(Webアプリの場合、通常はHTML
ファイル)、アクション と呼ばれる何らかのコマンドが埋め込まれたもの -
テンプレートはファイル全体であることもあれば、ファイルの一部しかテンプレートとして宣言されていないこともある
-
アクションが埋め込まれたテキスト、すなわちテンプレートがテンプレートエンジンによって解析(構文解析)され、「実行」されて新たなテキストが生成される
-
Go言語では任意のアクションは
{{...}}
で囲って記述する
プレーンなアクション
- 単純なテンプレートの例を示す
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Web Programming</title>
</head>
<body>
{{ . }}
</body>
</html>
-
{{ . }}
の中のドット「.
」 がアクションであり、テンプレートエンジンがテンプレートを実行したときに、「その部分をデータで置換える」という意味を持つコマンド -
次は上で定義したテンプレートから、最終的なhtmlを作成するgoスクリプトの方を見てみる
package main
import (
"net/http"
"html/template"
)
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("tmpl.html")
t.Execute(w, "hoge")
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/process", process)
server.ListenAndServe()
}
-
t, _ := template.ParseFiles("tmpl.html")
関数ParseFilesでテンプレートファイルtmpl.htmlを解析(コンパイル)する。
この関数は解析済みテンプレート(Template型)とエラーを返す -
因みに、errorではなくパニックに変換するためにMust()を合わせて利用することもある
var t = template.Must(template.ParseFiles("index.html"))
-
t.Execute(w, "hoge")
Executeメソッドを呼び出してデータ(この例では「hoge」)をコンパイル済みテンプレートに当てはめている。
生成されたhtmlはt.Execute()の第一引数で指定したio.Writerであるhttp.ResponseWriter
に書き込まれる。
Fprint系の関数とやっていることは似ている -
├── main.go
└── templ.html
以上二つのファイルを上のようなディレクトリ構成にしておくと、ビルド後に無事htmlファイルを返せるようになる
渡されるデータが構造体である場合
-
上の例で見たのは文字列"hoge"をデータとして渡す方法だった。「
.
」はそのまま"hoge"と置き換えられるので分かりやすい -
しかし一般的なのは構造体を渡すことで、そのメンバ変数がデータとして使われる。例えば先ほどのmain.goを次のように変えた場合だとどうするか
...
type Person struct{
Name:string
Sex:string
}
var person Person = {
Name:"HOGE"
Sex:"Male"
//personのメンバ変数NameとSexをテンプレートに埋め込みたい
}
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("tmpl.html")
t.Execute(w, person) //構造体personが渡されている
...
- 答えは次のようになる
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Web Programming</title>
</head>
<body>
{{ .Name }}
{{ .Sex }}
</body>
</html>
-
ドット「
.
」単体では構造体そのものとの置き換えを指すが、.Name
,.Sex
とすることで特定のメンバ変数との置き換えという意味になる -
構造体名.メンバ変数名
でメンバ変数の値にアクセスできるのと近いものを感じる
その他アクション
テンプレートに渡されたデータと置き換えるドット「.
」のコマンドは最も基本的なアクションだった。
しかし「.
」以外にもアクションは存在する。それらは単純な置き換えとは違う、よりプログラム寄り(条件文やループ)の処理を行う
条件アクション
- 条件アクションとは、多数のデータ評価値の中から引数の値に応じて1つを選択するものである。次の形式で表せる。(ただしargは任意の引数)
{{ if arg }}
コンテンツ
{{ end }}
- もう1つのタイプは次の形式。
{{ if arg }}
コンテンツ
{{ else }}
他のコンテンツ
{{ end }}
イテレータアクション
- イテレータアクションとは、配列やスライス、マップ、チャネルの要素ごとに反復処理を行うアクション。反復ループの内部では、ドットに配列やスライス、マップ、チャネルの要素が次々に設定される。
書式は次の通り
{{ range array }}
#arrayは大抵データとして渡されたスライスやマップのイメージ
{{ . }}
#range内でドットに要素が設定される
{{ end }}
- arrayがnilだった場合のフォールバックも存在する
代入アクション
代入アクションを使うと、そのアクションで囲まれたセクション内でargに指定した有効な値をドット「.
」の値として設定できる。書式は次の通り
{{ with arg }}
このセクション内でドットにargが設定される
{{ end }}
- argが空白だった場合のフォールバックも存在する
インクルードアクション
- テンプレートに別のテンプレートを差し込むことができる。書式は次の通り
{{ template "name" }}
- このアクションは次の章で詳しく説明する
テンプレートを分割したい
テンプレートAとテンプレートBがあったとき、bodyの要素は違ってもヘッダーは共通にしたい
というような、テンプレートを部品化して再利用する需要が出てくる。
この場合、共通部品であるヘッダーをテンプレートCとして作成してそれぞれのテンプレートに埋め込めばよい
静的なテンプレートの追加方法
-
インクルードアクション
{{ template "name" }}
を使えばテンプレートに別のテンプレートを差し込むことができる。 -
単純な例を見る。
2つのテンプレートファイル、t1.htmlとt2.htmlを作成する。この例ではテンプレートt1.htmlがt2.htmlをインクルードする。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=9">
<title>Go Web Programming</title>
</head>
<body>
<div> ここは、t1.html(インクルードの前)</div>
<div>t1.html内でのドットの値 - [{{ . }}]</div>
<hr/>
{{ template "t2.html" }}
<hr/>
<div> ここは、t1.html(インクルードの後)</div>
</body>
<div style="background-color: yellow;">
ここはt2.htmlです<br/>
</div>
- ハンドラは次のようにする
package main
import (
"html/template"
"net/http"
)
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("template/t1.html", "template/t2.html")
t.Execute(w, "こんにちは")
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/", process)
server.ListenAndServe()
}
-
t, _ := template.ParseFiles("template/t1.html", "template/t2.html")
に注目すると、
今までと違い、ParseFilesメソッドの引数が二つになっている。
ParseFilesメソッドは可変長引数関数であり、コンパイルするファイルを複数指定することができる -
ParseFiles関数の引数が可変長でも、返り値としてのコンパイル済みテンプレートは一つしかなく、それは最終的にレスポンスとして返すhtmlのテンプレートである。
その主要素はもちろん親の(ここではt1.html)テンプレートであり、親のテンプレートは必ず第一引数に指定しなくてはならない。逆にインクルードされる部品としてのテンプレートは第二引数以降に指定する -
ディレクトリ構成はこのようになっている
├── main.go
└── template
└── t2.html
└──t1.html -
http://127.0.0.1:8080/process
にアクセスすると以下のページが表示される
動的なテンプレートの追加方法
-
先ほどの例では部品テンプレート(t2.html)は静的だった
-
部品テンプレートにも動的なデータを含みたい場合は次のような書式になる
{{ template "name" arg}}
-
{{template "name"}}と違うのは後ろに引数argがくっついている点
こうすることで、インクルードされるテンプレートに渡すデータを指定できる -
さきほどの例を変更して、t2.htmlにもデータが渡るようにしてみる
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=9">
<title>Go Web Programming</title>
</head>
<body>
<div> ここは、t1.html(インクルードの前)</div>
<div>t1.html内でのドットの値 - [{{ . }}]</div>
<hr/>
{{ template "t2.html" . }}
#argにアクション「.」が指定されている
<hr/>
<div> ここは、t1.html(インクルードの後)</div>
</body>
<div style="background-color: yellow;">
ここはt2.htmlです<br/>
t2.htmlに渡された値 - [{{ . }}]
</div>
スコープの話
-
アクション「
.
」はt.Execute(w, テンプレートに渡すデータ)
で指定したデータと置き換えられること、そのデータが構造体であれば「.メンバ変数
」とするとそのメンバ変数と置き換えられることを既に伝えた -
その性質を利用して入れ子の構造体を自在に扱えたりもする
-
次のようなt1.html,t2.html,main.goを作成する
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=9">
<title>Go Web Programming</title>
</head>
<body>
<div> ここは、t1.html(インクルードの前)</div>
<div>性別 - [{{ .Sex }}]</div>
<hr/>
{{ template "t2.html" .Name }}
<hr/>
<div> ここは、t1.html(インクルードの後)</div>
</body>
<div style="background-color: yellow;">
ここはt2.htmlです
[{{ .Givenname }}]
[{{ .Familyname }}]
</div>
package main
import (
"html/template"
"net/http"
)
type Name struct {
Givenname string
Familyname string
}
type Person struct {
Name Name
Sex string
}
func process(w http.ResponseWriter, r *http.Request) {
person := Person{
Name: Name{
Givenname: "小町",
Familyname: "烏丸",
},
Sex: "Famale",
}
t, err := template.ParseFiles("template/t1.html", "template/t2.html")
if err != nil {
print(err)
}
t.Execute(w, person)
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/", process)
server.ListenAndServe()
}
-
main.goで入れ子になった構造体を定義し、親の構造体のメンバ変数はt1.htmlへ、入れ子の構造体のメンバ変数はt2.htmlに渡している
-
t1.htmlの
{{ template "t2.html" .Name }}
に注目すると、t2.htmlに渡されるデータが.Name
になっている -
こうすることでt2.htmlでは「
.
」のスコープはName構造体に限定されるため、t2.htmlでは入れ子になっている構造体のメンバ変数へのアクセスは[{{ .given_name }}]
とスマートになっている -
http://127.0.0.1:8080/process
にアクセスすると以下のページが表示される
余談
-
頭文字が大文字であれば外部から参照できる、というのがGo言語の特徴
-
先ほどの例ではアクション「
.
」を通じて構造体を渡しているが、その過程でこちらのパッケージを外部に参照させる処理が含まれているのか
main.go
で
type Name struct {
givenname string
familyname string
}
- のようにName構造体のメンバ変数の頭文字を小文字に変更すると、テンプレートに値が渡らない
参考
Go の html/template でヘッダーやフッター等の共通化を実現する方法
Goプログラミング実践入門 ―標準ライブラリでゼロからWebアプリを作る―
-
大抵はクライアントから渡される情報を指す。ユーザ名など。テンプレートは既に用意されたものであるため静的で、データはクライアントによって変わるものであるため動的(ただしこれから見ていく例では単純化するためサーバサイドで定義した文字列等を使っている) ↩
-
ハンドラがテンプレートエンジンを呼び出して、使用するテンプレートを通常はテンプレートのリストとして、動的なデータと一緒に渡します。テンプレートエンジンはHTMLを生成して、ResponseWriterにそれを書き込み、さらにResponseWriterはクライアントに送り返すHTTPレスポンスにそれを追加します。(イメージ図共に『Goプログラミング実践入門』より引用) ↩