Vue.js + Vuexで開発をしてみよう!
蔀 凌太
こんにちは、フロントエンドエンジニアの蔀です。
ここ数年のフロントエンド開発の潮流の変化は急激で、雨後の筍のように色々なフレームワークが出てきていますね。
8月末には、Mediumでこんな記事が人気になりました。
The State Of JavaScript: Front-End Frameworks と銘打たれたこの記事は、React/Angular/Angular2/Ember/Vue/Backbone といった、近年流行しているJavaScriptフレームワークに関する興味、満足度、知名度などを調査して比較してくれています。
注目していただきたいのが、「Satisfaction(使ってみて満足したかどうか)」の項目です。
近年流行しているReact.jsと肩を並べ、Vue.jsが非常に高い満足度であることがわかります。
Vue.jsについては日本語ドキュメントも充実していますが、「実際に作ってみた」系記事が少ないのが実情です。 今日はこのVue.jsで簡単なアプリケーションを作ってみましょう!
環境
- Max OSX - 10.10.5以上
- node.js - v5.0.0以上
- npm - v3.0.0以上
node.jsやnpmの準備がまだの方はこちらからインストールしておいてください。
事前準備
以下の記事では、ES2015という新しいJavaScriptの文法を特に断りなく使用していきます。
全く最近のJavaScriptに触れてないよ!どうしよう!という方は、まずES2015の紹介を読んでおくとよいでしょう。
また、Vue.jsのチュートリアルをやってみてから、一通りVue.jsのドキュメントを読んでおくと、本記事の理解が進むと思います。
作るものの要件
GIF検索用のシンプルなWebアプリケーションを作ることにします。
- GIFの検索ができる(GIPHYのAPIを使用)
- 検索結果のGIFを並べて表示してくれる
ワイヤーフレームも書いてみました。
開発環境を整える
プロジェクトのひな形を作る
vue-cliをインストールしましょう。
vue-cliはVue.jsのプロジェクトに必要な構成を簡単にやってくれるツールです。
ローカルサーバーやビルドツールの設定を代わりにやってくれるのが最高です。
プロジェクト名はgiphy-vue
にしておきましょう。
1
2
3
4
5
6
7
8
9
# vue-cli のインストール(グローバル)
$ npm install -g vue-cli
# "webpack" を使ったプロジェクトのひな形を作ってくれます
# いくつかの質問が出てきますが、分からなければ全部Enterを押しても大丈夫です
$ vue init webpack gipfy-vue
# 依存関係をインストールしてgo!
$ cd giphy-vue
# 少し時間がかかるので、コーヒーなどを飲みながら待ちます
$ npm install
プロジェクトの中身の確認
さて、npm install
が終了したら、どんなものが入ったのかを確認しましょう。
src
ファイルの中身を見ると、なにやら見慣れない拡張子がありますね。App.vue
やHello.vue
というファイルが見えます。
この.vue
ファイルには、実現したいひとまとまりの機能を記述していき(これを コンポーネント と呼びます)、コンポーネントを組み合わせてアプリケーションを作っていきます。
作ってみよう
ヘッダーの作成
早速、ヘッダーを作ってみましょう。
src/components
に、Header.vue
というファイルを作ってください。
ファイルの中に、次のように記述していきます。
src/components/Header.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
<header class="mdl-layout__header">
<div class="mdl-layout__header-row">
<i class="material-icons">dehaze</i>
<span class="mdl-layout-title">GIPHY-VUE</span>
</div>
</header>
</div>
</template>
<style scoped>
.mdl-layout__header-row {
padding: 0 16px;
}
i {
padding-right: 16px
}
</style>
次に、このヘッダーコンポーネントを呼び出すため、src/App.vue
を編集していきます。
App.vue
は、このアプリケーション全体を包み込むコンポーネント、 トップレベルコンポーネント となります。
このコンポーネントに、色々な子コンポーネントを記述していくことでアプリケーションを作っていきます。
デフォルトで作られているApp.vue
の中身を空にしてから、次のように記述してください。
src/App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div id="app">
<!-- ここに先ほど作ったヘッダーを配置 -->
<vue-header></vue-header>
</div>
</template>
<script>
import VueHeader from './components/Header'
export default {
components: {
VueHeader
}
}
</script>
<script>
タグの中で、Header.vue
を呼び出しているのがわかると思います。
その下にあるのは普通のJavaScriptオブジェクトですね。このオブジェクトのcomponents
にコンポーネントを登録すると、<vue-header>
のように、HTMLの中でタグとして呼び出せるようになります。
W3C標準に従うため、キャメルケースのコンポーネントのタグ名は必ずvue-component-name
のように、ケバブケースで書きましょう。
App.vue
を保存したら、npm run dev
と入力してローカルサーバを立ち上げます。
http://localhost:8080
を見ると、次のようにヘッダーが表示されているはずです。
リロードや再ビルドしなくても変更が反映されるように、vue-cli
がローカルサーバーを設定してくれています。便利ですね。
(CSSはMaterial Design Liteを使っています)
必要なコンポーネントを用意する
同じような感じで、アプリケーションに必要なパーツを作っていきましょう。
作成するのは、
- Card.vue - gifを表示するカード
- Search.vue - 検索フォーム
- TimeLine.vue - Card.vueを並べて表示する部分
といったところでしょう。
ディレクトリ構成はこのような感じになります。
src/page1/Card.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div class="gif mdl-card mdl-shadow--2dp">
<div class="mdl-card__title mdl-card--expand"></div>
<div class="mdl-card__actions">
<span class="filename">Title</span>
</div>
</div>
</template>
<style>
.gif {
margin: 0 auto;
width: 60%;
display: flex;
justify-content:space-between;
margin-bottom: 60px;
}
.gif.mdl-card {
width: 256px;
height: 256px;
}
.gif> .mdl-card__actions {
height: 52px;
padding: 16px;
background: rgba(0, 0, 0, 0.2);
}
.filename {
color: #fff;
font-size: 14px;
font-weight: 500;
}
</style>
src/page1/Search.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="mdl-textfield mdl-js-textfield">
<input class="mdl-textfield__input" type="text" id="sample1">
<i class="material-icons">search</i>
</div>
</template>
<style media="screen">
.mdl-textfield {
padding: 0;
background-color: #f6f6f6;
margin: 30px auto;
width: 60%;
display: flex;
margin-bottom: 60px;
}
</style>
src/page1/TimeLine.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<main class="mdl-layout__content">
<div class="page-content">
<search></search>
<card v-for="n in 10"></card>
</div>
</main>
</template>
<script>
import Search from './Search'
import Card from './Card'
export default {
components: {
Search,
Card
}
}
</script>
<style>
main {
background-color: #f6f6f6;
width: 100%;
margin: 0 auto;
}
.page-content {
display: flex;
flex-direction: column;
}
</style>
これでコンポーネントの準備は完了です。
これらのコンポーネントを、App.vue
で呼び出します。
src/App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div id="app">
<vue-header></vue-header>
<time-line></time-line>
</div>
</template>
<script>
import VueHeader from './components/Header'
import TimeLine from './components/page1/TimeLine'
export default {
components: {
VueHeader,
TimeLine
}
}
</script>
Vuexを使ってみる
アプリケーションのデータフロー
さて、うまくいけば、アプリケーションの見た目は以下のようになっているはずです。
まだ普通の静的なページですので、JavaScriptのロジックを追加し、GIPHYのAPIを呼び、返ってきたデータを表示できるようにしたいと思います。
この状態から、
- GIFの検索ができる
- 検索結果のGIFを並べて表示してくれる
ためには、どうすればよいでしょうか?
- 検索ボックスに入力した文字列をAPIに渡す
- APIで返ってきた値の数だけ、GIFを
Card.vue
に渡して表示する(v-for
でループさせる)
おそらくこういったことをしないといけませんが、ちょっとめんどくさそうです。
そもそも、上記3項目の「やりたいこと」を実現するために、
- 検索キーワード
- 検索結果
といった「データ」を、コンポーネント間でやり取りし、色々と処理しなければなりません。
その処理についていちいち「あれをこうして、これをこうして……」とバトンリレーのように書いていくのは大変ですよね。
そこで登場するのがVuexです。
VuexはFluxの実装の1つ(正確にはReduxに似た実装)になります。
Fluxについては漫画で説明するFluxが分かりやすいでしょう。
Vuexは開発しながら理解していきましょう。まずはVuexのセットアップをしてみます。
Vuexのセットアップ
Vuexをインストールしましょう。
Vuexは現在2.0系がrc版で公開されています。
1.0系とは一部書き方が異なりますが、今後は2.0系が主流となると思われますので、こちらを使用してみます。
(今後、Vuexを使って開発をする場合は、リリースノートを見て最新版をインストールするようにしてください!)
1
$ npm i --save vuex@^2.0.0-rc.5
インストールしたら、src/main.js
を次のように編集します。
src/main.js
1
2
3
4
5
6
7
8
import store from './vuex/store'
/* eslint-disable no-new */
new Vue({
store,
el: 'body',
components: { App }
})
Vueインスタンスにstore
を渡していますね。
まだ./vuex/store
は存在していないので、この時点ではエラーとなります。
新たにsrc/vuex
というディレクトリをつくり、store.js
というファイルをつくりましょう。
src/vuex/store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
const state = {}
const actions = {}
const getters = {}
const mutations = {}
export default new Vuex.Store({
state,
getters,
actions,
mutations
})
Vuexとは?
公式Documentは、Vuexを上の図で説明していますね。
- Vue Components(アプリケーションの実体)
- Actions(ユーザの操作/APIとのやりとり)
- Mutations(状態への変更処理)
- State(状態)
という要素が矢印で繋がれ、データがその間を循環しているのがわかります。
大事なのはこれが一方向の循環だということです。
一方向の循環とすることで、データの流れが簡潔になり、開発しやすくなります。
開発者同士で会話する際に、お互いいつも同じ「循環のイメージ」が思い浮かびますから、コミュニケーションコストが下がります。
(その他にも、Pure functionによる副作用の削減やSingle source of truthの原則、Time travel機能など、色々な論点があるのですが、Vuex固有の話というより、Flux実装にまつわる話題なので割愛させていただきます。興味がある方はこちらを読むと良いでしょう。)
そして、先ほど作ったstore.js
内のstate
, actions
, getters
, mutations
という変数は、この循環に対応しています。
このVuexが描くサイクルを実装していくと、わかりやすく拡張しやすいアプリケーションが開発できるというわけです。
さっそくこのサイクルに沿って、GIPHY-VUEを作っていきましょう。
Vuexを装着する
stateを決める
まずは基本となるStateの定義からです。
Stateは、アプリケーション全体の情報を管理する場所です。
アプリケーションは、このStateの情報を「唯一の情報源: Single source of truth」として利用し、見た目の構築やサーバとの通信などに利用します。
GIPHY-VUEでは、検索キーワードと、検索結果を状態(State)として保存しておきたいですね。
src/vuex/store.js
1
2
3
4
const state = {
keyword: '',
gifs: []
}
mutation typeを作る
次に、アプリケーション内でユーザーが行なうであろう操作について記述していきます。
いきなりActionsを作る前に、新しく、src/vuex/mutation-types.js
というファイルを作りましょう。
名前が示す通り、state
をmutate(変更)する操作のタイプを記述します。
ユーザーがする操作は、
- 検索ワードを書き換える
- 検索する
といったところでしょう。
src/vuex/mutation-types.js
1
2
export const CHANGE_KEYWORD = 'CHANGE_KEYWORD'
export const SEARCH = 'SEARCH'
わざわざString
型を同じ文字列の変数に代入して、さらにexport
していますね。
ユーザーの操作のタイプは、アプリケーション内のいたるところで参照するので、
変数にしておいたほうが使い勝手が良かったり、lintチェックの補助を受けられたりという利点があります。
これを、store.js
内で呼び出します。
src/vuex/store.js
1
2
3
4
import {
CHANGE_KEYWORD,
SEARCH
} from './mutation-types'
actionsを作る
アプリケーションの状態を変更するようなユーザからの入力や外部APIの呼び出しは、Actionsと呼ばれます。
store.js
のactions
を次のように変更します。
src/vuex/store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function getGIFs (query) {
const params = encodeURIComponent(query).replace(/%20/g, '+')
return fetch('http://api.giphy.com/v1/gifs/search?q=' + params + '&api_key=dc6zaTOxFJmzC')
.then(res => res.json())
}
const actions = {
[CHANGE_KEYWORD] ({ commit }, keyword) {
commit(CHANGE_KEYWORD, keyword)
},
[SEARCH] ({ commit, state }) {
getGIFs(state.keyword)
.then(data => {
commit(SEARCH, data)
})
}
}
ES2015の書き方が使われていますので注意してください。
先ほど記述したCHANGE_KEYWORD
が早速登場しています。メソッド名に使われていますね。
第2引数にはkeyword
という変数が登場しています。何らかの方法でこのメソッドにkeyword
を渡してやる必要がありそうです。
また、第1引数には、commit()
という謎のメソッドが分割代入(ES2015)されていますね。
これは「変更をアプリケーションの状態にcommitする」、つまり、先ほど書いたstate
を書き換えることを示しています。
つまり、Mutationsのことですね。
SEARCH
というActionsの中では、GIPHYのAPIを呼び、検索結果をdataという変数にしてcommit()
に渡しています。
getGIFs()
という関数内ではGIPHYの開発者用公開APIキーを利用しています。
mutationsを作る
同じように、store.js
の中に次のように記述します。
src/vuex/store.js
1
2
3
4
5
6
7
8
const mutations = {
[CHANGE_KEYWORD] (state, keyword) {
state.keyword = keyword
},
[SEARCH] (state, gifs) {
state.gifs = gifs.data
}
}
3行目を見ると、state.keyword = keyword
という形で、Stateを変更していることがわかります。
Vue Componentsの編集
あとは、Vuexの循環の最後の要素、Vue Componentsの編集をしていきます。
検索フォームの編集
src/components/page1/Search.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="mdl-textfield mdl-js-textfield">
<input @change="CHANGE_KEYWORD($event.target.value)" class="mdl-textfield__input" type="text" id="sample1">
<i @click="SEARCH" class="material-icons">search</i>
</div>
</template>
<script>
import { mapActions } from 'vuex'
import { CHANGE_KEYWORD, SEARCH } from '../../vuex/mutation-types'
export default {
methods: {
...mapActions([
CHANGE_KEYWORD, SEARCH
])
}
}
</script>
2行目と4行目を見ると、@change
, @click
というVue.jsのイベントハンドラにCHANGE_KEYWORD
, SEARCH
というメソッドが登録されています。mutation-types.js
で定義した変数ですね。
この変数は、同時に、Actionsのメソッド名にもなっていたことを思い出してください。
14行目を見ると、mapActions()
というメソッドでCHANGE_KEYWORD
, SEARCH
を使っているのがわかります。
これは、store.js
内で定義したActionsをコンポーネントで呼び出せるようにしてくれるメソッドです。
つまり、@change="CHANGE_KEYWORD($event.target.value)"
は、「change
イベントが発行されると、CHANGE_KEYWORD()
というActionsに、検索フォームの中身を渡す」という意味になります。
CHANGE_KEYWORD()
の内容は以下のとおりでしたね。
src/vuex/store.js
1
2
3
4
const actions = {
[CHANGE_KEYWORD] ({ commit }, keyword) {
commit(CHANGE_KEYWORD, keyword)
}
ここで、Actionsは、Mutationsを次に呼んでいたことを思い出してください。Actionsの引数として渡された$event.target.value
は、同じメソッド名(CHANGE_KEYWORD
)のMutationsによって、state.keywordに代入されることになります。
src/vuex/store.js
1
2
3
4
const mutations = {
[CHANGE_KEYWORD] (state, keyword) {
state.keyword = keyword
},
こうして、「ユーザーの操作の結果」を、Stateに保存することが出来ました。
あとは、検索ボタン(虫眼鏡のマーク)をクリックすると、SEARCH
というActionsが呼ばれ、GIPHYのAPIから得られた検索結果がMutationsによって、同様にStateに保存されることになります。
すべてVuexのサイクルに沿った同じ流れなのでわかりやすいですね。
検索結果の表示
さて、Stateに保存した検索結果をアプリケーション側に表示する必要がありますね。
Stateの値をコンポーネントが取得するためには、Gettersというものを使います。
store.js
にGettersを付け加えましょう。
src/vuex/store.js
1
2
3
const getters = {
gifs: state => state.gifs
}
ここではgifs
というメソッドを定義しています。state.gifs
を返してくれます。
では、このGettersはどこで呼ぶのでしょうか?
src/components/TimeLine.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<main class="mdl-layout__content">
<div class="page-content">
<search></search>
<card
v-for="gif in gifs"
:gif="gif">
</card>
</div>
</main>
</template>
<script>
import Search from './Search'
import Card from './Card'
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['gifs'])
},
components: {
Search,
Card
}
}
</script>
19行目に注目してください。computedというプロパティの中に、mapGettes()
が登場しています。
先ほどのmapActions()
と同じように、gettersをこのコンポーネントから使えるようにしてくれています。
6行目を見てください。v-for
の中でgifs
が使われています。これが先ほど定義したGettersですね。
gifs
は検索結果の配列を返すので、forループで配列の要素をひとつずつpropsにしてCard.vue
に渡しているということになります。
つまり、Card.vue
では、propsで受け取ったgifの情報を表示してやるだけ、ということですね!
src/components/Card.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="gif mdl-card mdl-shadow--2dp">
<div class="mdl-card__title mdl-card--expand">
<img :src="gif.images.fixed_height.url" alt="" />
</div>
<div class="mdl-card__actions">
<span class="filename">{{gif.slug}}</span>
<i class="material-icons">favorite</i>
</div>
</div>
</template>
<script>
export default {
props: {
gif: Object
}
}
</script>
Card.vue
の15行目を見ると、propsで、gif
という値を受け取っています。これを4行目のimgタグのsrc
要素で使っています。
GIPHYのデータ構造通りにプロパティを指定し、表示したいGIFのURLを渡してやりましょう。
完成
うまく行けば、このように検索ができるようになっているはずです! 見たいGIFをガンガン検索しましょう。
今回作ったアプリケーションは検索と閲覧機能だけの非常にシンプルな作りですが、
- ページのルーティング機能をつけてSPA(Single Page Application)にする
- お気に入り登録できるようにする
- 別ページでお気に入りのGIFを閲覧できるようにする
などといったことも、Vue.js + Vuexを使えば非常に簡単に実現できます! 基本的に頭のなかに思い描くことは、Vuexのあの「循環」だけで大丈夫です。
例えばお気に入り登録とその閲覧であれば、
- ハートをユーザがクリック
- Actionsがコンポーネントから呼ばれる
- Mutationsが
state.favorites
にgifを追加 - 別ページのコンポーネントのGettersで
state.favorites
を取得して表示
という流れになります。先ほどの検索の流れとだいたい同じですね? アプリケーションが大規模になればなるほど、Vuexの恩恵を感じることでしょう!
是非、Vuexにトライしてみてください!