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
状態管理パターンとは
3つの役割に分けることができる。
一方向の流れで管理がシンプルだが、複数のコンポーネントで情報源を共有しようとすると値が重複し、管理が面倒になる。
そこで、情報源を1つに集約しようとするのが Vuexです。
今回は簡単な入力フォームを作成します。
vue.jsを使わなくても作れますが、練習用に作成します。
画面の流れ
画面の作り
- h1のヘッダは使いまわせそう。
- エラー文は出しわけが発生する
- テキストエリアと確認文の切り替え
- ボタンの文字も2種類文字がある
作成の方法
これを1コンポーネントで作成します。
準備とか
まず、vuexを使用するので、インストールします。
npm install --save vuex
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
}
]
})
書き換えると、こうなります。
ちなみに、ページリロードは自動でしてくれるので、更新をする必要はありません。
ロゴも残ってしまっているので、削除しましょう。
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>
[コード]
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>
こうなっているはず
[コード]
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>
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
}
})
確認ボタンを押すと、コンソールにbuttonAction
と出てきます。
データをstoreへ集約する
データが分散しています。管理を楽にするためにstoreへ集めます。
vuex構成
- getter ・・・テンプレートへ値を返します
- state ・・・値を保持する
- mutation・・・stateの値を変更する
- action ・・・mutationを呼び出す
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
}
})
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]
+ }
+ }
}
/* 省略 */
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
}
})
[コード]
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
}
})
送信後の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')
+ }
}
},
/* 以下略 */
おわり
動きましたでしょうか?
サーバーサイドレンダリングについては触れませんでした。
https://ja.nuxtjs.org/
というものがあるそうです。
使ってみたら、また共有したいと思います。