More than 1 year has passed since last update.

vue-cli環境構築資料
https://gist.github.com/bora-apo/4f9b25e3631818a32077a0a912402ac5#file-vue-cli-build-md

第1回Vue.js勉強会資料
https://gist.github.com/bora-apo/4f9b25e3631818a32077a0a912402ac5#file-vue_stady1-md

この資料は2017/4/25に社内で開催した、第2回Vue.js勉強会の資料です。
今回はVue.jsと一緒に使われるライブラリ「Vuex」について試してみます。

vuex

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。
https://vuex.vuejs.org/ja/intro.html

状態管理パターンとは

vuex勉強会_1-01.png

https://jsfiddle.net/pocchi/ttw0u72L/

vuex勉強会_1-02.png

3つの役割に分けることができる。
一方向の流れで管理がシンプルだが、複数のコンポーネントで情報源を共有しようとすると値が重複し、管理が面倒になる。

vuex勉強会_1-03.png

そこで、情報源を1つに集約しようとするのが Vuexです。

今回は簡単な入力フォームを作成します。
vue.jsを使わなくても作れますが、練習用に作成します。

画面の流れ

vuex勉強会-03.png

vuex勉強会-04.png

画面の作り

vuex勉強会-05.png

  • h1のヘッダは使いまわせそう。
  • エラー文は出しわけが発生する
  • テキストエリアと確認文の切り替え
  • ボタンの文字も2種類文字がある

作成の方法

vuex勉強会.png

これを1コンポーネントで作成します。

準備とか

まず、vuexを使用するので、インストールします。
npm install --save vuex
スクリーンショット 2017-04-23 0.30.00.png

スクリーンショット 2017-04-22 20.22.36.png

npm run dev
で立ち上がったデフォルトの画面です。こんな感じになっているかと思います。

src/component/Hello.vue

デフォルトの画面を作っているのがここ。

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <h2>Essential Links</h2>
    <ul>
      <li><a href="https://vuejs.org" target="_blank">Core Docs</a></li>
      <li><a href="https://forum.vuejs.org" target="_blank">Forum</a></li>
      <li><a href="https://gitter.im/vuejs/vue" target="_blank">Gitter Chat</a></li>
      <li><a href="https://twitter.com/vuejs" target="_blank">Twitter</a></li>
      <br>
      <li><a href="http://vuejs-templates.github.io/webpack/" target="_blank">Docs for This Template</a></li>
    </ul>
    <h2>Ecosystem</h2>
    <ul>
      <li><a href="http://router.vuejs.org/" target="_blank">vue-router</a></li>
      <li><a href="http://vuex.vuejs.org/" target="_blank">vuex</a></li>
      <li><a href="http://vue-loader.vuejs.org/" target="_blank">vue-loader</a></li>
      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'hello',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}

ul {
  list-style-type: none;
  padding: 0;
}

li {
  display: inline-block;
  margin: 0 10px;
}

a {
  color: #42b983;
}
</style>

これをコピーして進めていきます。
src/components/Hello.vueをコピーします。
src/components/Form.vueという名前にしましょう。

src/components/Form.vue

必要な部分を残して消します。
<template></template>の中身を削除しちゃいましょう!
適当に書き換えてみます。

<template>
-  <div class="hello">
-    <h1>{{ msg }}</h1>
-    <h2>Essential Links</h2>
-    <ul>
-      <li><a href="https://vuejs.org" target="_blank">Core Docs</a></li>
-      <li><a href="https://forum.vuejs.org" target="_blank">Forum</a></li>
-      <li><a href="https://gitter.im/vuejs/vue" target="_blank">Gitter -Chat</a></li>
-      <li><a href="https://twitter.com/vuejs" target="_blank">Twitter</a>--</li>
-      <br>
-      <li><a href="http://vuejs-templates.github.io/webpack/" target="_blank">Docs for This Template</a></li>
-    </ul>
-    <h2>Ecosystem</h2>
-    <ul>
-      <li><a href="http://router.vuejs.org/" target="_blank">vue-router</a></li>
-      <li><a href="http://vuex.vuejs.org/" target="_blank">vuex</a></li>
-      <li><a href="http://vue-loader.vuejs.org/" target="_blank">vue-loader</a></li>
-      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>
-    </ul>
-  </div>
+ <div>Formページ</div> 
</template>

<script>
export default {
-  name: 'hello',
+  name: 'form',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  }
}
</script>

このForm.vueを大元のコンポーネントとして使用していきます。
しかし、これだけでは表示はまだHello.vueのままです。
ルーティングを書き換えます。

src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
- import Hello from '@/components/Hello' //コンポーネントを読み込む
+ import Form from '@/components/Form'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/', //パスを指定
-      name: 'Hello',
-      component: Hello // 上で読み込んだコンポーネントを指定 
+      name: 'Form',
+      component: Form 
    }
  ]
})

書き換えると、こうなります。
ちなみに、ページリロードは自動でしてくれるので、更新をする必要はありません。

スクリーンショット 2017-04-22 22.54.23.png

ロゴも残ってしまっているので、削除しましょう。

src/App.vue

<img src="./assets/logo.png">を削除します

<template>
  <div id="app">
-   <img src="./assets/logo.png">
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

これで消えました。
ここまでは以下にコードがあります。
https://bitbucket.org/Pocchi/vuex-study/commits/f518b6320d14799716e71146e93995e183f797a1

モジュールを作成します

今回はわかりやすいようにcomponents/modules/ディレクトリを作成します。 その下に以下のファイルを作っていきます。

  • modules/HeadComp.vue
  • modules/TextareaComp.vue
  • modules/StringComp.vue

src/components/Form.vueをコピーして、それぞれの名前をつけておきましょう!
(src/components/Form.vueはそのまま!消しちゃだめ)

src/components/moduls/HeadComp.vue

<template>
- <div>Formページ</div>
+ <h1>{{title}}</h1>  
</template>

<script>
export default {
- name: 'form',
+ name: 'headComp',
  data () {
    return {
-     msg: 'Welcome to Your Vue.js App'
+     title: '感想を入力'
    }
  }
}
</script>

HeadComp.vueをFormに登録します

src/components/Form.vue

<template>
- <div>Formページ</div>
+  <div>
+    Formページ
+    <HeadComp></HeadComp>
+  </div>

</template>

<script>
+ import HeadComp from '@/components/modules/HeadComp'
export default {
  name: 'form',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
- }
+ },
+ components: {
+   HeadComp
+ }
}
</script>

こうなっているはず!
スクリーンショット 2017-04-22 23.49.29.png

[コード]
https://bitbucket.org/Pocchi/vuex-study/commits/3ea694640dd813364578f1a9fe22f89b9f195787

src/components/modules/TextareaComp.vue

 <template>
-  <div>Formページ</div>
+  <div>
+    <p class="error">{{error}}</p>
+    <textarea></textarea>
+  </div>
 </template>

 <script>
 export default {
-  name: 'form',
+  name: 'textareaComp',
   data () {
     return {
-      msg: 'Welcome to Your Vue.js App'
+      error: '入力は必須です'
     }
   }
 }
</script>

 <!-- Add "scoped" attribute to limit CSS to this component only -->
 <style scoped>
-h1, h2 {
-  font-weight: normal;
-}
-
-ul {
-  list-style-type: none;
-  padding: 0;
-}
-
-li {
-  display: inline-block;
-  margin: 0 10px;
-}
-
-a {
-  color: #42b983;
+.error {
+  color: red;
 }
 </style>

src/components/Form.vue

<template>
   <div>
     Formページ
     <HeadComp></HeadComp>
+    <TextareaComp></TextareaComp>
   </div>
 </template>

 <script>
 import HeadComp from '@/components/modules/HeadComp'
+import TextareaComp from '@/components/modules/TextareaComp'
+
 export default {
   name: 'form',
   data () {
     return {
       msg: 'Welcome to Your Vue.js App'
     }
   },
   components: {
-    HeadComp
+    HeadComp,
+    TextareaComp
   }
 }
 </script>

</script>

こうなっているはず
スクリーンショット 2017-04-23 0.02.11.png
[コード]
https://bitbucket.org/Pocchi/vuex-study/commits/0566b5d42783f82bd1d0af8ba8b7a0ce6d8e3962

src/components/modules/StringComp.vue

<template>
-  <div>Formページ</div>
+  <p>{{string}}</p>
 </template>

 <script>
 export default {
-  name: 'form',
+  name: 'stringComp',
   data () {
     return {
-      msg: 'Welcome to Your Vue.js App'
+     string: '入力された感想をここに出す'
     }
   }
 }
 </script>

src/components/Form.vue

<template>
   <div>
     Formページ
     <HeadComp></HeadComp>
     <TextareaComp></TextareaComp>
+    <StringComp></StringComp>
   </div>
 </template>

 <script>
 import HeadComp from '@/components/modules/HeadComp'
 import TextareaComp from '@/components/modules/TextareaComp'
+import StringComp from '@/components/modules/StringComp'

 export default {
   name: 'form',
   data () {
     return {
       msg: 'Welcome to Your Vue.js App'
     }
   },
   components: {
     HeadComp,
-    TextareaComp
+    TextareaComp,
+    StringComp
   }
 }
 </script>

スクリーンショット 2017-04-23 0.13.30.png

[コード]
https://bitbucket.org/Pocchi/vuex-study/commits/67daf522eb0659b13e17c66dfd70c8f69db2543d?at=topic/re_form

buttonの作成

src/components/Form.vue

<template>
   <div>
     Formページ
     <HeadComp></HeadComp>
     <TextareaComp></TextareaComp>
     <StringComp></StringComp>
+    <button v-on:click="buttonAction">{{button}}</button>
   </div>
 </template>

 <script>
 import HeadComp from '@/components/modules/HeadComp'
 import TextareaComp from '@/components/modules/TextareaComp'
 import StringComp from '@/components/modules/StringComp'
+import { mapActions, mapGetters } from 'vuex'

 export default {
   name: 'form',
   data () {
     return {
-      msg: 'Welcome to Your Vue.js App'
+      button: '確認'
     }
   },
+  methods: mapActions('Form', {
+    'buttonAction': 'buttonAction'
+  }),
   components: {
     HeadComp,
     TextareaComp,
     StringComp
   }
 }
</script>

mapActionsというのはvuexのactionを使うためのもの。
vuexでは、storeという状態管理を集約したモジュールを作成する。
storeはmain.jsに注入することで、その子となるテンプレートで使用できる。

src/main.js

// The Vue build version to load with the `import` command
 // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
 import Vue from 'vue'
 import App from './App'
 import router from './router'
+import store from './store'

 Vue.config.productionTip = false

 /* eslint-disable no-new */
 new Vue({
   el: '#app',
   router,
   template: '<App/>',
-  components: { App }
+  components: { App },
+  store
 })

src/store/index.js

src/store/index.jsを作成する。

import Vue from 'vue'
import Vuex from 'vuex'
import router from '../router'

Vue.use(Vuex)

const Form = {
    namespaced: true,
    state: {},
    mutations: {},
    actions: {
      buttonAction({ commit, state, rootState })  {
      console.log("buttonAction")
      }
   }
}

export default new Vuex.Store({
  modules: {
    Form
  }
})

スクリーンショット 2017-04-23 0.41.12.png
確認ボタンを押すと、コンソールにbuttonActionと出てきます。

データをstoreへ集約する

データが分散しています。管理を楽にするためにstoreへ集めます。

vuex構成

  • getter ・・・テンプレートへ値を返します
  • state ・・・値を保持する
  • mutation・・・stateの値を変更する
  • action ・・・mutationを呼び出す

vuex (1).png

HeadCompのtitle

ボタンを押したら変更できるようにしましょう

src/components/modules/HeadComp.vue

 <template>
   <h1>{{title}}</h1>
 </template>

 <script>
+import { mapGetters } from 'vuex'
+
 export default {
   name: 'headComp',
-  data () {
-    return {
-      title: '感想を入力'
-    }
-  }
+  computed: mapGetters({
+  'title': 'getTitle'
+  })
 }
 </script>

src/store/index.js

import Vue from 'vue'
 import Vuex from 'vuex'
 import router from '../router'

 Vue.use(Vuex)

 const Form = {
     namespaced: true,
     state: {},
     mutations: {},
     actions: {
       buttonAction({ commit, state, rootState })  {
         console.log("buttonAction")
+        commit('setStepCount', null, {root: true})//rootへのアクセス
       }
    }
 }

+const Head = {
+  state: {
+    title: ["感想を入力", "確認画面", "送信完了"]
+  },
+  mutations: { },
+  actions: { },
+  getters: {
+    getTitle (state, getters, rootState) {
+      return state.title[rootState.stepCount]
+    }
+  }
+}
+
 export default new Vuex.Store({
+  state: {
+    stepCount: 0
+  },
+  mutations: {
+    setStepCount (state) {
+      console.log("rootsetStepCount")
+      state.stepCount++
+    }
+  },
   modules: {
-    Form
+    Form,
+    Head
   }
 })

スクリーンショット 2017-04-23 1.08.51.png

[コード]
https://bitbucket.org/Pocchi/vuex-study/commits/b9fe2316787348526bd135550c84a50d233bb2f9#Lsrc/store/index.jsF10T10

buttonを送信へ切り替え

src/components/Form.vue

<template>
   <div>
     Formページ
     <HeadComp></HeadComp>
     <TextareaComp></TextareaComp>
     <StringComp></StringComp>
     <button v-on:click="buttonAction">{{button}}</button>
   </div>
 </template>

 <script>
 import HeadComp from '@/components/modules/HeadComp'
 import TextareaComp from '@/components/modules/TextareaComp'
 import StringComp from '@/components/modules/StringComp'
 import { mapActions, mapGetters } from 'vuex'

 export default {
   name: 'form',
-  data () {
-    return {
-      button: '確認'
-    }
-  },
   methods: mapActions('Form', {
     'buttonAction': 'buttonAction'
   }),
+  computed: mapGetters('Form', {
+    'button': 'getButton'
+  }),
   components: {
     HeadComp,
     TextareaComp,
     StringComp
   }
 }
 </script>

src/store/index.js

 import Vue from 'vue'
 import Vuex from 'vuex'
 import router from '../router'

 Vue.use(Vuex)

 const Form = {
     namespaced: true,
-    state: {},
+    state: {
+      button: ["確認", "送信"],
+    },
     mutations: {},
     actions: {
       buttonAction({ commit, state, rootState })  {
         console.log("buttonAction")
         commit('setStepCount', null, {root: true})//rootへのアクセス
       }
-   }
+    },
+  getters: {
+    getButton (state, getters, rootState) {
+      return state.button[rootState.stepCount]
+    }
+  }
 }
/* 省略 */

[コード]
https://bitbucket.org/Pocchi/vuex-study/commits/43699e83fac2e72d557af137eb2a1643e08b8cd7?at=topic/re_form

formの制御

テキストエリアの入力があればエラー文を消す

src/components/modules/TextareaComp.vue

<template>
   <div>
     <p class="error">{{error}}</p>
-    <textarea></textarea>
+    <textarea v-model="impression"></textarea>
   </div>
 </template>

 <script>
+import { mapGetters } from 'vuex'
+
 export default {
   name: 'textareaComp',
-  data () {
-    return {
-      error: '入力は必須です'
-    }
+  computed: {
+    impression: {
+      get () {
+        return this.$store.state.impression
+      },
+      set (value) {
+        this.$store.commit('updateImpression', value)
+      }
+    },
+    ...mapGetters('Textarea', {
+      'error': 'getError'
+    })
   }
 }
 </script>

src/store/index.js

/* Head以下に追加 */
+const Textarea = {
+  namespaced: true,
+  state: {
+    errorMsg: "入力は必須です",
+  },
+  getters: {
+    getError (state, getters, rootState) {
+      if (rootState.errorFlag) {
+        return null
+      } else {
+        return state.errorMsg
+      }
+    }
+  }
+}
+
 export default new Vuex.Store({
   state: {
-    stepCount: 0
+    stepCount: 0,
+    impression: "",
+    errorFlag: false//trueなら通過
   },
   mutations: {
     setStepCount (state) {
       console.log("rootsetStepCount")
       state.stepCount++
+    },
+    updateImpression (state, value) {
+      state.impression = value
+      if (state.impression) {
+        state.errorFlag = true
+      } else {
+        state.errorFlag = false
+      }
     }
   },
   modules: {
     Form,
-    Head
+    Head,
+    Textarea
   }
 })

スクリーンショット 2017-04-23 1.34.50.png
[コード]
https://bitbucket.org/Pocchi/vuex-study/commits/3ef5063678e62775edd59dc8235f4e8a9004bcb4?at=topic/re_form

確認ボタンを押してテキストエリアを切り替え

src/components/Form.vue

<template>
   <div>
     Formページ
     <HeadComp></HeadComp>
-    <TextareaComp></TextareaComp>
-    <StringComp></StringComp>
+    <component
+    :is="isComponent"
+    ></component>
     <button v-on:click="buttonAction">{{button}}</button>
   </div>
 </template>

 <script>
 import HeadComp from '@/components/modules/HeadComp'
 import TextareaComp from '@/components/modules/TextareaComp'
 import StringComp from '@/components/modules/StringComp'
 import { mapActions, mapGetters } from 'vuex'

 export default {
   name: 'form',
   methods: mapActions('Form', {
     'buttonAction': 'buttonAction'
   }),
   computed: mapGetters('Form', {
-    'button': 'getButton'
+    'button': 'getButton',
+    'isComponent': 'getComponent'
   }),
   components: {
     HeadComp,
     TextareaComp,
     StringComp
   }
 }
 </script>

src/components/modules/StringComp.vue

<template>
   <p>{{string}}</p>
 </template>

 <script>
+import { mapGetters } from 'vuex'
+
 export default {
   name: 'stringComp',
-  data () {
-    return {
-     string: '入力された感想をここに出す'
-    }
-  }
+  computed: mapGetters('String', {
+    'string': 'getString'
+  })
 }
 </script>

src/store/index.js

 import Vue from 'vue'
 import Vuex from 'vuex'
 import router from '../router'

 Vue.use(Vuex)

 const Form = {
     namespaced: true,
     state: {
       button: ["確認", "送信"],
+      component: ["TextareaComp", "StringComp"]
     },
     mutations: {},
     actions: {
       buttonAction({ commit, state, rootState })  {
         console.log("buttonAction")
-        commit('setStepCount', null, {root: true})//rootへのアクセス
+        if (rootState.errorFlag) {
+          commit('setStepCount', null, {root: true})//rootへのアクセス
+        }
       }
     },
   getters: {
     getButton (state, getters, rootState) {
       return state.button[rootState.stepCount]
+    },
+    getComponent (state, getters, rootState) {
+      return state.component[rootState.stepCount]
     }
   }
 }

 const Head = {
  /* 省略 */
 }

+const String = {
+  namespaced: true,//名前空間を有効にする
+  getters: {
+    getString (state, getters, rootState) {
+      return rootState.impression
+    }
+  }
+}
+
 export default new Vuex.Store({
   state: {
     stepCount: 0,
     impression: "",
     errorFlag: false//trueなら通過
   },
   mutations: {
     setStepCount (state) {
       console.log("rootsetStepCount")
       state.stepCount++
     },
     updateImpression (state, value) {
       state.impression = value
       if (state.impression) {
         state.errorFlag = true
       } else {
         state.errorFlag = false
       }
     }
   },
   modules: {
     Form,
     Head,
-    Textarea
+    Textarea,
+    String
   }
 })

スクリーンショット 2017-04-23 1.53.04.png

[コード]
https://bitbucket.org/Pocchi/vuex-study/commits/a44142a8fb0d0091de20488012b44d95e303c50e?at=topic/re_form

送信後のthanksページを作成します

src/components/Thanks.vueを作成します。

src/components/Thanks.vue

<template>
  <div>
    Thanksページ
    <HeadComp></HeadComp>
    送信ありがとうございました!
  </div>
</template>

<script>
import HeadComp from '@/components/modules/HeadComp'
import { mapActions, mapGetters } from 'vuex'

export default {
  name: 'thanks',
  components: {
    HeadComp
  }
}
</script>

src/router/index.js

import Vue from 'vue'
 import Router from 'vue-router'
 import Form from '@/components/Form'
+import Thanks from '@/components/Thanks'

 Vue.use(Router)

 export default new Router({
   routes: [
     {
       path: '/',
       name: 'Form',
       component: Form
+    },
+    {
+      path: '/thanks',
+      name: 'Thanks',
+      component: Thanks
     }
   ]
 })

src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import router from '../router'

Vue.use(Vuex)

const Form = {
     namespaced: true,
     state: {
       button: ["確認", "送信"],
       component: ["TextareaComp", "StringComp"]
     },
     mutations: {},
     actions: {
       buttonAction({ commit, state, rootState })  {
         console.log("buttonAction")
         if (rootState.errorFlag) {
           commit('setStepCount', null, {root: true})//rootへのアクセス
         }
+        if (rootState.stepCount == 2) {
+          router.push('thanks')
+        }
       }
     },
/* 以下略 */

スクリーンショット 2017-04-23 2.15.42.png

[コード]
https://bitbucket.org/Pocchi/vuex-study/commits/f1cc369888ed65aaeb0d432a7d5e9b81e87e2a62?at=topic/re_form

おわり

動きましたでしょうか?

サーバーサイドレンダリングについては触れませんでした。
https://ja.nuxtjs.org/
というものがあるそうです。
使ってみたら、また共有したいと思います。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.