format_list_bulleted
【Golang/Vue.js】GolangとVue.jsとChart.jsで小さなSPAアプリを作る方法について解説
最終更新日時:2020-05-09 11:21:54



Chart.jsなどを作って美しいUIのWebアプリを作りたい。と多くの人は思うでしょう。そんな思いが強い方にはSPA(Single Page Application)でWebアプリを作るのがおすすめです。本記事ではSPAとは何か?から始まり、実際にすごく小さなSPAアプリをゼロからハンズオンで作っていきたいと思います。本記事は「Golangで静的ファイル(JavaScript/CSS)を読み込む方法」の続きという位置づけなので、参考にしてみてもいいかもしれません。(内容は繋がっていないので読んでいなくても大丈夫です)

SPAとは何か?

MPAとSPA

そもそもSPAとはなんなのでしょうか、それにはMPA(Multiple Page Application)と比較するとわかりやすいです。

RoRやDjangoのviews、Golangのtemplateパッケージを用いたページの表示など、Webサーバーでページの表示を行うことがMPAです。MPAではクライアントのリクエストに対してページが書かれたhtmlファイルが返されます。

(MPAは以下のようなイメージです *一例です)

mpaの説明画像

一方でJavaScriptでもDOM操作をすることができるのでWebページを作成することができます。そこで、SPAではWebサーバーからデータだけをクライアントに渡して、クライアントのJavaScriptファイルでデータを見やすい形に整えて表示します。したがってSPAではクライアントのリクエストに対してJSONなどで記述されたデータが返されます。

(SPAは下のようなイメージです *一例です)

spaの説明画像

これだけだとSPAの方が複雑でなんでそんなことをするのかと疑問に感じる方もおられるでしょう。ですので次項でSPAのメリット(とデメリット)について説明します。

SPAのメリットとデメリット

SPAを導入することでは一般的に以下のようなメリットとデメリットがあると考えられます。

メリットデメリット
ページの遷移が滑らかで早いページの初期ロードが長い
JavaScriptを活用した美しいUIの実装が容易JavaScriptの記述量が長くなる

JavaScriptでページ遷移を行うのでユーザーはページの遷移時にフロント側でページの表示をできるので高速で、かつ滑らかにページの遷移をすることが可能です。一方でこれらのプログラムをJavaScriptで記述するのでjsファイルのサイズが大きく、初期ページの読み込みが長くなってしまいます。また、JavaScriptを介してページを表示するので美しい画面を表示することが容易です。もちろんこれらのDOM操作をJavaScriptで行う必要があるので、jsファイルの記述量が大きくなってしまいます。

今回のアプリをSPAで実装する理由はJavaScript(Chart.js)を介してページを表示するので美しい画面を表示するためにSPAが理想的だからです。MPAだとGolangでレスポンスされたhtmlファイルからJavaScriptで要素を取得してデータを読み込み、それをChart.jsを用いて再び画面い表示するという設計になります。一方でSPAだとJSONでJavaScriptがGolangサーバーからデータを受け取り、Chart.jsを通じて画面に表示するという設計になりスッキリします。

今回作成するアプリ

それでは早速アプリを作成していきます。まずは作成するアプリを確認してみましょう。ページ構成は以下のようになります。

/トップ画面、グラフを表示するページへのリンク
/temprature気温の線グラフが表示されるページ
/precipitation降水量の棒グラフが表示されるページ
その他のパス「このページは存在しません」が表示されるページ

画面の動きは以下のようになります。ルーティングにはvue-routerを使用しています(vue-routerの実装方法は後ほど)。

それぞれの画面は以下のようなレイアウトになっています。

(トップページ)

(グラフのページ 左:気温の線グラフ 右:降水量の棒グラフ)

 

(「このページは存在しません」のページ)

今回のアプリの実装で使用したコードはcode-databaseのgithubリポジトリに上がっていますのでお手元で確認したい方はぜひ活用してみてください。

前提知識

本記事は以下のような知識がある方におすすめです。先に不足している知識を勉強しときたいというかた向けにリンクも貼っておきます。

実装の手順

実装は以下の手順で行っていきたいと思います。

  1. npmで使用ライブラリのインストール
  2. vueで画面の実装
  3. vue-routerで画面のルーティング
  4. Golangでapiサーバーの作成
  5. axiosでのリクエストとレスポンス
  6. Chart.jsでのデータの表示

また、ファイル構造は以下のような形にします。

golang_vue/
 ├ node_modules/
 ├ static/
 │ └ main.js
 ├ index.html
 ├ main.go
 └ package.json

実装してみる

早速作っていきましょう。

npmで使用ライブラリのインストール

まずは今回用いるnpmパッケージをインストールしていきます。今回用いるのは以下の通りです。

  • vue
  • vue-router
  • chart.js
  • vue-chartjs
  • axios
  • vue-axios

それぞれの役割は以下の通りです。

vueVue.js(JavaScriptフレームワーク)
vue-routerVue.jsでルーティングをするため
chart.js表を表示するため
vue-cahrtjsVue.jsでChart.jsを使用するため
axiosAjax通信をするため
vue-axiosVue.jsでaxiosを使用するため

npmで全てのライブラリを入れていきます。

(ターミナル)

$ npm init

まずはルートディレクトリでnpmを初期化します。package.jsonが作成されていれば大丈夫です。

次に全てのライブラリを潜影蛇手(インストール)します。package.jsonの"dependencies"が以下のようになっていればバッチリです。

{
  "name": "golang_vue",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Code Database",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.19.2",
    "chart.js": "^2.9.3",
    "chartjs": "^0.3.24",
    "vue": "^2.6.11",
    "vue-axios": "^2.1.5",
    "vue-chartjs": "^3.5.0",
    "vue-router": "^3.1.6"
  }
}

続いてindex.htmlを作成してこれらのライブラリを読み込んでいきます。

<!DOCTYPE html>
<html lang='en'>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <script src='/node_modules/vue/dist/vue.min.js'></script>
    <script src='/node_modules/vue-router/dist/vue-router.min.js'></script>
    <script src='/node_modules/chart.js/dist/Chart.min.js'></script>
    <script src="/node_modules/vue-chartjs/dist/vue-chartjs.min.js"></script>
    <script src='/node_modules/axios/dist/axios.min.js'></script>
    <script src='/node_modules/vue-axios/dist/vue-axios.min.js'></script>
    <title>golang_vue</title>
</head>
<body>
</body>
</html>

src内が/node_modules/から始まる絶対パスになっていますがこれはGolangで静的ファイルを表示ための準備です。apiサーバを作成する際にまた説明をします。また、ライブラリを読み込む順番に気をつけましょう。vue.min.jsファイルを他のファイルの後で読み込んだり、Chart.min.jsファイルをvue-chartjs.min.jsファイルより後に読み込んだりするとエラーが起きてしまします(後に読み込むファイルが前に読み込むファイルの関数などを使用する場合があるため)。

vueで画面の実装

続いてvueを利用して画面をコンポーネントで作成して表示できるようにしましょう。まずはvueを表示するdiv#appタグをindex.htmlに用意します。

(index.html)

<body>
    <div id='app'>
        <router-view></router-view>
    </div>
    <script src="/static/main.js"></script>
</body>

main.jsも/node_modules/と同様に/static/のパスの後に記述して後にGolangで静的ファイルとしてサーブします。

今回の画面で使用するコンポーネントは以下のようになっています。

コンポーネント名(const定義)コンポーネントの説明子コンポーネント 
HeaderComponentヘッダーのコンポーネント
TempratureChart気温の線グラフを表すチャート(vue-chartで作成)
PrecipitationChart
降水量の棒グラフを表すチャート(vue-chartで作成)
TopPage
"/"でみれるトップページHeaderComponent
TemperatureChartPage
"/temprature"でみれる気温の線グラフのページHeaderComponent, TempratureChart
PrecipitationChartPage
"/precipitation"でみれる降水量の棒グラフのページHeaderComponent, PrecipitationChart
StatusNotFoundPage
「このページは存在しません」のページHeaderComponent

これらのコンポーネントをローカルコンポーネントで定義していきます(*大規模ではないのでWebpackなどでファイルを分割したり単一コンポーネントファイルを作成するなどはしません)。

(main.js)

const HeaderComponet = {
    template: '<h4>気候のグラフ</h4>'
}

const TemperatureChart = {}

const PrecipitationChart = {}

const TopPage = {
    components: {
        'header-component': HeaderComponet,
    },
    template: `
    <div>
        <header-component></header-component>
        <p>本サイトは沖縄県と東京都の気温と降水量を表示しています</p>
        <p>リンク</p>
    </div>
    `
}
const TemperatureChartPage = {
    components: {
        'header-component': HeaderComponet,
        'temprature-chart': TemperatureChart,
    },
    template: `
    <div>
        <header-component></header-component>
        <router-link to="/">トップに戻る</router-link>
        <router-link to="/precipitation">降水量を見る</router-link>
        <div style="width:50%;">
            <temprature-chart></temprature-chart>
        </div>
    </div>
    `
}

const PrecipitationChartPage = {
    components: {
        'header-component': HeaderComponet,
        'precipitation-chart': PrecipitationChart
    },
    template: `
    <div>
        <header-component></header-component>
        <div style="width:50%;">
            <precipitation-chart></precipitation-chart>
        </div>
    </div>
    `
}
const StatusNotFoundPage = {
    components: {
        'header-component': HeaderComponet,
    },
    template: `
    <div>
        <header-component></header-component>
        <p>お探しのページは見つかりませんでした</p>
    </div>
    `,
}

new Vue({
    el: "#app",
})
  • TempratureChartとPrecipitationChartのコンポーネントはvue-cahrtjsでChart.jsを用いて作成するのでまだ中身は書いていません
  • Golangでnode_modulesの適用もしていないので画面も表示されません

vue-routerで画面のルーティング

コンポーネントを用意できたら次にvue-routerを使ってルーティングを設定していきましょう。すでにルーティングの設計とコンポーネントの設計はご紹介したので適宜そちらを参照してください。また、vue-routerの使い方については「Vue Routerの書き方、使い方について解説」に導入方法が書いてありますので参考にしてみてください。

(main.js)

const routes = [
    { path: '/', component: TopPage },
    { path: '/temperature', component: TemperatureChartPage },
    { path: '/precipitation', component: PrecipitationChartPage },
    { path: '*', component: StatusNotFoundPage },
]

const router = new VueRouter({
    routes: routes
})

それぞれのURLに対してページのコンポーネントを渡しています。また指定されていないURLに対しては404エラーページのコンポーネントを表示させます。これらのルーティング設定をVueインスタンス生成時のオプションに加えて適用させましょう。

(main.js)

new Vue({
    el: "#app",
    router,  //追記
})

Golangでapiサーバーの作成

フロントでのコンポーネントの準備ができたらGolangでapiサーバーを立てる準備をします。まずは初期ロードを表示できるようにしましょう。

(main.go)

package main

import (
	"net/http"
	"os"
	"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() {
	dir, _ := os.Getwd()
	http.HandleFunc("/", mainHandler)
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(dir+"/static/"))))
	http.Handle("/node_modules/", http.StripPrefix("/node_modules/", http.FileServer(http.Dir(dir+"/node_modules/"))))
	http.ListenAndServe(":8000", nil)
}

"/"のリクエストに対してindex.htmlを表示させる処理を実装します。(参考: 「templateを使ってhtmlファイルを画面に表示する」)

また、node_modulesとstatic/main.jsの静的ファイルをサーブする処理も記述します。(参考: 「Golangで静的ファイル(JavaScript/CSS)を読み込む方法」)

ここまでできたら一度、サーバーを立ち上げて動作を確認してみましょう。以下のような画面が表示されれば大丈夫です。

ルーティングも機能しているか確認しましょう。(表の実装をまだしていないので表が表示されていなくても大丈夫です。)

続いて、クライアント側からのリクエストに対してJSON形式のデータをレスポンスするapiを実装しましょう。以下のような二つのapiを作ります。このJSONがあればクライアント側(Vue.js)で美しい表を描画することができます。

リクエストURLレスポンス形式内容
/api/temprature/
JSON地点名(Label)とデータ(data)のキーを持つ辞書を要素に持つ配列
/api/precipitation/
JSON地点名(Label)とデータ(data)のキーを持つ辞書を要素に持つ配列

したがってGolangで用意するデータ(構造体の配列)の各要素(構造体)は以下のようになります。

//ClimateDataElem データの一つのデータセット
type ClimateDataElem struct {
	Label string
	Data  []float64
}

このデータを作成して、JSON形式でレスポンスに書き加えます。今回データの作成には「気温と雨量の統計」のデータを使用しました。

//ClimateDataElem データの一つのデータセット
type ClimateDataElem struct {
	Label string
	Data  []float64
}

func tempratureHandler(w http.ResponseWriter, r *http.Request) {
	var temperatureData []ClimateDataElem
	temperatureData = append(temperatureData, ClimateDataElem{
		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, ClimateDataElem{
		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},
	})
	js, err := json.Marshal(temperatureData) //構造体をJSON形式にする
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json") //レスポンスの形式を "application/json"に設定する
	w.Write(js) //JSONをレスポンスに書き込む
}

func precipitationHandler(w http.ResponseWriter, r *http.Request) {
	var temperatureData []ClimateDataElem
	temperatureData = append(temperatureData, ClimateDataElem{
		Label: "沖縄県",
		Data:  []float64{107.0, 119.7, 161.4, 165.7, 231.6, 247.2, 141.4, 240.5, 260.5, 152.9, 110.2, 102.8},
	})
	temperatureData = append(temperatureData, ClimateDataElem{
		Label: "東京都",
		Data:  []float64{52.3, 56.1, 117.5, 124.5, 137.8, 167.7, 153.5, 168.2, 209.9, 197.8, 92.5, 51.0},
	})
	js, err := json.Marshal(temperatureData)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(js)
}

func main() {
	dir, _ := os.Getwd()
	http.HandleFunc("/", mainHandler)
	http.HandleFunc("/api/temprature/", tempratureHandler) // 追記
	http.HandleFunc("/api/precipitation/", precipitationHandler) // 追記
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(dir+"/static/"))))
	http.Handle("/node_modules/", http.StripPrefix("/node_modules/", http.FileServer(http.Dir(dir+"/node_modules/"))))
	http.ListenAndServe(":8000", nil)
}

JSONのレスポンスを作成するには以下のメソッドを使用しました。これらのメソッドについては「GolangでJSONを用いたAPIを実装する方法について解説」で詳しく解説しているので参考にしてみてください。

メソッド(メソッドのチェーン)説明
json.Marshal(構造体)
引数に入れた構造体(map[string]{}interfaceが推奨)
を[]bytes型のJSON文字列に変換する
w.Header().Set(key, value string)
*wはhttp.ResponseWriter型
wのHeaderを取得し、ヘッダーのkeyの内容をvalueに書き換える
w.Write(v []byte)
*wはhttp.ResponseWriter型
wの内容にvを書き込む

これで、リクエストに対してJSON文字列をレスポンスするapiが完成しました。

axiosでのリクエストとレスポンス

apiが作成できたのでクライアント側でリクエストを送れるようにしましょう。今回は手軽に非同期のAjax通信を実装できるaxiosを使用します。axiosをvueで使用するために、vue-axiosもすでにインストールしてあります。

今回の実装ではTempratureChartPageとPrecipitationChartPageのコンポーネントでapiからデータを取得し、propsでTempratureChartとPrecipitationChartにデータを渡し表を出力させます。したがって、TempratureChartPageとPrecipitationChartPageのコンポーネントは以下のようになります。

const TemperatureChartPage = {
    data() {
        return {
            jsonData: []
        }
    },
    components: {
        'header-component': HeaderComponet,
        'temprature-chart': TemperatureChart,
    },
    created() {
        this.axios.get('/api/temprature/')
            .then((response) => {
                this.jsonData = response.data
            })
    },
    template: `
    <div>
        <header-component></header-component>
        <router-link to="/">トップに戻る</router-link>
        <router-link to="/precipitation">降水量を見る</router-link>
        <div style="width:50%;">
            <temprature-chart v-bind:jsonData="jsonData"></temprature-chart>
        </div>
    </div>
    `,
}

const PrecipitationChartPage = {
    data() {
        return {
            jsonData: []
        }
    },
    components: {
        'header-component': HeaderComponet,
        'precipitation-chart': PrecipitationChart
    },
    created() {
        this.axios.get('/api/precipitation/')
            .then((response) => {
                this.jsonData = response.data
            })
    },
    template: `
    <div>
        <header-component></header-component>
        <router-link to="/">トップに戻る</router-link>
        <router-link to="/temperature">気温を見る</router-link>
        <div style="width:50%;">
            <precipitation-chart v-bind:jsonData="jsonData"></precipitation-chart>
        </div>
    </div>
    `,
}

created()でaxios.get()を用いてリクエストを送信して、レスポンスをjsonDataというdataに代入します。この際に、.then()内部をアロー関数にしないとコンポーネントのdataを参照できないことに注意しましょう。function()を用いるとコンポーネントのdata()にを参照できなくなります。Vue.jsはこのthisの扱いが少し難しいです。

さらにjsonDataをpropで子コンポーネント(TempratureChartとPrecipitationChart)に渡します。

Chart.jsでのデータの表示

あとはChart.jsを用いてpropで受け取ったdataをきれいに表にしましょう。Chart.jsもChart.jsをvueで扱うためのvue-cahrtjsもすでにインストールしてあるはずなのですぐに使えます。以下のようなコードになります。

const TemperatureChart = {
    extends: VueChartJs.Line,
    props: {
        jsonData: Array
    },
    watch: {
        jsonData: {
            deep: true,
            handler: function () {
                const data = []
                for (let i = 0; i < this.jsonData.length; i++) {
                    data[i] = {
                        label: this.jsonData[i].Label,
                        data: this.jsonData[i].Data,
                        borderColor: `rgb(${(i + 1) * 50}, ${(i + 1) * 100}, ${(i + 1) * 100})`,
                        fill: false,
                        lineTension: 0.1
                    }
                }
                this.renderChart(
                    {
                        "labels": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
                        "datasets": data
                    },
                    {
                        scales: {
                            yAxes: [{
                                ticks: {
                                    suggestedMax: 40,
                                    suggestedMin: -20,
                                }
                            }]
                        }
                    }
                )
            }
        } 
    }
}

const PrecipitationChart = {
    extends: VueChartJs.Bar,
    props: {
        jsonData: Array
    },
    watch: {
        jsonData: {
            deep: true,
            handler: function () {
                const data = []
                for (let i = 0; i < this.jsonData.length; i++) {
                    data[i] = {
                        "label": this.jsonData[i].Label,
                        "data": this.jsonData[i].Data,
                        "borderWidth": 5,
                        "backgroundColor": `rgb(${(i + 1) * 50}, ${(i + 1) * 100}, ${(i + 1) * 100})`,
                    }
                }
                this.renderChart(
                    {
                        "labels": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
                        "datasets": data
                    },
                    {
                        scales: {
                            yAxes: [{
                                ticks: {
                                    suggestedMax: 300,
                                    suggestedMin: 0,
                                }
                            }]
                        }
                    }
                )
            }
        }
    },
}

まずはextends: VueChartJs.Lineextends: VueChartJs.Barで使用する表のtypeを指定します。これはvue-chartjs特有の書き方です。

propでは先ほどaxiosで受け取り渡したjsonDataを受け取ります。

watchメソッドで常にpropで渡されるdetaを監視します。これはaxiosが非同期でAjax通信を行い、レスポンスを受け取るため、propの値が変わったら表を表示する必要があるためです。監視するjsonDataの配列の要素も関しにするため、deep: trueを指定します。watchメソッド周りについては「watchプロパティの書き方・使い方について解説」も参考になると思います。

wartch内部ではjsonDataの値からChart.jsのdatasetsを作成します。datasetsについては「Chart.jsで線グラフと棒グラフの美しい雨温図を実装する」を参考にしてみてください。そして、datasetsをrenderChart()メソッドの引数に渡して"datasets"の部分に渡します。

これで全ての実装が完了しました。この状態でサーバーを立ち上げて表が表示されていれば完璧です。(用意していないURLに対して404ページが表示されているかも確認してみましょう)

実装に使用したコード

今回使用したコードを紹介します。

(フォルダの階層)

golang_vue/
 ├ node_modules/
 ├ static/
 │ └ main.js
 ├ index.html
 ├ main.go
 └ package.json

(main.go)

package main

import (
	"encoding/json"
	"net/http"
	"os"
	"text/template"
)

//ClimateDataElem データの一つのデータセット
type ClimateDataElem 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 tempratureHandler(w http.ResponseWriter, r *http.Request) {
	var temperatureData []ClimateDataElem
	temperatureData = append(temperatureData, ClimateDataElem{
		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, ClimateDataElem{
		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},
	})
	js, err := json.Marshal(temperatureData)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(js)
}

func precipitationHandler(w http.ResponseWriter, r *http.Request) {
	var temperatureData []ClimateDataElem
	temperatureData = append(temperatureData, ClimateDataElem{
		Label: "沖縄県",
		Data:  []float64{107.0, 119.7, 161.4, 165.7, 231.6, 247.2, 141.4, 240.5, 260.5, 152.9, 110.2, 102.8},
	})
	temperatureData = append(temperatureData, ClimateDataElem{
		Label: "東京都",
		Data:  []float64{52.3, 56.1, 117.5, 124.5, 137.8, 167.7, 153.5, 168.2, 209.9, 197.8, 92.5, 51.0},
	})
	js, err := json.Marshal(temperatureData)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(js)
}

func main() {
	dir, _ := os.Getwd()
	http.HandleFunc("/", mainHandler)
	http.HandleFunc("/api/temprature/", tempratureHandler)
	http.HandleFunc("/api/precipitation/", precipitationHandler)
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(dir+"/static/"))))
	http.Handle("/node_modules/", http.StripPrefix("/node_modules/", http.FileServer(http.Dir(dir+"/node_modules/"))))
	http.ListenAndServe(":8000", nil)
}

(main.js)

const HeaderComponet = {
    template: '<h4>気候のグラフ</h4>'
}

const TemperatureChart = {
    extends: VueChartJs.Line,
    props: {
        jsonData: Array
    },
    watch: {
        jsonData: {
            deep: true,
            handler: function () {
                const data = []
                for (let i = 0; i < this.jsonData.length; i++) {
                    data[i] = {
                        label: this.jsonData[i].Label,
                        data: this.jsonData[i].Data,
                        borderColor: `rgb(${(i + 1) * 50}, ${(i + 1) * 100}, ${(i + 1) * 100})`,
                        fill: false,
                        lineTension: 0.1
                    }
                }
                this.renderChart(
                    {
                        "labels": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
                        "datasets": data
                    },
                    {
                        scales: {
                            yAxes: [{
                                ticks: {
                                    suggestedMax: 40,
                                    suggestedMin: -20,
                                }
                            }]
                        }
                    }
                )
            }
        } 
    }
}

const PrecipitationChart = {
    extends: VueChartJs.Bar,
    props: {
        jsonData: Array
    },
    watch: {
        jsonData: {
            deep: true,
            handler: function () {
                const data = []
                for (let i = 0; i < this.jsonData.length; i++) {
                    data[i] = {
                        "label": this.jsonData[i].Label,
                        "data": this.jsonData[i].Data,
                        "borderWidth": 5,
                        "backgroundColor": `rgb(${(i + 1) * 50}, ${(i + 1) * 100}, ${(i + 1) * 100})`,
                    }
                }
                this.renderChart(
                    {
                        "labels": ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
                        "datasets": data
                    },
                    {
                        scales: {
                            yAxes: [{
                                ticks: {
                                    suggestedMax: 300,
                                    suggestedMin: 0,
                                }
                            }]
                        }
                    }
                )
            }
        }
    },
}

const TopPage = {
    components: {
        'header-component': HeaderComponet,
    },
    template: `
    <div>
        <header-component></header-component>
        <p>本サイトは沖縄県と東京都の気温と降水量を表示しています</p>
        <p>リンク</p>
        <router-link to="/temperature">気温を見る</router-link>
        <router-link to="/precipitation">降水量を見る</router-link>
    </div>
    `
}

const TemperatureChartPage = {
    data() {
        return {
            jsonData: []
        }
    },
    components: {
        'header-component': HeaderComponet,
        'temprature-chart': TemperatureChart,
    },
    created() {
        this.axios.get('/api/temprature/')
            .then((response) => {
                this.jsonData = response.data
            })
    },
    template: `
    <div>
        <header-component></header-component>
        <router-link to="/">トップに戻る</router-link>
        <router-link to="/precipitation">降水量を見る</router-link>
        <div style="width:50%;">
            <temprature-chart v-bind:jsonData="jsonData"></temprature-chart>
        </div>
    </div>
    `,
}

const PrecipitationChartPage = {
    data() {
        return {
            jsonData: []
        }
    },
    components: {
        'header-component': HeaderComponet,
        'precipitation-chart': PrecipitationChart
    },
    created() {
        this.axios.get('/api/precipitation/')
            .then((response) => {
                this.jsonData = response.data
            })
    },
    template: `
    <div>
        <header-component></header-component>
        <router-link to="/">トップに戻る</router-link>
        <router-link to="/temperature">気温を見る</router-link>
        <div style="width:50%;">
            <precipitation-chart v-bind:jsonData="jsonData"></precipitation-chart>
        </div>
    </div>
    `,
}

const StatusNotFoundPage = {
    components: {
        'header-component': HeaderComponet,
    },
    template: `
    <div>
        <header-component></header-component>
        <p>お探しのページは見つかりませんでした</p>
    </div>
    `,
}

const routes = [
    { path: '/', component: TopPage },
    { path: '/temperature', component: TemperatureChartPage },
    { path: '/precipitation', component: PrecipitationChartPage },
    { path: '*', component: StatusNotFoundPage },
]

const router = new VueRouter({
    routes: routes
})

new Vue({
    el: "#app",
    router,
})

(index.html)

<!DOCTYPE html>
<html lang='en'>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <script src='/node_modules/vue/dist/vue.min.js'></script>
    <script src='/node_modules/vue-router/dist/vue-router.min.js'></script>
    <script src='/node_modules/chart.js/dist/Chart.min.js'></script>
    <script src="/node_modules/vue-chartjs/dist/vue-chartjs.min.js"></script>
    <script src='/node_modules/axios/dist/axios.min.js'></script>
    <script src='/node_modules/vue-axios/dist/vue-axios.min.js'></script>
    <title>golang_vue</title>
</head>
<body>
    <div id='app'>
        <router-view></router-view>
    </div>
    <script src="/static/main.js"></script>
</body>
</html>

(package.json)

{
  "name": "golang_vue",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Code Database",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.19.2",
    "chart.js": "^2.9.3",
    "chartjs": "^0.3.24",
    "vue": "^2.6.11",
    "vue-axios": "^2.1.5",
    "vue-chartjs": "^3.5.0",
    "vue-router": "^3.1.6"
  }
}

今回のアプリの実装で使用したコードはcode-databaseのgithubリポジトリに上がっています。

この記事のまとめ

本記事ではVue.jsとGolangを用いて小さなSPAを作成してみました。最後に記事の要点をまとめておきます。

  • SPAはMPAよりもJavaScriptで凝ったデザインを実装するのに適している
  • Vue.jsで簡単にルーティングとコンポーネント表示ができる
  • GolangではシンプルにJSONを返すapiサーバーを作成することができる
  • Chart.jsでは勝手に美しいグラフを作成してくれる

皆さんもSPAで見た目の美しいアプリを作ってみましょう!