第2の人生の構築ログ

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

Go 言語の html/template で、意外と分かり難い"Uncaught SyntaxError: Unexpected token '<'" エラーの原因

久しぶりに Go 言語のテンプレート(html/template)を使用し、テンプレートの HTML から外部ファイルを読み込もうとしていました。そこで遭遇した以下のエラー。

原因がわかってしまえば、そうだよね、と納得なのですが、以下のエラーだけ見ると、何が悪いのかさっぱりで、暫しキョトンとしてしまいました。

Uncaught SyntaxError: Unexpected token '<' f:id:dr_taka_n:20210903214623p:plain

よくわからないところに×点マークです。 f:id:dr_taka_n:20210903214620p:plain

文字通り読めば、何かタグの閉め忘れのような HTML の構文ミスをおかしているのではないかと考えます。しかし、何度見てもそこに問題はありません。

この時の一連のソースコードは以下の通りとなります。

$ tree
.
├── main.go
├── main.js
└── templates
    └── index.html

main.go

package main

import (
    "fmt"
    "html/template"
    "log"
    "net/http"
    "path/filepath"
    "sync"
)

const PORT = 8080

type templateHandler struct {
    once     sync.Once
    filename string
    tmpl     *template.Template
}

func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    t.once.Do(func() {
        t.tmpl = template.Must(template.ParseFiles(filepath.Join("templates", t.filename)))
    })
    if err := t.tmpl.Execute(w, nil); err != nil {
        log.Fatal("template ServeHTTP:", err)
    }
}

func main() {
    http.Handle("/", &templateHandler{filename: "index.html"})

    if err := http.ListenAndServe(fmt.Sprintf(":%d", PORT), nil); err != nil {
        log.Fatal("ListenAndServe:", err)
    }
    log.Printf("Listening port %d ...", PORT)
}

ちなみに、本題とは関係ないのですが、リクエスト毎に処理を行うメソッドにて、テンプレートのコンパイルは当然ながら都度行う必要はありません。ですので、このメソッドでコンパイルを行うとした場合には、テンプレートのコンパイルは、syncOnceを使って一度だけ実行することを担保しています。

template/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>テンプレートサンプル</title>
</head>
<body>
  <p>こんにちは!</p>
  <script type="text/javascript" src="main.js"></script>
</body>
</html>

main.js

document.addEventListener("DOMContentLoaded", () => {
  console.log("DOMの読み込み完了!");
});

おそらく、Go のテンプレートを扱い慣れた人であればすぐに理由に気付いてしまうのではないでしょうか。

そう、テンプレートファイル(index.html)で読み込んでいる外部ファイル(main.js)がちゃんと読み込めていない、つまりは、Go のサーバから配信できていないことが問題になります。いやぁ、これはエラーの文言と睨めっこしてもわかりません。。

JavaScript のファイルも Go のサーバから配信してもらうようにします。

当然、通常の Static ファイルを公開する方法で大丈夫です。

まずは、静的コンテンツをおく場所を用意しておきます。

$ tree
.
├── main.go
├── public
│   └── js
│       └── main.js
└── templates
    └── index.html

public ディレクトリとその以下にjsディレクトリを作成し、元々用意していたmain.jsを配置します。

静的ファイルを配信するためのハンドラを用意します。

main.go のハンドラを設定している箇所に以下を追加します。

func main() {
    http.Handle("/", &templateHandler{filename: "index.html"})
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public"))))  // 追加

http.FileServer はファイルを配信することができる http.Handler interface を実装した関数です。起点となるディレクトリを引数に渡しています。 http.StripPrefix関数を一枚挟んでいますが、これは、例えば、/static/js/main.js というパスでリクエストした場合に、/static/を strip (削除) し、起点として指定したディレクトリ(ここでは public)配下のパスを参照するという意味になります。

Go の実装は以上で、HTML の参照パスを変更しておきます。

templates/index.html

 </head>
 <body>
   <p>こんにちは!</p>
-  <script type="text/javascript" src="main.js"></script>
+  <script type="text/javascript" src="/static/js/main.js"></script>
 </body>
 </html>

対応は以上です。これで再度リクエストしてみると、エラーも消え、しっかり JavaScript ファイルも読み込めていることがわかります。

f:id:dr_taka_n:20210903214618p:plain