はじめに
Goの標準で用意されているテンプレートエンジンを使って,HTMLページを表示させてみた.
環境
- macOS BigSur
- Go 1.17.5 darwin/arm64
テンプレートエンジンとは
本題に入る前にテンプレートエンジンについて説明する.
テンプレートエンジンとは「テンプレート」と呼ばれる雛形に「データ」を組み込んで最終的なHTMLを生成するもの.Web用のテンプレートエンジンは多くの本格的なフレームワークに含まれている.Goも例外ではなく,数種のテンプレートエンジンが組み込まれている.通常,ハンドラがテンプレートエンジンを呼び出してデータをテンプレートに組み込み,出来上がったHTMLをクライアントに返す.簡単なイメージ図はこんな感じ.
テンプレートエンジンには標準規格がないが,大まかに2つの種類がある.
- ロジックなしのテンプレートエンジン
- ロジック埋め込みテンプレートエンジン
ロジックなしのテンプレートエンジンは全くロジックの処理は行わず,文字列の置換だけを行う.表現とロジックを明確に分離することで,ロジックはハンドラによってのみ処理される.一方ロジック埋め込みテンプレートエンジンは,プログラミング言語のコードをテンプレートに埋め込み,実行時にテンプレートエンジンによって処理を行う.
Goのテンプレートエンジン
Go言語のテンプレートエンジンは,ロジックなし型とロジック埋め込み型のハイブリッドである.下図はWebアプリにおけるGo言語のテンプレートエンジンの動作を表している.
マルチプレクサはリクエストを受け付け、URLに対応するハンドラへ転送する(他の言語ではルーティングと呼んだりする).ハンドラがテンプレートエンジンを呼び出して,使用するテンプレートを動的なデータと一緒に渡す.テンプレートエンジンはHTMLを生成して,ResponseWriterに生成したHTMLを書き込み,HTTPレスポンスにそれを追加する.
Go言語には標準ライブラリとして,text/template と html/template が用意されている.
それでは実践
1. Hello World
まずは一番シンプルな,"Hello World"をテンプレートエンジンを使って表示させるものから始める.以下のようなテンプレートファイルtemplate.html
を用意する.body
内の{{ . }}
に注目.このドット「.」はアクションと呼び,テンプレートエンジンがテンプレートを実行したときに,この部分にデータを埋め込む.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Hello Worldを表示させる</title>
</head>
<body>
<h1>{{ . }}</h1>
</body>
</html>
次にハンドラ関数からテンプレートエンジンを呼び出す処理を記述する.server.go
内にhandler
というハンドラ関数が定義されており,これがテンプレートエンジンを呼び出している.まず,ParseFiles
関数でテンプレートファイルを解析し,Execute
メソッドを呼び出してデータ(今回は"Hello World!")をテンプレートのアクションに埋め込んでいる.
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("template.html")
t.Execute(w, "Hello World!")
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/", handler)
server.ListenAndServe()
}
最後にserver.go
を実行して,http://localhost:8080/ に接続すると...
"Hello World!"が表示されていることが確認できる.
2. 条件アクション
次に重要なアクションの一つである条件アクションを実装してみる.条件アクションとは,テンプレートに渡されたデータをif
else
などを用いて条件分岐させるアクションである.以下のようなtemplate.html
を用意する.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>条件アクション</title>
</head>
<body>
{{ if .key1 }}
<h1>False</h1>
{{ else if .key2 }}
<h1>True</h1>
{{ else }}
<h1>False</h1>
{{ end }}
</body>
</html>
ここで,マップが
map[string]string{"key1":"val1", "key2":"val2"}
のように定義されているとき,テンプレート内では
<p>{{ .key1 }}</p>
のように指定することで,対応するバリューを表示させることができる.
<p> val1 </p>
ハンドラ関数を次のように定義する.今回はExcute
メソッドでbool
をセットしたマップをテンプレートのアクションに埋め込んでいる.
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("template.html")
t.Execute(w, map[string]bool{
"key1": false,
"key2": true,
})
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/", handler)
server.ListenAndServe()
}
最後にserver.go
を実行して,http://localhost:8080/ に接続すると...
3. イテレータアクション
イテレータアクションとは,配列やスライス,マップの要素ごとに反復処理を行うアクションである.次のように記述する.
{{ range array }}
ドットには要素が入る {{ . }}
{{ end }}
次のようなテンプレートファイルとハンドラ関数を用意する.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>イテレータアクション</title>
</head>
<body>
<ul>
{{ range . }}
<li>{{ . }}</li>
{{ end }}
</ul>
</body>
</html>
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("template.html")
daysOfWeek := []string{"月", "火", "水", "木", "金", "土", "日"}
t.Execute(w, daysOfWeek)
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/", handler)
server.ListenAndServe()
}
ここでは曜日名を格納した文字列のスライスをテンプレートエンジンに渡し,実行時に {{ range . }}
にあるドッドに渡され,スライスの各要素が順に処理される.最後にserver.go
を実行して,http://localhost:8080/ に接続すると...
4. 代入アクション
代入アクションは,そのアクションで囲まれたセクション内で指定の値をドットに代入できるものである.次のように記述する.
{{ with hoge }}
ドットに {{ . }} が設定される
{{ end }}
//実行結果
ドットに hoge が設定される
以下のようなテンプレートファイルとハンドラ関数を用意する.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>代入アクション</title>
</head>
<body>
<div>ドットの値は{{ . }}です</div>
<div>
{{ with "go"}}
ドットは今、{{ . }}に設定されています
{{ end }}
</div>
<div>ドットの値は{{ . }}に戻りました</div>
</body>
</html>
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("template.html")
t.Execute(w, "hello")
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/", handler)
server.ListenAndServe()
}
最後にserver.go
を実行して,http://localhost:8080/ に接続すると...
5. インクルードアクション
インクルードアクションとは,テンプレートの中に別のテンプレートを差し込むアクションのこと.これによってテンプレートを入れ子にすることができる.インクルードアクションは{{ template "name" . }}
のように記述する.templateの引数は,1つ目はインクルードするテンプレート名,2つ目はテンプレートに渡すデータを指定する.次のようなテンプレートファイルtemplate1.html
とtemplate2.html
を用意する.template1.html
はtemplate2.html
をインクルードする.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=9">
<title>インクルードアクション</title>
</head>
<body>
<div> ここは、template1.html(インクルードの前)</div>
<div>template1.html内でのドットの値 - [{{ . }}]</div>
<hr/>
{{ template "template2.html" . }}
<hr/>
<div> ここは、template1.html(インクルードの後)</div>
</body>
</html>
<div style="background-color: blue;">
ここはtemplate2.htmlです<br/>
template2.html内でのドットの値 - [{{ . }}]
</div>
テンプレートを作成するときに名前を設定していないため,テンプレート名にファイル名が使用されていることがわかる.テンプレート名を定義するには次のように記述する.
{{ define "main" }}
<div style="background-color: blue;">
ここはtemplate2.htmlです<br/>
template2.html内でのドットの値 - [{{ . }}]
</div>
{{ end }}
こうすることでテンプレート名はmain
と定義された.
そして次のようなハンドラ関数を用意して実行すると
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("t1.html", "t2.html")
t.Execute(w, "Hello World!")
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/", handler)
server.ListenAndServe()
}