JavaScript
vue.js
Vuex

Vue.js入門ハンズオン、ビンゴゲームのルーレットを作成してみる

More than 1 year has passed since last update.

概要

  • 想定読者、npmはとりあえず触ったことはある、Vue.js触ってみたいなという人向け
  • Vue.jsをさらっと使用してみる
  • ハンズオンみたいな感じでガンガン触る形式
  • Vue.jsの機能とかを網羅するというよりもとりあえず物を作ってみるという形
  • 細かい機能には触れず、ビンゴゲームのルーレットを作成するために必要な機能だけを触っていく
  • Vuexに関してはとりあえず使用

ある程度の環境構築にも触れて、そのまま進めていけば物はできる形にしています。

最終的なソース(GitHub)

環境準備

筆者環境

名称 バージョン
Node.js 6.8.1
npm 4.0.1

最終的なpackage.json

{
  "name": "vuebingo-qiita",
  "version": "1.0.0",
  "description": "vuebingo-qiita",
  "main": "index.js",
  "scripts": {
    "webpack": "webpack"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.21.0",
    "babel-loader": "^6.2.10",
    "babel-plugin-transform-runtime": "^6.15.0",
    "babel-preset-es2015": "^6.18.0",
    "copy-webpack-plugin": "^4.0.1",
    "vue-loader": "^10.0.2",
    "vue-template-compiler": "^2.1.8",
    "webpack": "^1.14.0"
  },
  "dependencies": {
    "vue": "^2.1.8",
    "vuex": "^2.1.1"
  }
}

最終的なディレクトリ構成

.
├── bin
│   ├── app.js
│   └── index.html
├── node_modules
├── package.json
├── src
│   ├── app.vue
│   ├── app.js
│   ├── components
│   │   ├── controller.vue
│   │   ├── history.vue
│   │   └── result.vue
│   ├── index.html
│   └── store
│       ├── index.js
│       └── mutations.js
└── webpack.config.js

Node環境

事前にNode.js、npmはインストールを行っておいてください。
まず作成したいディレクトリでnpm initと打ちpackage.jsonを生成しておきます。
いろいろこれから作成するものの説明の入力を求められますがエンター連打でおっけいです。
連打した結果、下記のようなpackage.jsonが生成されます。

{
  "name": "vuebingo-qiita",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

このpackage.jsonを作成したディレクトリを今回のプロジェクトのルートディレクトリとします。

webpackインストール

webpackをインストールします。
webpackは簡単に言えば、依存関係を解決して色々なものをひとまとめのファイルに格納してくれるものです。
他にも機能は色々ありますが、ここでは説明を省きます。

ルートディレクトリでnpm install --save-dev webpackと打ってWebpackをインストールしましょう。
--save-devというオプションをつけることによって、package.jsonのdevDependencies(開発用パッケージの項目)に追加されます。

{
  "devDependencies": {
    "webpack": "^1.14.0"
  }
}

ここではローカルインストール、つまりそのディレクトリ内のモジュールとしてインストールを行います。
ローカルのwebpackをそのまま実行するには下記コマンドを打つかpackage.jsonのscriptsの項目に追記する方法があります。

`npm bin`/webpack

いちいち打つのは面倒なのでpackage.jsonのscriptsの中に下記を追記して実行する形にしましょう。

{
  "scripts": {
    "webpack": "webpack"
  }
}

これでnpm run webpackでローカルのwebpackが実行できるようになります。
webpackがインストールされていないディレクトリでも使えるコマンドとしてインストールするにはnpm install -g webpackとしましょう。
srcディレクトリの中に空のファイルとして、今回のメインのJSとなるapp.jsを作成しておきましょう。
そしてwebpackの設定ファイルであるwebpack.config.jsをルートディレクトリに仮で作成します。

webpack.config.js
module.exports = {
    entry: {
        app: './src/app.js'
    },
    output: {
        path: __dirname + '/bin/',
        filename: '[name].js'
    }
};

これでnpm run webpackを行うと下記のように出力されればwebpackの実行準備は完了です。

$ npm run webpack
> vuebingo-qiita@1.0.0 webpack /Users/***/vuebingo-qiita
> webpack

Hash: 382b305823bf809a5c9f
Version: webpack 1.14.0
Time: 34ms
 Asset     Size  Chunks             Chunk Names
app.js  1.39 kB       0  [emitted]  app
   [0] ./src/app.js 0 bytes {0} [built]

ひとまず仮でWebpackを使用してみる

新しくsrc/index.htmlを作成して下さい。
先ほど作成したsrc/app.jsとsrc/index.htmlに、仮で下記を記入しておきます。

app.js
alert('bingo!!');
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script src="app.js"></script>
</body>
</html>

次にWebpackの設定です。
htmlをコピーするためにnpm install --save-dev copy-webpack-pluginでwebpackのコピー用プラグインをインストールしておきましょう。
webpack.config.jsを下記のように変更します。

const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
    entry: {
        'app': './src/app.js'
    },
    output: {
        path: __dirname + '/bin/',
        filename: '[name].js'
    },
    devtool: 'inline-source-map',
    plugins: [
        new CopyWebpackPlugin([
            {from: './src/index.html'}
        ])
    ]
};

これでnpm run webpackを叩いてみましょう。
binディレクトリにindex.htmlがコピーされ、src/app.jsがbin/app.jsとして出力されます。
一旦bin/index.htmlをブラウザで表示してみるとアラートでbingo!!と表示されるはずです。

Vue.js準備

npm install --save vueでインストールします。
package.jsonのdependenciesにvueの項目が追加されます。

package.json
{
  "dependencies": {
    "vue": "^2.1.8"
  }
}

Vue.jsを動かす

app.jsを下記の記述に変更します。

app.js
var Vue = require('vue');

これでbin/app.jsにVue.jsが内蔵されます。
中身を見るとVue.jsのクレジット表記が見つかるかと思います。

vue-loader

便利にするためにvue-loaderというものを使用します。
まず下記コマンド。

npm install --save-dev vue-loader vue-template-compiler

webpack.config.jsに下記を追加

    module: {
        loaders: [
            {
                test: /\.vue$/, loader: 'vue'
            }
        ]
    }

続いてHTMLとVueを仮で動かしてみましょう。

ランタイム限定ビルドとスタンドアロン

Vue.jsには、CDNや別途ダウンロードしscriptタグでファイルを読み込むランタイム限定ビルドと、npmでインストールするスタンドアロンがあります。

下記はランタイム限定ビルド(npm installで入れたもの)では動かないので注意。
なので読み飛ばしてランタイム限定ビルドのほうから初めてもらって構いません。

スタンドアロン版

スタンドアロン版(CDN等)では下記の記述で一旦動くと思いますが、この記事ではランタイム限定ビルドで実装していきます。
なので解説は行いません。

<div id="app">
  {{ msg }}
</div>
var app = new Vue({
    el: '#app',
    data: {
        msg: 'app message'
    }
});

ランタイム限定ビルドで上記のコードを動かそうとすると下記のようなエラーが出ると思います。

[Vue warn]: Failed to mount component: template or render function not defined. 
(found in root instance)

ランタイム限定ビルド

src/index.htmlのapp.jsの読み込みの前に下記を追加します。

index.html
<div id="app"></div>

app.vueというファイルをsrc配下に作成します。

app.vue
<template>
    <div>{{ msg }}</div>
</template>
<script>
    module.exports = {
        data: function () {
            return {
                msg: 'app message'
            }
        }
    }
</script>

これがvue-loaderによって、Vue.jsのひとつのコンポーネントとして読み込み可能になります。

templateタグに囲まれている部分がそのコンポーネントのHTMLとして機能する部分となります。
scriptタグがJSの記述箇所です。

そして、module.exportsに入れるオブジェクトに、dataプロパティを定義しています。
そのプロパティにオブジェクトを返す無名関数を定義してあげると、Vueで使用できる変数となります。
dataプロパティでは、Vueで使用する変数の初期化を行います。
HTMLの中にマスタッシュ記法{{ }}で変数を記入してあげると、変数に変化があった時に、その箇所を更新してくれます。

app.jsを下記に変更します。

app.js
var Vue = require('vue');
var App = require('./app.vue');

var app = new Vue({
    render: function (h) {
        return h(App);
    }
}).$mount('#app');

$mountの引数に起点となるエレメントの指定、renderで第一引数に取る関数の中にルートとなるコンポーネントを入れてあげます。
ここでwebpackを叩いてみましょう。
これで<div id="app"></div>があった箇所のHTMLが<div>app message</div>となります。

その他必要なもの

ひとまず動いたので、楽に開発するためのものを入れていきましょう。

babel導入

npm install --save-dev babel-core # babel-loaderを動かすために必要
npm install --save-dev babel-loader # vueファイルをwebpack上でbabelで動かすために必要
npm install --save-dev babel-preset-es2015 # es2015をes5に書き換えるために必要
npm install --save-dev babel-plugin-transform-runtime # 下記URL参考

参考:babel-polyfillとbabel-runtimeの使い分けに迷ったので調べた

webpack.config.jsのmoduleのloadersに下記を追加。

{
    test: /\.js$/,
    exclude: /node_modules/,
    loader: 'babel'
}

ルートにbableの設定を追加。

babel: {
    presets: ['es2015'],
    plugins: ['transform-runtime']
},

jsとvueファイルも書き換え

app.js
import Vue from 'vue';
import App from './app.vue';

var app = new Vue({
    render: h => h(App)
}).$mount('#app');
app.vue
<template>
    <div>{{ msg }}</div>
</template>
<script>
    export default {
        data() {
            return {
                msg: 'app message'
            }
        }
    }
</script>

書き方が変わっているだけで行っていることは一緒です。
ここでさっきと同じように動いているか確認しましょう。
Uncaught TypeError: $export is not a functionなどが表示されていたら失敗しているので、再度設定を見直しましょう。

コンポーネントを仮で作成

src/componentsのディレクトリを作成し、配下にビンゴの結果(result)、スタートストップのボタン(controller)、今まで出た数字(history)のコンポーネントとしてvueファイルを作成します。

result.vue
<template>
    <div>result</div>
</template>
controller.vue
<template>
    <div>controller</div>
</template>
history.vue
<template>
    <div>history</div>
</template>

その上で、app.vueを下記のように書き換えます。

<template>
    <div>
        <result></result>
        <controller></controller>
        <history></history>
    </div>
</template>
<script>
    import Result from './components/result.vue';
    import Controller from './components/controller.vue';
    import History from './components/history.vue';

    export default{
        components: {
            Result,
            Controller,
            History
        }
    }
</script>

componentsプロパティに読み込んだvueファイルを列挙してあげることで、各コンポーネントが有効になります。
こんな感じのHTMLが生成されれば、コンポーネントの注入が成功しています。

<div>
    <div>result</div>
    <div>controller</div>
    <div>history</div>
</div>

これでひとまずはVue.jsの枠組み、コンポーネントの作成が完了しました。
次からは機能的な実装に入っていきます。

そもそもビンゴゲーム

ルール説明

ここで一旦ビンゴゲームのルールを確認していきましょう。
今回は番号を出力するのみです。
出た番号を使ってカードをどうこうするといったことは考えず、数字を出力するというところだけに焦点を当てます。

ビンゴは1から75までの数字を使用します。
出力側は1から75までの数字をランダムに表示します。
すでに表示された数字は二度と表示しません。
これを全ての数字を使い切るまで行います。

Vue.js実装していく

vueファイルにHTMLを仮入れ

作成しておいた各コンポーネントのvueファイルにHTMLを入れていきます。

result.vue
<template>
    <div class="result">
        <div class="now-shuffle">
            シャッフル中
        </div>
        <div class="result-number">
            結果の数字
        </div>
    </div>
</template>
controller.vue
<template>
    <div class="controller">
        <button type="button">STOP</button>
        <button type="button">START</button>
    </div>
</template>
history.vue
<template>
    <ul>
        <li>履歴1</li>
        <li>履歴2</li>
        <li>履歴3</li>
    </ul>
</template>

これで下記のようなHTMLが出来上がるはずです。

<div>
    <div class="result">
        <div class="now-shuffle">
            シャッフル中
        </div>
        <div class="result-number">
            結果の数字
        </div>
    </div>
    <div class="controller">
        <button type="button">STOP</button>
        <button type="button">START</button>
    </div>
    <ul>
        <li>履歴1</li>
        <li>履歴2</li>
        <li>履歴3</li>
    </ul>
</div>

Vuex入れていくよ

仮のHTMlが入ったので、データのやりとりを作っていきます。
ここでVuexが登場します。
Vuexとは状態管理を行うライブラリです。
今回、Appという中にResult、Controller、Historyというコンポーネントを用意しましたが、これらの間でデータを共有するために使います。
グローバル変数を用いたり無理やり共有することも可能なのですが、このコンポーネントだけといったスコープを小さくする意味でもVuexを用いてデータをやり取りすることは有効です。

早速入れていきます。

npm install --save vuexでVuexをインストールしましょう。

インストールが完了したら、srcディレクトリの中にstoreというディレクトリとその中にindex.jsを作成しましょう。

そうしたらstore/index.jsを作成しVuexの記述を行っていきます。

index.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

Vue.useでVuexを引数にすることでVuexで管理するデータを注入することが可能になります。
そのstoreを入れる準備としてapp.jsを下記のように変更します。

app.js
import Vue from 'vue';
import App from './app.vue';
import store from './store'; // 追加 src/store/index.js

var app = new Vue({
    store, // 追加
    render: h => h(App)
}).$mount('#app');

とりあえず、簡単にデータを注入してみましょう。
index.jsに続いて記入します。

index.js
export default new Vuex.Store({
    state: {
        test: 1234
    }
});

stateプロパティを定義したオブジェクトをVuex.Storeの引数にします。
stateプロパティに状態を管理したいプロパティを定義することができます。

result.vueでVuexの状態を取り出してみましょう。

result.vue
<template>
    <div class="result">
        <div class="now-shuffle">
            シャッフル中
        </div>
        <div class="result-number">
            結果の数字 {{ resultNumber }}
        </div>
    </div>
</template>
<script>
    export default {
        computed: {
            resultNumber() {
                return this.$store.state.test;
            }
        }
    }
</script>

ここで算出プロパティと呼ばれるcomputedプロパティを使用します。
これは、無名関数で定義されることと、Vueで定義しているデータにアクセスすることができるので複雑な処理を返すのに適しています。
computedプロパティの中で、this.\$storeでルートのStoreにアクセスでき、this.$store.stateにそれぞれのプロパティをつけることでルートのstateの各プロパティにアクセスできます.

ここで管理する項目について整理しておきましょう。
以下となります。

  • ビンゴの残っている数字
  • ビンゴの止めた時の結果
  • 今まで過去に出た数字、履歴
  • 今、シャッフルしているか結果を表示している状態か

これらをstateとして登録するため、先ほどのStoreを書き換えしょう。

index.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        remainingNumber: allRange(75),
        resultNumber: 0,
        history: [1, 2, 3],
        nowShuffle: false
    }
});

/**
 * 1から引数に与えた数までの連番を格納した配列を返す
 * @param {number} num
 * @returns {[number]}
 */
function allRange(num) {
    if (typeof num !== 'number') {
        return null;
    }

    const args = [];
    for (let i = 1; i <= num; i++) {
        args.push(i);
    }

    return args;
}

ここは履歴は仮で[1,2,3]を入れておきます。
この状態で一度、vueファイルに埋め込んでいきましょう。

result.vue
<template>
    <div class="result">
        <div v-if="nowShuffle" class="now-shuffle">
            シャッフル中
        </div>
        <div v-else class="result-number">
            {{ resultNumber }}
        </div>
    </div>
</template>
<script>
    export default{
        computed: {
            nowShuffle(){
                return this.$store.state.nowShuffle;
            },
            resultNumber(){
                return this.$store.state.resultNumber;
            }
        }
    }
</script>

result.vueには、今シャッフルしているか(nowShuffle)と、結果の数字(resultNumber)を埋め込んでいます。
これを算出プロパティの中に入れることで、変更を監視し、変更されればそれに対応してHTMLの変更も行われます。

history.vue
<template>
    <ul>
        <li v-for="number in history">{{ number }}</li>
    </ul>
</template>
<script>
    export default{
        computed: {
            history(){
                return this.$store.state.history.concat().reverse();
            }
        }
    }
</script>

history.vueも同様に埋め込みます。
埋め込まれているのは、今まで出た過去の数字の履歴(history)です。
配列は後ろにそのままpushで入れるつもりですので、逆順に表示するようにしておきます。

controller.vue
<template>
    <div v-if="isOver" class="controller">
        <button v-if="nowShuffle" @click="stop" type="button">STOP</button>
        <button v-else @click="start" type="button">START</button>
    </div>
    <div v-else>
        <p>終了</p>
    </div>
</template>
<script>
    export default{
        methods: {
            start(){
                this.$store.commit('start');
            },
            stop(){
                this.$store.commit('stop');
            }
        },
        computed: {
            nowShuffle(){
                return this.$store.state.nowShuffle;
            },
            isOver(){
                return this.$store.state.remainingNumber.length > 0;
            }
        }
    }

</script>

controller.vueも同様に埋め込みます。
今シャッフルしているか(nowShuffle)、残りの数字(remainingNumber)の数から終了しているか判定しています。

また、ここでmethodsにthis.$store.commit('start')this.$store.commit('start')という処理があります。
これがVuexの状態を更新するメソッドで、ミューテーションというもので定義されます。
storeディレクトリに下記のmutations.jsを作成しましょう。

mutation.js
export default {
    start(state){
        if (!state.nowShuffle) {
            state.history.push(state.resultNumber);
        }
        state.nowShuffle = true;
    },
    stop (state){
        if (!state.nowShuffle) {
            return;
        }

        const length = state.remainingNumber.length;
        const i = Math.floor(Math.random() * length);
        state.resultNumber = state.remainingNumber[i];
        state.remainingNumber.splice(i, 1);
        state.nowShuffle = false;
    }
};

先ほどのcommitの引数の文字列が、このミューテーションのメソッドと結びつきます。
これで機能としては完了です。

最後に、Vuex.Storeに先ほどのmutationsを入れて、stateのhistoryを空配列に変えてあげれば完了です。

index.js
import Vue from 'vue';
import Vuex from 'vuex';
import mutations from './mutations';

Vue.use(Vuex);

export default new Vuex.Store({
    mutations,
    state: {
        remainingNumber: allRange(75),
        resultNumber: 0,
        history: [],
        nowShuffle: false
    }
});

これで、疑似的にビンゴゲームが動くはずです。

最後に

凄くざっくり触ってもらえたと思います。
Vue.jsは公式サイトのチュートリアルがとても充実しているので、一通り触ってみるといいと思います。
また、ここでは使用しませんでしたが、コンポーネント毎にCSSを定義できるcss-loaderや、SPAを作る手助けとなるvue-routerなどVue.jsの開発を便利にしてくれるツールは色々あります。
ぜひ試してみてください!