第2の人生の構築ログ

自分の好きなことをやりつつ、インカムもしっかりと。実現していく過程での記録など。読書、IT系、旅行、お金に関係する話などの話題。

Google App Engine (GAE) を使って Go Web アプリ/静的 Web コンテンツを公開する (その2)

f:id:dr_taka_n:20190921101850p:plain

前回は簡単な Go Web アプリケーションを GAE の環境に配置(deploy)するところまで記載しました。

www.morelife.work

今回は、続けて Go アプリ以外の静的コンテンツの公開と Go のテンプレートの利用を行っていきます。

GAE で静的ファイルを公開する

GAE の場合は、リクエストされたパスによってどう処理するかのマッピングを app.yaml 構成ファイルで行います。この構成ファイルが肝になり、プラットフォームとコンテンツのマッピング、コンテンツのハンドラなども app.yaml で指定します。詳細は以下のページに記載があります。

cloud.google.com

トップページの / で終わっている URL の場合に index.html に割り当てる

ここではまずパスに / まで指定された URL の場合は、index.html の静的ファイルを参照するように指定します。

app.yaml

handlers:
  - url: /$
    static_files: public/index.html
    upload: public/index.html
  - url: /.*
    script: auto

handlers 配下の指定は上から順に評価され、最初にマッチした部分がリクエストの処理に適用されます。

最初の url: /$ の部分が正規表現で最後が / の URL を意味しており、その場合には、public ディレクトリ配下の index.htmlを参照しなさいと GAE に指示しています。また、アップロードする対象のファイルを指定しています。

次の url: /.* は全てのパターンにマッチしており、それまでのパターンにマッチしなかった場合には、ここでは Go の Web アプリを参照することになります。それまでにマッチしていない URL のパターンは全て Go で拾うよ、という意味です。ですので、最初に動作させていました https://xxx.appspot.com/hello はここで処理されます。

この時、ローカルのファイルの構成は以下の通りとなっています。

$ tree
.
├── app.yaml
├── helloworld.go
├── helloworld_test.go
├── index.yaml
└── public
    └── index.html

public/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>GAE App Static</title>
</head>
<body>
<h2>GAE App Static file</h2>
</body>
</html>

この状態でデプロイ(gcloud app deploy)し、https://xxx.appspot.com/ (xxxは適切なホスト名に置き換えてください)でアクセスします。

f:id:dr_taka_n:20190921091112p:plain:w400

ちゃんと静的ファイル index.html が表示されました。

HTML、画像ファイルなど、全ての静的ファイルも参照できるようにする

その他の HTML、画像ファイル、CSS、JS なども参照できるようにしてみましょう。

以下の設定を追加します。

app.yaml

diff --git a/app.yaml b/app.yaml
index 4688cd5..ba21b70 100644
--- a/app.yaml
+++ b/app.yaml
@@ -4,5 +4,8 @@ handlers:
   - url: /$
     static_files: public/index.html
     upload: public/index.html
+  - url: /(.+\.(html|css|js|gif|png|jpg))$
+    static_files: public/\1
+    upload: public/.+\.(html|css|js|gif|png|jpg)$
   - url: /.*
     script: auto

ファイルの構成は以下の通りです。

$ tree
.
├── app.yaml
├── helloworld.go
├── helloworld_test.go
├── index.yaml
└── public
    ├── about.html
    └── index.html

curlabout.html をつっついてみます。

$ curl -X GET http://xxx.appspot.com/about.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>About</title>
</head>
<body>
<h2>About</h2>
</body>
</html>

OK です。

画像ファイルも参照してみます。設定は既に app.yaml に追加していますので、コンテンツだけ用意します。

$ tree
.
├── app.yaml
├── helloworld.go
├── helloworld_test.go
├── index.yaml
└── public
    ├── about.html
    ├── assets
    │   └── images
    │       └── 652f4b3ae27d42ce5cd07a032165deb6.png
    ├── contact
    │   └── hi.html
    │   └── index.html
    ├── css
    └── index.html

public/contact/hi.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Hi</title>
</head>
<body>
<h2>Hi</h2>
<img src="/assets/images/652f4b3ae27d42ce5cd07a032165deb6.png">
</body>
</html>

http://xxx.appspot.com/contact/hi.html にアクセスします。

f:id:dr_taka_n:20190921094422p:plain:w400

OK です。

その他の / で終わっている URL の場合も index.html に割り当てる

先程、トップページ(ルート)だけ index.html を補完する設定を入れてろいましたので、それ以外の / で終わっている URL の場合にも index.html に割り当てる設定を入れておきます。

現状、http://xxx.appspot.com/ は以下のように補完されています。

f:id:dr_taka_n:20190921091112p:plain:w400

ただし、http://xxx.appspot.com/contact/ でアクセスしますと、KO です。

f:id:dr_taka_n:20190921095455p:plain:w400

当然、http://xxx.appspot.com/contact/index.html と直接指定すれば問題ありません。

f:id:dr_taka_n:20190921095639p:plain:w400

app.yaml に以下の設定を追加します。

app.yaml

diff --git a/app.yaml b/app.yaml
index ba21b70..12050f4 100644
--- a/app.yaml
+++ b/app.yaml
@@ -4,6 +4,9 @@ handlers:
   - url: /$
     static_files: public/index.html
     upload: public/index.html
+  - url: /(.*)/$
+    static_files: public/\1/index.html
+    upload: public/.*/index.html
   - url: /(.+\.(html|css|js|gif|png|jpg))$
     static_files: public/\1
     upload: public/.+\.(html|css|js|gif|png|jpg)$

これで http://xxx.appspot.com/contact/ で参照しても表示されるようになりました。

f:id:dr_taka_n:20190921095920p:plain:w400

Go のテンプレートを使う

Go のテンプレートを使います。Ruby でいうところの、erb、Java でいうところの jsp といったテンプレートエンジンです。

まずは、helloworld.go にテンプレートを使うための記述を追加しておきます。

helloworld.go

diff --git a/helloworld.go b/helloworld.go
index e2b71d7..4b87a93 100644
--- a/helloworld.go
+++ b/helloworld.go
@@ -1,10 +1,13 @@
 package main

 import (
+       "bytes"
        "fmt"
+       "html/template"
        "log"
        "net/http"
        "os"
+       "time"
 )

 func indexHandler(w http.ResponseWriter, r *http.Request) {
@@ -18,8 +21,38 @@ func indexHandler(w http.ResponseWriter, r *http.Request) {
        }
 }

+type tmplParams struct {
+       DaysOfWeek []string
+       Date time.Time
+}
+
+func formatDate(t time.Time) string {
+       return t.Format("2006-01-02")
+}
+
+func process(w http.ResponseWriter, r *http.Request) {
+       funcMap := template.FuncMap{ "fdate": formatDate }
+       t := template.New("show-days.html").Funcs(funcMap)
+       t, _ = t.ParseFiles("tmpl/show-days.html")
+       params := tmplParams{
+               DaysOfWeek: []string{"月", "火", "水", "木", "金", "土", "日"},
+               Date:       time.Now(),
+       }
+       buf := &bytes.Buffer{}
+       err := t.Execute(buf, params)
+       if err != nil {
+               fmt.Fprintf(os.Stderr, "%+v\n", err)
+               http.Error(w, "something wrong", http.StatusInternalServerError)
+       } else {
+               buf.WriteTo(w)
+       }
+}
+
 func main() {
        http.HandleFunc("/hello", indexHandler)
+       http.HandleFunc("/process", process)

        port := os.Getenv("PORT")
        if port == "" {

テンプレートは以下になります。

tmpl/show-days.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Template Example</title>
</head>
<body>
  <p>今日は、{{ .Date | fdate }} です。</p>
  <ul>
      {{ range .DaysOfWeek }}
          <li>{{ . }}</li>
      {{ end }}
  </ul>
</body>
</html>

ちなみに、もっとシンプルな例でよかったのですが、ここでは幾つか別の要素も試しています。

  • パラメータは構造体のデータとして受け渡しをしています。
    • 受け取る方(show-days.html)では、. 自体が構造体のデータを指しますので、. 以降にそのフィールドを指定することでアクセスできます。
    • 構造体では、フィールドは大文字で始めておかないとテンプレート側では見えなくなります。(あたり前と言えばあたり前なのですが、よくうっかり小文字のままとしてしまいますので、注意です。)
  • 呼び出し側で関数を定義し、それをテンプレート側で使えるようにしています。
    • formatDate がそれです。template.New().Funcs でマッピングしており、テンプレート側では、fdate という関数名で使えます。
  • テンプレートで使っている | (パイプ)はパイプラインと言われるもので、 Unix のパイプと考え方は同じです。
    • .Datetime.Time 型の現在日時が取り出され、それが formatDate(fdate) の引数として渡されています。
  • 通常、template.Execute は直接 http.ResponseWriter に write するように実装します(t.Execute(w, params))。ここで行っているバッファリングの処理は通常行う必要はありません。
    • ここでは、テンプレート側でのエラーも考慮し、一旦バッファリングした後に buf.WriteTohttp.ResponseWriter で write しています。こうしておかないと、テンプレート内でエラーが発生したとしても、t.Execute(w, params) は http のレスポンスを 200 で返して、処理できるところまで処理して返してしまいます。
    • 通常であれば、テンプレートにロジックは持ち込まず、テンプレートにはデータだけを渡すべきです。ですので、ここまでの処理は必要なく、バッファリングせずに直接 http.ResponseWriter で write すればよいと思います。(t.Execute(w, params))

現在のファイル構成は以下の通りです。

$ tree
.
├── app.yaml
├── helloworld.go
├── helloworld_test.go
├── index.yaml
├── public
│   ├── about.html
│   ├── assets
│   │   └── images
│   │       └── 652f4b3ae27d42ce5cd07a032165deb6.png
│   ├── contact
│   │   ├── hi.html
│   │   └── index.html
│   ├── css
│   └── index.html
└── tmpl
    └── show-days.html

http://xxx.appspot.com/process にアクセスします。

f:id:dr_taka_n:20190921101208p:plain:w400

OK ですね。

だいぶ長くなってしまいましたが、「Google App Engine (GAE) を使って Go Web アプリ/静的 Web コンテンツを公開する」は以上になります。