久しぶりに Go 言語のテンプレート(html/template)を使用し、テンプレートの HTML から外部ファイルを読み込もうとしていました。そこで遭遇した以下のエラー。
原因がわかってしまえば、そりゃそうでしょう、となるのですが、以下のエラーだけ見ると、何が悪いのかさっぱりで、暫しキョトンとしてしまいました。
Uncaught SyntaxError: Unexpected token '<'
よくわからないところに×点マークです。
文字通り読めば、何かタグの閉め忘れのような 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 ファイルも読み込めていることがわかります。