Golangでtemplateパッケージを用いることで非常に簡単にhtmlファイルを表示させることができます。htmlファイルを表示できるとなるとCSSで画面をきれいにしたり、JavaScriptで動きをつけたくなると思います。(私は思いました)今回は.cssファイルや.jsファイルをtemplateで表示されたhtmlファイル内に読み込む方法について紹介します。tamlplateパッケージについては「templateを使ってhtmlファイルを画面に表示する」を参考にしてください!
今回実装する画面
今回は気温のデータがChart.jsできれいに表示される以下のような画面を実装します。
(画面の動き)
非常にきれいな画面ですね。このきれいさはほとんどChart.jsの力を借りています。
前提知識
本記事は以下のような知識がある方におすすめです。(知らなくてもなるべくわかるようにして参考リンクも多めにしてあります)
- Golangのtemplateパッケージの扱いがわかる
- JavaScriptの基本的な扱い(要素取得がわかれば全然大丈夫です!)
- Chart.jsの使い方(全然知らなくても行ける、簡単!)
tamlplateパッケージについては「templateを使ってhtmlファイルを画面に表示する」を参考にしてみてください!(これの続きと言っても過言ではないです)。また、JavaScriptについてもCode Databaseでは多数の記事を扱っていますので参考にしてみてください!さらにさらに、Chart.jsについても「Chart.jsのインストールと基本的な使い方について解説」というバッチリな記事があるので参考にしてみてください!
実装の手順
実装は以下の手順でしていきたいと思います。
- サーバーを立ててトップページを表示する
- 表示するデータを作成する
- データをデータベースページで表示する
- staticフォルダを作成してmain.jsを用意する
- main.js内でChart.jsを用いて表を表示する
- data.htmlファイル内でmain.jsを読み込む
また、実装のファイル構造は以下のようにします。
golang_static/ ├ static/ │ └ main.js ├ index.html ├ data.html └ main.go
さらに、今回作成する画面のソースコードはcode-databaseのgithubリポジトリに上げているので、手元で確認したい方はぜひ活用してみてください。先に全体のコードを確認しておくのも手かもしれません。
実装してみる
それでは早速実装していきましょう。
サーバーを立ててトップページを表示する
まずはサーバーを立ち上げてトップページを表示します。
(index.html)
<!DOCTYPE html> <html lang='en'> <head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <title>golang_template</title> </head> <body> <h1>トップページ</h1> <a href='/data/'>データを見る</a> </body> </html>
(mian.go)
package main import ( "net/http" "text/template" ) func mainHandler(w http.ResponseWriter, r *http.Request) { t, err := template.ParseFiles("index.html") if err != nil { panic(err.Error()) } if err := t.Execute(w, nil); err != nil { panic(err.Error()) } } func main() { http.HandleFunc("/", mainHandler) http.ListenAndServe(":8000", nil) }
この辺りはさらっと解説して進みます。"/"のpathに対してtemplateで表示されたindex.htmlファイルが表示されるようにし、index.htmlファイル内では"/data/"パスへのアンカーを通しておきます。難しいなと感じたら「templateを使ってhtmlファイルを画面に表示する」の記事を参考にしましょう。
表示するデータを作成して表示する
次に表示する気温のデータを作成します。
表にするので、表のタイトル(label)と実際のデータが必要になります。したがって以下のような構造のデータにしましょう。
//TemperatureDataElem 気温データの一つのデータセット type TemperatureDataElem struct { Label string Data []float64 }
そして、データ(tempratureData)を実際に作成します。
func dataHandler(w http.ResponseWriter, r *http.Request) { var temperatureData []TemperatureDataElem temperatureData = append(temperatureData, TemperatureDataElem{ Label: "沖縄県", Data: []float64{17.0, 17.1, 18.9, 21.4, 24.0, 26.8, 28.9, 28.7, 27.6, 25.2, 22.1, 18.7}, }) temperatureData = append(temperatureData, TemperatureDataElem{ Label: "東京都", Data: []float64{5.2, 5.7, 8.7, 13.9, 18.2, 21.4, 25.0, 26.4, 22.8, 17.5, 12.1, 7.6}, }) t, err := template.ParseFiles("data.html") if err != nil { panic(err.Error()) } if err := t.Execute(w, temperatureData); err != nil { panic(err.Error()) } } func main() { http.HandleFunc("/", mainHandler) http.HandleFunc("/data/", dataHandler) http.ListenAndServe(":8000", nil) }
"/data/"パスに対するハンドラ関数の準備をして、dataHandler()内部には先ほど定義したもののデータセットをappend関数を用いて用意しておきましょう。
そして、templateパッケージでdata.htmlに先ほど作成したtempratureDataを組み込んでおきます。
ここまでできたらdata.htmlを操作して表の表示を実装しましょう。
<body> <h1>データページ</h1> <a href='/'>トップへ戻る</a> <table> {{ range . }} <tr> <th>{{ .Label }}の気温</th> </tr> <tr> {{ range .Data }} <td style="display: inline;">{{ . }}度</td> {{ end }} </tr> {{ end }} </table> </body>
range関数を用いてtempratureDataをリストレンダリングします。range関数などの使い方に関しても難しいなと感じたら「templateを使ってhtmlファイルを画面に表示する」の記事を参考にしましょう。
ここまでできたら一度サーバーを立ち上げて確認してみましょう。
go run main.go
以下のように表示できていれば大丈夫です。
staticフォルダを作成してmain.jsを用意する
それでは本題のjavaScriptファイルを作成して、Chart.jsを用いた表の実装に移っていきます。その前に、まずはファイルを作成する準備をしていきましょう。staticフォルダを作成して、その内部にmain.jsを作成します。したがってファイル構成は以下のようになります。
golang_static/ ├ static/ │ └ main.js ├ index.html ├ data.html └ main.go
今回はCSSファイルなどを作成してはいませんが、このような静的ファイルを作成する際には静的ファイルようのフォルダを作ってそこにまとめておくと良いです。(理由は後ほど)
main.js内でChart.jsを用いて表を表示する
次に先ほど作成したmain.js内にChart.jsを用いて表を作成していきましょう。今回はChart.jsをCDNで読み込みます。したがって、data.htmlのheadタグ内に以下の記述をする必要があります。
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
goからJavaScriptへの値渡しはテンプレートで表示されたhtmlファイルを経由して読み込みます。したがって、値渡し用のinput[type=hidden]タグをdeta.html内部に記載しましょう。
さらにChart.jsで表を表示するcanvas要素を定義しておきます(Chart.jsではcanvas要素に表を出力します)。ここまでで、base.htmlのbodyは以下のようになります。
<body> <h1>データページ</h1> <a href='/'>トップへ戻る</a> <table> {{ range . }} <tr> <th>{{ .Label }}の気温</th> <input type='hidden' id="label_value" value="{{ .Label }}"> </tr> <tr> <input type='hidden' id="data_value" value="{{ .Data }}"> {{ range .Data }} <td style="display: inline;">{{ . }}度</td> {{ end }} </tr> {{ end }} </table> <div style="width: 50%;"> <canvas id="temperatureChart"></canvas> </div> <script src="/static/main.js"></script> </body>
main.jsの記述は次のようになります。
const temperatureChart = document.getElementById('temperatureChart') const label_values = document.querySelectorAll('#label_value') const data_values = document.querySelectorAll('#data_value') const datasets = [] for (let i = 0; i < label_values.length; i++){ data = data_values[i].value.replace(/ /g, ',') datasets[i] = { "label": label_values[i].value, "data": JSON.parse(data), "borderColor": `rgb(${(i + 1) * 50}, ${(i + 1) * 100}, ${(i + 1) * 100})`, "fill": false, "lineTension": 0.1 } } new Chart(temperatureChart, { "type": "line", "data": { "labels": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], "datasets": datasets }, "options": { scales: { yAxes: [{ ticks: { suggestedMax: 40, suggestedMin: -20, } }] }, } })
まずはJavaScriptで表を表示するcanvas要素、値渡しのためのinput[type=hidden]要素をそれぞれ取得します。
次に、JavaScript内でのデータの変数である定数datasetsを用意しておきます。この後はdatasetsを作成して(for文の箇所)、作成したdatasetsを元にChart.jsを用いて表を出力する(new Chart()の箇所)という手順です。
どちらもChart.jsの予備知識がいるので「気温の線グラフを作成する」を参考にしてみてください。
datasetsの作成の際にreplace()とJSON.parse()メソッドを用いていますが、これはvalueで取得したgoの変数がカンマなしの文字列であるためです。replace()メソッドでスペースをカンマに置き換え、JSON.parse()メソッドでこれを配列形式に変換しています。
- replace()メソッドについては「文字列を置換する方法(replace)について解説」を参考にしてみてください
- JSON.parse()メソッドについては「JSON.parse() - JavaScript | MDN」を参考にしてみてください
Chartインスタンスを作成する際のオプションのうち、dataオプション内のdatasetsオプションをfor文で動的に生成して変数として挿入しています。非常にシンプルです。
data.htmlファイル内でmain.jsを読み込む
さぁ、main.jsを作成できたので、data.htmlで読み込みましょう!しかし、どのようなパス指定をすればいいのか最初はわからない。。(自分がそうでした)。試しに相対パスで指定してみましょう(まぁ当然失敗するのでしょう)。
<body> <!-- 省略 --> <div style="width: 50%;"> <canvas id="temperatureChart"></canvas> </div> <script src="./static/main.js"></script> </body>
(ターミナル)
go run main.go
結果はこうなる。
案の定表示されない。そしてJavaScriptでこういう謎のエラーを吐かれる。
main.js:1 Uncaught SyntaxError: Unexpected token '<'
これはよくよくみるとhtmlファイルがmain.jsをして読み込まれている事によるエラーですね。。
ブラウザ上ではdata.htmlファイルはドメイン(この場合はlocalhost:8000)/data/
に配置されるのでここにファイルをおく記述をgoでしていないため、表示されないということになる。そのため、それをgoで書いてあげる必要がある。ただし、全てのJavaScriptファイルなどの静的ファイルにそんなことをしていたら日が暮れるので、staticフォルダというものを作りここに静的ファイルをおいてサーバーに置いてあげるということをする!
まずはGoで"/static/"というパスを要求されたらどこのファイルを参照させるかを記述する。
(mian.go)
package main import ( "net/http" "os" "text/template" ) // 省略 func main() { dir, _ := os.Getwd() //追記 http.HandleFunc("/", mainHandler) http.HandleFunc("/data/", dataHandler) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(dir+"/static/")))) //追記 http.ListenAndServe(":8000", nil) }
まず、os.Getwd()でmain.goがあるディレクトリを取得しておきます。
そして、"/static/"というパスを要求されたらここのフォルダを参照して!という意味のコードを書きます。ここで難しい点が
- hoge/fuga/static/というリクエストにもhoge/static/というリクエストを同一のものとして扱う必要がある
という点です。これらをhttp.StripPrefix()メソッドで解決します。使用するメソッドの役割は以下の通りです。
メソッド | 説明 |
http.FileServer(http.Dir("path")) | pathにあるディレクトリをhandlerとして返す |
http.StripPrefix("path", handler) | pathより前を取り除いたpathを作成しhandlerに渡す |
http.Handle("path", handler) | pathという要求がきたらhandlerを返す |
http.StripPrefix()メソッドであるpathから前を切り取る。つまり、localhost:8000/hoge/fuga/static/もlocalhost:8000/hoge/static/も同じリクエストとして扱ってくれます。
data.htmlの方ではmain.jsを以下のように指定します。
<body> // 省略 <div style="width: 50%;"> <canvas id="temperatureChart"></canvas> </div> <script src="/static/main.js"></script> </body>
これでJavaScriptが適用されて美しい雨温図が作成されているはずです!お疲れ様でした!
実装に使用したコード
今回使用したコード全体を紹介します。
(フォルダ階層)
golang_static/ ├ static/ │ └ main.js ├ index.html ├ data.html └ main.go
(main.go)
package main import ( "log" "net/http" "os" "text/template" ) //TemperatureDataElem 気温データの一つのデータセット type TemperatureDataElem struct { Label string Data []float64 } func mainHandler(w http.ResponseWriter, r *http.Request) { t, err := template.ParseFiles("index.html") if err != nil { panic(err.Error()) } if err := t.Execute(w, nil); err != nil { panic(err.Error()) } } func dataHandler(w http.ResponseWriter, r *http.Request) { var temperatureData []TemperatureDataElem temperatureData = append(temperatureData, TemperatureDataElem{ Label: "沖縄県", Data: []float64{17.0, 17.1, 18.9, 21.4, 24.0, 26.8, 28.9, 28.7, 27.6, 25.2, 22.1, 18.7}, }) temperatureData = append(temperatureData, TemperatureDataElem{ Label: "東京都", Data: []float64{5.2, 5.7, 8.7, 13.9, 18.2, 21.4, 25.0, 26.4, 22.8, 17.5, 12.1, 7.6}, }) t, err := template.ParseFiles("data.html") if err != nil { panic(err.Error()) } if err := t.Execute(w, temperatureData); err != nil { panic(err.Error()) } } func main() { dir, _ := os.Getwd() log.Print(http.Dir(dir + "/static/")) http.HandleFunc("/", mainHandler) http.HandleFunc("/data/", dataHandler) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(dir+"/static/")))) http.ListenAndServe(":8000", nil) }
(index.html)
<!DOCTYPE html> <html lang='en'> <head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <title>golang_template</title> </head> <body> <h1>トップページ</h1> <a href='/data/'>データを見る</a> </body> </html>
(data.html)
<!DOCTYPE html> <html lang='en'> <head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script> <title>golang_template</title> </head> <body> <h1>データページ</h1> <a href='/'>トップへ戻る</a> <table> {{ range . }} <tr> <th>{{ .Label }}の気温</th> <input type='hidden' id="label_value" value="{{ .Label }}"> </tr> <tr> <input type='hidden' id="data_value" value="{{ .Data }}"> {{ range .Data }} <td style="display: inline;">{{ . }}度</td> {{ end }} </tr> {{ end }} </table> <div style="width: 50%;"> <canvas id="temperatureChart"></canvas> </div> <script src="/static/main.js"></script> </body> </html>
(main.js)
const temperatureChart = document.getElementById('temperatureChart') const label_values = document.querySelectorAll('#label_value') const data_values = document.querySelectorAll('#data_value') const datasets = [] for (let i = 0; i < label_values.length; i++){ data = data_values[i].value.replace(/ /g, ',') datasets[i] = { "label": label_values[i].value, "data": JSON.parse(data), "borderColor": `rgb(${(i + 1) * 50}, ${(i + 1) * 100}, ${(i + 1) * 100})`, "fill": false, "lineTension": 0.1 } } new Chart(temperatureChart, { "type": "line", "data": { "labels": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"], "datasets": datasets }, "options": { scales: { yAxes: [{ ticks: { suggestedMax: 40, suggestedMin: -20, } }] }, } })
この記事のまとめ
本記事ではGolangで静的ファイルを表示する方法について紹介しました。最後の内容の要点をまとめておきます。
- Golangでは静的ファイルようのルートハンドリングをして静的ファイルの読み込みを行う
- http.StripPrefix()メソッドでパスの処理をしてからファイルを返すようにする
- htmlファイル経由でgoファイルからjsファイルへの値渡しをするのはダサい
皆さんもぜひGoでブログなどを作る際は静的ファイルで見た目をきれいにしましょう!