Go + gin-gonic + Vue で作るWebアプリの始め方

この記事では Goと[gin-gonic](https://github.com/gin-gonic/gin) (Go の Web Application Framework) および webpack と Vue を使ってWebアプリを作る際の初期環境の構築方法について説明します。自分自信、webpackやVueのプロではないので、もっといい方法などあればコメント歓迎です。なおこの工程でできる完成品は https://github.com/m-mizutani/web-app-go に置いてあります。

## 構築手順

### webpack周りの設定

最初にyarnのパッケージを諸々追加します。 `yarn init` するとパッケージの内容についていろいろ聞かれるのでよしなに入力します。

```bash
$ yarn init
$ yarn add -D @babel/cli @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server html-webpack-plugin vue-loader vue-template-compiler css-loader vue-style-loader sass-loader
$ yarn add babel-polyfill vue  node-sass axios
```

そのあと、 `webpack.config.js` を作成します。スタイルシートにはSCSSを使うことを想定しています。 `./static` がデプロイ用ファイルの置き場、`./src` が元のJavaScriptやスタイルシートを格納するディレクトリです。

```js
const path = require('path');
const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
  mode: "development",
  entry: ["babel-polyfill", path.resolve("src", "js", "index.js")],
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "static/js/"),
    publicPath: "/js"
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: "vue-loader"
      },
      {
        test: /\.js$/,
        loader: "babel-loader"
      },
      {
        test: /\.s[ac]ss$/i,
        use: [
          // Creates `style` nodes from JS strings
          'vue-style-loader',
          // Translates CSS into CommonJS
          'css-loader',
          // Compiles Sass to CSS
          'sass-loader',
        ],
      }
    ]
  },
  resolve: {
    extensions: [".js", "json", "jsx", "vue"],
    alias: {
      vue$: "vue/dist/vue.esm.js"
    }
  },
  devServer: {
    contentBase: "static",
    proxy: {
      "/api": "http://localhost:9080"
    }
  },
  plugins: [new VueLoaderPlugin()]
};
```

最後の方に書いてあるとおり、 `/api/*` に対してはgoで書いたserverにproxyさせます。

さらに、npm からコマンド実行できるように `package.json` に以下の項目を追加しておきます。

```json
  "scripts": {
    "start": "webpack-dev-server",
    "build": "webpack --optimize-minimize"
  },
```

### Web関連のコードを追加

以下のファイルを初期ファイルとして配置します。 "This is test" というボタンをクリックするとAjaxでJSONをAPIサーバから取得して、その中の `message` という項目を表示するというシンプルなものです。

- `src/css/main.scss`
- `src/js/index.js`
- `src/js/app.vue`
- `static/index.html`

**main.scss**

```scss
body {
  font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;
}
```

**index.js**

```js
import '../css/main.scss'

import _ from 'lodash';
import "babel-polyfill";
import Vue from "vue";

import App from "./app.vue";

new Vue({
    el: "#app",
    render: h => h(App)
});
```

**app.vue**

```vue
<template>
  <div>
    <div>MyApp</div>
    <button v-on:click="showMessage">This is test</button>
    <div>{{message}}</div>
  </div>
</template>
<script>
import axios from "axios";
const appData = {
  message: ""
};
export default {
  data() {
    return appData;
  },
  methods: {
    showMessage: showMessage
  }
};
function showMessage() {
  axios.get("/api/v1/hello").then(res => {
    console.log(res);
    appData.message = res.data.message;
  });
}
</script>
<style>
</style>
```

**index.html**

```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>MyApp</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="/js/bundle.js"></script>
  </body>
</html>
```

### Goのサーバを書く

Goのモジュールを初期化します。パッケージ名は `github.com/m-mizutani/web-app-go` という想定ですので必要に応じて置き換えます。

```sh
$ go mod init github.com/m-mizutani/web-app-go
```

そのあと `main.go` をレポジトリのトップディレクトリに作成します。

```go
package main

import (
	"fmt"
	"os"

	"github.com/gin-contrib/static"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
)

var logger = logrus.New()

var logLevelMap = map[string]logrus.Level{
	"trace": logrus.TraceLevel,
	"debug": logrus.DebugLevel,
	"info":  logrus.InfoLevel,
	"warn":  logrus.WarnLevel,
	"error": logrus.ErrorLevel,
}

type arguments struct {
	LogLevel       string
	BindAddress    string
	BindPort       int
	StaticContents string
}

func runServer(args arguments) error {
	level, ok := logLevelMap[args.LogLevel]
	if !ok {
		return fmt.Errorf("Invalid log level: %s", args.LogLevel)
	}
	logger.SetLevel(level)
	logger.SetFormatter(&logrus.JSONFormatter{})

	logger.WithFields(logrus.Fields{
		"args": args,
	}).Info("Given options")

	r := gin.Default()

	r.Use(static.Serve("/", static.LocalFile(args.StaticContents, false)))
	r.GET("/api/v1/hello", func(c *gin.Context) {
		c.String(200, `{"message":"hello, hello, hello"}`)
	})

	if err := r.Run(fmt.Sprintf("%s:%d", args.BindAddress, args.BindPort)); err != nil {
		return err
	}

	return nil
}

func main() {
	args := arguments{
		LogLevel: "info",
		BindAddress: "0.0.0.0",
		BindPort: 9080,
		StaticContents: "./static",
        }

	if err := runServer(args); err != nil {
		logger.WithError(err).Fatal("Server exits with error")
	}
}
```

## 開発

### Goサーバを動かす

`go run .` でも起動しますが、開発のためにhot reloadができると便利なので
[air](https://github.com/cosmtrek/air) を使います。 `.air.conf` という設定ファイルを用意します。以下サンプルです。

```toml
[build]
include_ext = ["go"]
exclude_dir = ["src", "tmp", "node_modules"]
delay = 1000 # ms
stop_on_error = true
log = "air_errors.log"
```

ファイルを作成したら以下の通り実行します。

```
$ air -c .air.conf
```

### Webpack dev server を動かす

こちらはデフォルトでホットリロードに対応してくれているので、そのまま `webpack-dev-server` を起動します。 package.json の `script` の項目を記述していれば、以下のコマンドで起動します。

```
$ npm run start
```

サーバが起動したら http://localhost:8080 を開くことで

## デプロイ

Goのバイナリ + `./static` 以下のファイルを対象サーバにコピーしたり、Docker image 化してpushしたりとすることで、デプロイできます。