Edited at

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

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/

というものがあるそうです。

使ってみたら、また共有したいと思います。