エンジニアのタケヨシです。
名前だけでも覚えて帰ってください。
この記事は JavaScript Advent Calendar 2016 19日目の記事です。
普段からJSはjQueryを使って書いてはいるのですが、近年のフレームワーク、ライブラリには全然触れていなかったので、今年は少しづつでもいいから触ってみようと思いBackbone.js,Angular2,Vue.jsなどのチュートリアルをやって現在も勉強中です。
そこで今回は勉強も兼ねて、シンプルなじゃんけんゲームを作ってみたので、作成したものの紹介とWebサイト制作のJSとの違いなど感じたことをまとめてみました。
今回はVue.js v2.0で作ってみました。
Vue.js v2.0は10/1リリースされたようです。自分は1にも触れていなかったのですが、公式サイトを見たところ既にドキュメントが日本語なので、学ぶハードルの1つである英語という部分がなかったので選択しました。
これは余談ですが、Vue.jsのプロダクト名にはアニメの作品名が利用されているようで、 1.0.0はエヴァンゲリオン(Evangelion)、 2.0.0はGhost in the Shell(攻殻機動隊)と名前がついているようですよ。間にも色々ついているのでしょうか。グレンラガンとかはないんですかね。Vってドリルみたいな形してますし。あとはコンバトラーVとかですかね。
Vue.jsについて
Vue (発音は / v j u ː / 、 view と同様)はユーザーインターフェイスを構築するためのプログレッシブフレームワークです。他の一枚板(モノリシック: monolithic)なフレームワークとは異なり、Vue は初めから少しづつ適用していけるように設計されています。中核となるライブラリは view 層だけに焦点を当てているため、Vue.js を使い始めたり、他のライブラリや既存のプロジェクトに統合することはとても簡単です。一方、モダンなツールやサポートライブラリと併せて利用することで、洗練されたシングルページアプリケーションを開発することも可能です。
はじめに – vue.js
と公式であるとおり、開発環境も、コンパイル系も利用しないで、CDNで読み込むだけで利用するというやりかたもできるようになっています。近年のフレームワーク界隈でコンパイル前提で進んでいるイメージがあるため、Webサイト制作の人間からするとこういった読み込むだけで利用できるというのはとても嬉しい仕組みです。
じゃんけんげーむ
実際の動作サンプルはこちらから
※最新版Chrome,Firefoxで動作確認しています。2016/12/19現在
ページの概要
- 画面は2つ
- ゲーム画面
- 成績確認画面
- 画面の遷移はリンクで変更(ルーティング)
- データの保存はlocalstrageを利用
環境の準備
まずは、Vue.jsの準備を進めていきます。vue.jsにはvue-cliという簡単にVuejsの開発環境を構築できるツールがあるので、利用してみようと思います。
vue-cliのインストールはコンソール(ターミナル)で npm install vue-cli -g と打ってグローバル環境にインストールします。インストールが上手く行って、環境変数が通っていれば、vue というコマンドが利用できるようになったかと思います。vue -V と入力して確認して見ましょう。上手く言っていればバージョンが表示されるかと思います。
vue-cliでテンプレート生成
vue-cliではwebpack, browserifyなど、普段使っているバンドラーに合わせたテンプレートを利用することができます。各バンドラーにもテスト、リントなどが含まれたできるバージョンと最低限のテンプレートのsimpleがあります。また、対話式で1つずつ確認が行なわれるので、必要に応じて選択して利用することが可能です。
今回はbrowserify-simpleを利用してみました。このbrowserify-simpleではvue-routerは入っていないので、追加でインストールします。以下がコマンドラインの流れとなります。
npm install vue-cli -g // vue-cliをインストール vue init browserify-simple janken // vue-cliで生成 cd janken // 生成されたディレクトリへ移動 npm install // node_modulesをインストール npm install vue-router -S // vue-routerをインストール npm run dev // ここのコマンドは作業をするt
これで準備が完了です。上手くいった場合にjankenディレクトリ以下は以下のようになります。
│ .babelrc │ .gitignore │ index.html │ package.json │ README.md ├─node_modules ├─dist │ .gitkeep │ └─src // ソースはここに書いていきます。 App.vue main.js
メイン画面の作成
jsは全部で以下の5つになります。
│ App.vue //メイン画面 │ main.js // App.vue を呼び出したり、ルーターの設定をするファイル。 │ ├─components │ Game.vue // ゲーム画面 │ Score.vue // 成績画面 │ ├─util │ Storage.js // localstorageを扱うクラス
まずはApp内にgameとscoreのコンポーネントを作成していきます。Vue.jsでは単一のコンポーネントファイルは.vue拡張子で作成していきます。
App.vueの作成
vue-cliでテンプレートを作成するとindex.htmlでjsファイルの読み込みなどは終わっていたため、 基本的にindex.htmlはいじることはありません。(リセットCSSなどがあれば、追記する必要があります。) main.jsも同様で、この後作成するApp.vueを呼び出す設定が行なわれていたので、 今の段階では修正しません。最後のルーター処理で変更を加えるくらいでした。
ですので、変更、作成していくのはApp.vueや新規で作成するファイルがメインとなります。
前置きが長くなりましたが、App.vueのコードです。
<template> <div id="app"> <h1>じゃんけんゲーム</h1> <div class="inner"> <game scores="scores"></game> <score scores="scores"></score> </div> </div> </template> <script> import Game from './components/Game.vue'; import Score from './components/Score.vue'; import Storage from './util/Storage'; let storage = new Storage(); export default { name: 'app', data () { return { scores: storage.getData('scores') || [] }; }, watch: { scores : 'saveData' }, components:{ Game, Score }, methods: { saveData() { storage.setData('scores', this.scores); } } }; </script> <style scoped> /* css(省略)*/ </style>
templateの中身が実際に表示されるエリアです。その中に<game>と<score>という独自のタグがありますが、 これがこの後作成するgameとscoreの部品の独自タグになります。vue.jsではこういった自分で定義した、カスタム要素を利用することができます。
こういったカプセル化された部品単位はコンポーネントと呼ばれています。このあたりの概念的な考え方は重要なので、ぜひ公式ドキュメントを読んでいただければと思います。
このApp.vue自体も1つのコンポーネントです。
続いて、scriptタグ内にはそのコンポーネントに関する詳細が書かれています。利用するライブラリや、コンポーネントがある場合は、importやrequireなどで読み込んでおきます。
export default {} がコンポーネントのオプションの部分になります。nameにはtemplate内に書かれた基点の要素のid属性の値を指定します。(ここではapp) また、コンポーネントを利用するためにcomponentsオプションに登録します。dataにはプロパティ、watchは関したプロパティに変化があったときにアクションを起こす設定、 methodsはその名の通り関数などを登録していきます。
ここに書かれたscoresはこの後のゲーム、スコア部分でも共通して利用するプロパティとなります。
この流れで続いてはgame画面を作成していきます。
game画面の作成
続いてはgame画面の作成です。Game.vueというファイルを作成します。コンポーネントはcomponentsというディレクトリに入れます。
<template> <div id="game"> <div v-if="resultMessage" class="result"> <h2>{{ resultMessage }}</h2> <div><button v-on:click="start">もういちど</button></div> </div> <div class="imgArea"><img v-bind:src="src" alt=""></div> <ul> <li> <button v-on:click="onSelected" class="button" type="button" value="0">グー</button> </li> <li> <button v-on:click="onSelected" class="button" type="button" value="1">チョキ</button> </li> <li> <button v-on:click="onSelected" class="button" type="button" value="2">パー</button> </li> </ul> </div> </template> <script> import * as babel from 'babel-core'; import Storage from '../util/Storage'; let storage = new Storage(); export default { name: 'game', props: ['scores'], data() { return { src : 'dist/imgs/choki.png', imgList: [ 'dist/imgs/gu.png', 'dist/imgs/choki.png', 'dist/imgs/par.png' ], timer: null, resultMessage: '' } }, created () { this.start(); }, methods: { changeImg(number) { // 画像の切替 if(number && Math.abs(number) <= this.imgList.length) { this.src = this.imgList[number]; } else { var num = Math.floor(Math.random() * this.imgList.length); this.src = this.imgList[num]; } }, start () { this.reset(); this.timer = setInterval(() => { this.changeImg(); }, (1000 / 12)); }, onSelected(e) { clearInterval(this.timer); let button = e.target; let resultNum = parseInt(this.imgList.indexOf(this.src), 10); let selectNum = parseInt(button.value, 10); let decision = this.decisionJanken(selectNum, resultNum); var btns = document.querySelectorAll('.button'); for( let btn of btns ) { btn.setAttribute('disabled', true); } if(decision == 1) { this.resultMessage = 'かち'; } else if (decision == 2){ this.resultMessage = 'ひきわけ'; } else { this.resultMessage = 'まけー'; } this.$parent.$data.scores.push({ message: this.resultMessage }); button.classList.add('is-selected'); }, reset() { var btns = document.querySelectorAll('.button'); for( let btn of btns ) { btn.removeAttribute('disabled'); btn.classList.remove('is-selected'); } this.resultMessage = ''; }, decisionJanken(myHand, youHand) { let result = 0; // 0 は負け, 1は勝ち,2は引き分け switch(myHand) { // じゃんけんの判定 } return result; } } } </script> <style scoped> /* CSS 省略 */ </style>
game内のtemplateではいくつかテンプレート用の構文を利用しています。
コンポーネント内でのデータの紐付け
vue.jsではscirpt内に書かれたプロパティやメソッドをひもづけるための記述があります。
- データの出力
- テキストなど出力したい場合は{{ プロパティ名 }}と書くだけです。{{}}内では関数を利用することも可能です。
- 属性
- 属性については<img v-bind:属性名=プロパティ /> で行なうことができます。
- イベント
- 属性については<button v-on:メソッド名 /> で行なうことができます。
属性やイベントには省略記法もありますので、詳しくはテンプレート構文 – vue.jsをご覧ください。
vue.jsの機能としてコンポーネントのプロパティに変化が起きると、テンプレート上での出力も自動的に変更されます。この機能を使ってじゃんけんの画像の切替を行なっています。
あとはボタンにもscript内で作成したイベントハンドラを設定をしているので、クリックしたら判定が行なわれるという流れになっております。
結果の部分は判定時にのみ出力を行いたいのでv-ifで制御しています。v-if=プロパティという書き方になりますが、比較するプロパティがtrueの場合、要素が表示されるようになっています。
成績画面
次は成績画面です。
<template> <div id="score"> <h2>せいせき</h2> <div><button type="button" v-on:click.prevent="dataReset">クリア</button></div> <ul v-if="scores"> <li v-for="(score, i) of $parent.$data.scores"> <span>{{ (i + 1) }}:</span> {{ score.message }} </li> </ul> </div> </template> <script> export default { name: 'score', props: ['scores'], data() { return {} }, methods: { dataReset() { this.$parent.$data.scores = []; } } }; </script> <style scoped> /* CSS省略 */ </style>
成績画面は成績の表示と履歴消去機能を持たせています。
親の持っているscoresのデータを繰り返し処理で表示させています。テンプレート内でv-forを利用すると繰り返し処理を行なうことができます。
ルーティング
実際にはここだけでも完結したのですが、 ルーティング処理も試してみたかったので、画面分けてみました。
vueでルーティングを行なう際にはvue-routerというのが公式でサポートされています。
ただ、サードパーティのライブラリを利用することもできるようですので、お好みに合わせて利用してくださいませ。
main.jsとApp.vueの変更
ルーティングする際にはルートの設定を記述する必要があります。設定する内容はmain.jsに記述していきます。
import Vue from 'vue' import VueRouter from 'vue-router'; // 追記 import App from './App.vue' import Game from './components/Game.vue'; // 追記 import Score from './components/Score.vue'; // 追記 // ルータ設定 追記 Vue.use(VueRouter); const router = new VueRouter({ routes: [ { path: '/game', component: Game }, { path: '/score', component: Score }, { path: '*', redirect: '/game' } ] }); new Vue({ el: '#app', router, // 追記 render: h => h(App) });
まず、Vue.useでルーターを使うことを宣言します。次にルーターインスタンスを作成しました。
routesプロパティ内でpathごとの設定を書きます。urlが /path名 になった場合にcomponentに指定したコンポーネントが呼び出されます。
「*」は先に設定したどのpathにも当てはまらなかった場合、またはその他のpathだった場合の処理ですが、 今回はgame画面にリダイレクトするようにしています
続いてpathが合致した場合にコンポーネントが出力されるようApp.vueを修正していきます。
// App.vue <template> <div id="app"> <h1>じゃんけんゲーム</h1> <ul class="nav"> <li><router-link to="/game">ゲーム</router-link></li> <li><router-link to="/score">せいせき</router-link></li> </ul> <div class="inner"> <transition name="fade"> <router-view scores="scores"></router-view> </transition> </div> </div> </template>
gameとscoreのタグをrouter-viewというのに置き換えました。合致したpathのcomponentはrouter-viewというタグの箇所に表示されます。これはものすごく分かりやすく感じました。
指定したcomponent -> router-viewに表示されるという感じです。
最後に各画面にリンクできるようにリンクを設置します。リンクはaタグではなく、router-linkタグというのを利用します。>router-link to="path名"<テキスト>/router-link< 実際に表示される際にaタグに置き換わって表示されるようです。(aタグ以外のタグを指定することも可能です)
これでじゃんけんゲームが完成しました。
実際の動作サンプルはこちらから
※最新版Chrome,Firefoxで動作確認しています。2016/12/19現在
transitionのタグなど部分部分省略しておりますが、これにてゲームの作成は終了しました。ゲームは駄菓子屋に入ったときの気持ちで試してみてもらえると嬉しいです。
Webサイト制作との違い
ここからは感想です。
サイトにもよりますが、静的のWebサイトでは今回のような動的に何かを変更することはあまりなく、また便利だと言って、安易に導入すれば、逆にオーバースペックになるだろうと感じました。加えて、知識のない状態ではSEOやアクセシビリティの面でも不安が多く残ります。
しかし、そういった面がクリアされていれば、通常のWebサイトでも利用はできるんじゃないかなと思いました。
それに今回のゲーム程度あれば、みたいなことをjQueryでやることも可能でしょうが、jQueryで作るより短く、直感的に作成することができました。
Webサイト制作の現場ではjQueryの方が主流ですのでまだまだjQueryは必須ですが、選択肢を増やすためにやっておきたいのと、ライブラリの思想などもあり、確実に学びはあると思います。
環境についてもvue-cliのように環境を簡単にセットアップできるので、環境構築に苦しまず始められますしね。※当たり前ですが、知識なくやると後々苦しむことになるので、一応環境構築も勉強したほうがいいです。
攻殻機動隊で「さてどこへ行こうかしら。ネットは広大だわ」というセリフが出てきますが、フロントエンドに限らず、ネットの世界には自分の知らない知見はまだまだありますし、どんどん進化していきますから少しずつでもついていけるよう頑張っていきたいところですね。