vue+hugo+milligramでブログを作ってみた


最近、フロントエンドに`vue.js`を使うことが多くなりました。

`vue`は何がいいのかと言うと、私の感想では、「簡単に作れる」という点がいいなと思っています。それほど複雑でもないし。

ですが、大規模なものを`vue`で作っていくと、スケールしにくいんじゃないかなって感じはします。一方、`react`とかだと最初は複雑だったり、難しかったりするかもですが、スケールしやすいのではないかな。

とは言え、かなり大雑把な感想ですが、私は、vueのほうが好きですね。しかし、内容によって使い分けたほうが良いと思います。

## サンプルコード

そんな`vue`ですが、実際にどう使ったのかと言うと、jsonを読み込んでページに反映するみたいな感じのものを書きました。

> package.json

```json
{
  "name": "vue-sample",
  "version": "0.0.1",
  "author": "syui <syui@syui.cf>",
  "license": "MIT",
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot --host 0.0.0.0 --port 7000",
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
    "build:watch": "cross-env NODE_ENV=production WEBPACK_WATCH=true webpack --progress --hide-modules"
  },
  "dependencies": {
    "vue": "^2.4.2",
    "vuex": "^2.4.0"
  },
  "devDependencies": {
    "axios": "^0.17.1",
    "babel-core": "^6.0.0",
    "babel-loader": "^6.0.0",
    "babel-preset-latest": "^6.0.0",
    "copy-webpack-plugin": "^4.0.1",
    "cross-env": "^3.0.0",
    "css-loader": "^0.28.7",
    "extract-text-webpack-plugin": "^2.1.0",
    "file-loader": "^0.11.2",
    "html-webpack-plugin": "^2.28.0",
    "postcss": "^6.0.11",
    "postcss-base64": "^0.7.1",
    "postcss-cssnext": "^3.0.2",
    "postcss-loader": "^2.0.6",
    "postcss-smart-import": "^0.7.5",
    "postcss-url": "^7.1.2",
    "style-loader": "^0.18.2",
    "vue-loader": "^12.0.4",
    "vue-template-compiler": "^2.4.2",
    "webpack": "^2.6.1",
    "webpack-dev-server": "2.7.1"
  }
}
```

> webpack.config.js

```sh
var path = require('path');
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  watch: process.env.WEBPACK_WATCH === 'true',
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, './public-vue'),
    publicPath: '',
    filename: 'build.js'
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          loaders: {
          }
          // other vue-loader options go here
        }
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
        loader: 'file-loader',
        options: {
          name: '[name].[ext]?[hash]'
        }
      },
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            {
              loader: 'css-loader',
              options: { importLoaders: 1 }
            },
            {
              loader: 'postcss-loader',
              options: {
                plugins: [
                  require('postcss-smart-import')(),
                  require('postcss-url')(),
                  require('postcss-base64')({
                    extensions: ['.svg'],
                    root: 'src'
                  }),
                  require('postcss-cssnext')({
                    browsers: ['> 1%', 'last 2 versions', 'Firefox ESR', 'Opera 12.1'],
                    features: { autoprefixer: { remove: false } } // 'background-image: radial-gradient' broken in autoprefixer
                  })
                ]
              }
            }
          ]
        })
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin('[name].css'),
    new HtmlWebpackPlugin({
      template: 'src/index.html'
    })
  ],
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    }
  },
  devServer: {
    historyApiFallback: true,
    noInfo: true
  },
  performance: {
    hints: false
  },
  devtool: '#eval-source-map'
};

if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map';
  // http://vue-loader.vuejs.org/en/workflow/production.html
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ]);
}
```

ここでビルドやプレビューに必要なものをインストールします。

```sh
$ yarn install
```

インストールできたら先程作った`webpack`の設定ファイルに沿って、vueのコードを書いていきます。

> src/main.js

```
import Vue from 'vue'
import axios from 'axios'

Vue.component('demo-grid', {
	template: '#grid-template',
	props: {
		data: Array,
		columns: Array,
		filterKey: String
	},
	data: function () {
		var sortOrders = {}
		this.columns.forEach(function (key) {
			sortOrders[key] = 1
		})
		return {
			sortKey: '',
			sortOrders: sortOrders
		}
	},
	computed: {
		filteredData: function () {
			var sortKey = this.sortKey
			var filterKey = this.filterKey && this.filterKey.toLowerCase()
			var order = this.sortOrders[sortKey] || 1
			var data = this.data
			if (filterKey) {
				data = data.filter(function (row) {
					return Object.keys(row).some(function (key) {
						return String(row[key]).toLowerCase().indexOf(filterKey) > -1
					})
				})
			}
			if (sortKey) {
				data = data.slice().sort(function (a, b) {
					a = a[sortKey]
					b = b[sortKey]
					return (a === b ? 0 : a > b ? 1 : -1) * order
				})
			}
			return data
		}
	},
	filters: {
		capitalize: function (str) {
			return str.charAt(0).toUpperCase() + str.slice(1)
		}
	},
	methods: {
		sortBy: function (key) {
			this.sortKey = key
			this.sortOrders[key] = this.sortOrders[key] * -1
		}
	}
})

var status = new Vue({
	el: '#status',
	data: {
		gridColumns: ['name','team','xp','get','zukan'],
		gridData: []
	},
	beforeCreate: function () {
		axios.get('https://syui.gitlab.io/pokemon-zukan/json/status.json')
			.then(function (response) {
				status.gridData = response.data;
			})
			.catch(function (error) {
				console.log(error);
			});
	}
});
```

> src/index.html

```
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<title>title</title>
		<style>
			table {
			  border: 2px solid #42b983;
			  border-radius: 3px;
			  background-color: #fff;
			}
			
			th {
			  background-color: #42b983;
			  color: rgba(255,255,255,0.66);
			  cursor: pointer;
			  -webkit-user-select: none;
			  -moz-user-select: none;
			  -ms-user-select: none;
			  user-select: none;
			}
			
			td {
			  background-color: #f9f9f9;
			}
			
			th, td {
			  min-width: 120px;
			  padding: 10px 20px;
			}
		</style>
	</head>
	<body>

	<script type="text/x-template" id="grid-template">
	  <table>
	    <thead>
	      <tr>
	        <th v-for="key in columns"
	          @click="sortBy(key)"
	          :class="{ active: sortKey == key }">
	          {{ key | capitalize }}
	          <span class="arrow" :class="sortOrders[key] > 0 ? 'asc' : 'dsc'">
	          </span>
	        </th>
	      </tr>
	    </thead>
	    <tbody>
	      <tr v-for="entry in filteredData">
	        <td v-for="key in columns">
	          {{entry[key]}}
	        </td>
	      </tr>
	    </tbody>
	  </table>
	</script>

	<div id="status">
	  <demo-grid
	    :data="gridData"
	    :columns="gridColumns">
	  </demo-grid>
	</div>

  </body>
</html>
```

あとは、プレビューで確認してみましょう。

```sh
$ yarn dev
```

## hugo+milligram

この`vue`と静的サイトジェネレーターである`hugo`、cssフレームワークである`milligram`を組み合わせて、簡単にブログを作ってみました。

何でかと言うと、`vue`って、トップページやWebアプリを作るのには向いていると思うんですが、ブログを作るのには向いてないと思うんですよね。まず、各種ページを作ってmarkdownで書く体裁を作るのがめんどくさそう。ということで、ここは慣れたhugoを使うことに。あと、cssも書くの面倒ですからね。milligramというやつを使って設定することに。

組み合わせに関しては、`yarn build`して`./public-vue/build*`を`hugo`の`./static/`に置いて、適時、もしくは`head`とかに`<script type="text/javascript" src="build.js"></script>`みたいな感じで読み込めばいいと思います。

## 感想

ほとんどサンプルなんですけどね、ざっとコードを見てみた感じで言いますと、vueはわかりやすいし、作りやすい感じしますね。

なので、フロントエンドの初学者には、`vue.js`はおすすめかもです。