Help us understand the problem. What is going on with this article?

vue.js+Vuexチュートリアル

More than 3 years have 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/
というものがあるそうです。
使ってみたら、また共有したいと思います。

_P0cChi_
theport
ポート株式会社 PORT INC.は、「世界中に、アタリマエとシアワセを。」をミッションに、新しい時代の1ページを創る会社です。【PORTもくもく会】https://freestyle-mokumoku.connpass.com/
https://www.theport.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした