write ahead log

ロールフォワード用

golangでテンプレートエンジンを使う(その2)

以前, templateパッケージを利用する方法を調べたけど, 構成的な意味でもうちょっと実用的な例が欲しいので書き残しました.

コードはgithubに置いておきます.

github.com

やりたいこと

  • どのページでも使うlayoutをまとめたい
  • 部品的なHTMLを使いまわしたい

作るもののイメージ

ブログシステムを考えてみます.

一般に2種類のページ枠があるでしょう.

  • 一般公開ページ
  • 管理者ページ

もう少し具体的に以下のページを考えます.

  • 一般公開ページ
    • 個別記事の編集
    • 個別記事の参照
  • 管理者ページ
    • 個別記事の参照

一般公開ページと管理者ページではメニューが異なるという事にします.

挙げるとキリがないのでこの辺で.

コード

長いですが, メモなのでそれぞれ全部載せます.

テンプレートは全体的に適当です.

ディレクトリ構成
$ tree .
.
├── main.go
└── templates
    ├── admin_menu.html    # 管理者用メニュー(define "menu")
    ├── entry_editor.html  # 記事エディタ(define "contents")
    ├── entry_view.html    # 記事ビュワー(define "contents")
    ├── layout.html        # 共通レイアウト(define "layout")
    └── public_menu.html   # 公開用メニュー(define "menu")

1 directory, 6 files
テンプレートコード
共通レイアウト

layout.html

ここでdefine “layout"としています.

{{define "layout"}}
<!DOCTYPE html>
<html>
    <head>
       <title>{{.Title}}</title>
       <meta charset="utf-8">
   </head>
    <body>
        <div class="menu">
            {{template "menu" .}}
        </div>

        <div class="container">
            {{template "contents" .}}
        </div>
    </body>
</html>
{{end}}
管理者用メニュー

admin_menu.html

ここと公開用メニューでdefine “menu"としています.

{{define "menu"}}
<!-- Admin menu -->
<ul>
    <li><a href="">New Entry</a></li>
    <li><a href="">Edit Entry</a></li>
</ul>
{{end}}
公開用メニュー

public_menu.html

{{define "menu"}}
<!-- Public menu -->
<ul>
    <li><a href="">About me</a></li>
    <li><a href="">Latest Entry</a></li>
</ul>
{{end}}
記事エディタ

以下2つはdefine “contents"です.

entry_editor.html

{{define "contents"}}
<textarea>{{.Entry.Text}}</textarea>
{{end}}
記事ビュワー

entry_view.html

{{define "contents"}}
<pre>
{{.Entry.Text}}
</pre>
{{end}}
goのコード
package main

import (
    "fmt"
    "html/template"
    "net/http"
)

// RenderData はテンプレートへ引き渡す表示用のデータです
type RenderData struct {
    Title string
    Entry *Entry
}

// Entry は1記事に相当します
type Entry struct {
    Text string
}

var (
    // サンプルデータ
    sampleEntry *Entry      = &Entry{Text: "This is a sample entry"}
    sampleData  *RenderData = &RenderData{Title: "sample", Entry: sampleEntry}
    // テンプレートディレクトリ
    templatesDir string = "templates"
)

func main() {
    // admin:編集ページ
    http.HandleFunc("/admin/edit/someEntry", editAdminSomeEntry)
    // admin:参照ページ
    http.HandleFunc("/admin/refer/someEntry", referAdminSomeEntry)
    // public:参照ページ
    http.HandleFunc("/refer/someEntry", referSomeEntry)

    http.ListenAndServe(":8080", nil)
}

// admin用:編集ページ
func editAdminSomeEntry(w http.ResponseWriter, r *http.Request) {
    execTemplate(w, sampleData, "layout", "admin_menu", "entry_editor")
}

// admin用:参照ページ
func referAdminSomeEntry(w http.ResponseWriter, r *http.Request) {
    execTemplate(w, sampleData, "layout", "admin_menu", "entry_view")
}

// public用:参照ページ
func referSomeEntry(w http.ResponseWriter, r *http.Request) {
    execTemplate(w, sampleData, "layout", "public_menu", "entry_view")
}

// execTemplate はfilesのテンプレートからHTMLを構築して,
// wに対して書き込みます.
// HTML構築の際にはdataを利用します.
// filesで指定するテンプレートには必ず{{define "layout"}}された
// ものを1つだけ含む必要があります.
func execTemplate(w http.ResponseWriter, data interface{}, files ...string) {
    // 渡された引数からテンプレートパスの集合を作る
    var pathes []string
    for _, f := range files {
        p := fmt.Sprintf("%s/%s.html", templatesDir, f)
        pathes = append(pathes, p)
    }
    //上記で作ったパスの一覧を使ってテンプレートを作る
    template := template.Must(template.ParseFiles(pathes...))
    // layoutが必ず基点になるという事にする
    template.ExecuteTemplate(w, "layout", data)
}

テンプレートをキャッシュするか

init時に全てのテンプレートをParseしてmapなどに保存しておくことが出来るはずです.

今回のexecTemplate関数をmapからテンプレートを取得し, パッケージ標準のExecuteTemplateを呼び出す仕組みに変えるだけで高速化もできそうです.
(どの程度早くなるかはわかりませんが)