今まで「それReactでよくね?」ってVueを避けてたんだけど、そろそろ触ってみようと思います
※ 超初心者向けなのでVue有識者の方々には物足りない記事になってるかと思います
とりあえず触ってみるもの
Vue + Vuexの構成がReact + Redux的なデファクトスタンダードらしいのでそれを触ってみる
Nuxt.jsは素晴らしいらしいのだけど、それ使っちゃうと多分大事なところ全部Nuxtがやってくれちゃって自分で1から作れなくなるので今回はパス
代わりにvue-cliでwebpackを使ってみようと思ふ
作るもの
いつもの通り初回はTODOでも作ってみる
作ったものはこちら
インストール&環境構築
vue-cli
$ npm i -g vue-cli
OK.
Vue環境
$ vue-cli webpack vue-vuex-todo
? Project name vue-vuex-todo
? Project description A Vue.js project
? Author k-tada <white.rv0925@gmail.com>
? Vue build standalone
? Install vue-router? No
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm
vue-cli · Generated "vue-vuex-todo".
# Installing project dependencies ...
:
:
:
# Project initialization finished!
# ========================
To get started:
cd vue-vuex-todo
npm run dev
Documentation can be found at https://vuejs-templates.github.io/webpack
OK.
ちょこっと変更
2018/08/13現在、デフォルトだとbabel-preset-stage-2が入るみたいだけど、stage-3のObject Spread使いたいから面倒くさいからstage-0入れる
$ npm i -D babel-preset-stage-0
んで.babelrc
書き換える
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
- "stage-2"
+ "stage-0"
],
"plugins": ["transform-vue-jsx", "transform-runtime"],
"env": {
"test": {
- "presets": ["env", "stage-2"],
+ "presets": ["env", "stage-0"],
"plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"]
}
}
}
OK.
動確
ここで一度確認
$ npm run dev
でブラウザからhttp://localhost:8080/にアクセスしたら下記の画面が見えるはず
OK.
Vue作っていく
Vueだけなら、基本はsrc/components
の下に.vue
ファイルを作っていけばOKっぽい
今回のメインの目的はVueに触ることなので、デザインあまり考えたくなかったからTodoMVCっぽい感じで作ることにした。
VueのコンポーネントはReactと違ってJS in HTMLなので、下記のような感じで作ればいいっぽい。
コンポーネント設計のセンスは生まれつき持ち合わせて無いのでご愛嬌
<template>
<div class="app">
<div class="container">
<Logo />
<TodoApp />
</div>
</div>
</template>
<script>
import Logo from './components/Logo'
import TodoApp from './components/TodoApp'
export default {
name: 'App',
components: {
Logo,
TodoApp
}
}
</script>
<style scoped>
/* 省略 */
</style>
他のコンポーネントもさっくりと
<template>
<div class="logo">{{text}}</div>
</template>
<script>
export default {
name: 'Logo',
data () {
return {
text: 'todos'
}
}
}
</script>
<style scoped>
/* 省略 */
</style>
<template>
<div class="container">
<Form />
<List />
</div>
</template>
<script>
import Form from './todo/Form'
import List from './todo/List'
export default {
name: 'TodoApp',
components: {
Form,
List
}
}
</script>
<style scoped>
/* 省略 */
</style>
<template>
<div class="form">
<div class="toggle-all" />
<div class="input">
<input type="text" class="new" placeholder="What needs to be done?" />
</div>
</div>
</template>
<script>
export default {
name: 'Form'
}
</script>
<style scoped>
/* 省略 */
</style>
<template>
<div class="todos">
<div class="todo" v-for="todo in todos" :key="todo.id">
<input
class="toggle"
type="checkbox"
:checked="todo.isDone"
/>
<div class="text">{{todo.text}}</div>
</div>
</div>
</template>
<script>
export default {
name: 'List',
data () {
/* test data */
return {
todos: [
{ id: 1, text: 'aaa', isDone: false },
{ id: 2, text: 'bbb', isDone: true },
{ id: 3, text: 'ccc', isDone: false }
]
}
}
}
</script>
<style scoped>
/* 省略 */
</style>
コミットはこちら
https://github.com/k-tada/vue-vuex-todo/commit/2e0143784cba9cdedc818d1eafadb57bbbaaccd9
Vuex作ってく
Vuexの詳細な説明は他の記事を参照(なげやり)
インストール
vue-cli webpackで作った環境にはvuexは入ってないので自前でインストールする
$ npm i -S vuex
vuex store作る
ReduxではAction,ActionCreator,Reducerと色々用意しないといけないけど、VuexだとStore1つでOK。Ducksパターンっぽい
ReduxのcombineReducerっぽく複数のStoreを扱えるようにしとく。
import Vue from 'vue'
import Vuex from 'vuex'
import todos from './todos'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
todos
}
})
続いてToDO管理用のStore
export default {
namespaced: true,
state: {
nextId: 1,
todos: [],
isDoneAll: false
},
getters: {
allTodos (state) {
return state.todos
},
activeTodos (state) {
return state.todos.filter(t => !t.isDone)
},
completedTodos (state) {
return state.todos.filter(t => t.isDone)
}
},
mutations: {
addTodo (state, todo) {
state.todos.push({ id: state.nextId, text: todo, isDone: false })
state.nextId += 1
},
toggleTodo (state, id) {
state.todos = state.todos.map(t => ({ ...t, isDone: t.id === id ? !t.isDone : t.isDone }))
state.isDoneAll = state.todos.every(t => t.isDone)
},
toggleAllTodo (state, isDone) {
state.todos = state.todos.map(t => ({ ...t, isDone }))
state.isDoneAll = isDone
},
deleteTodo (state, id) {
state.todos = state.todos.filter(t => t.id !== id)
}
},
actions: {}
}
順に説明するとこんな感じ。
-
namespaced: true
Storeごとに名前空間をもたせられる。combineReducerされたStateっぽい感じになる。 -
state
Storeの持つステート。ReduxのStateと(多分)同じ。 -
getters
ステートから値を取り出すためのメソッド。別のStoreのステートも引っ張ってこれるっぽい。Reselect的なアレ。getters
を経由しなくても直接ステートを参照することも出来るのでステートから何かしら処理したデータをVue側で参照したいときに使う。 -
mutations
ActionCreator + Reducer的な。ステートを更新するためのメソッド。基本的にはステートの更新は必ずmutations経由で行う。Viewから直で呼べる。 -
actions
非同期処理部分。今は使ってないから空。ToDOをサーバから取得するような処理を書く場合はここに書くのかな。actionsでAPIコールしてmutationsを呼び出してステートを更新するイメージだと思う
Vueから呼び出す
まずはList
<template>
<div class="todos">
<div class="todo" v-for="todo in todos" :key="todo.id">
<input
class="toggle"
type="checkbox"
:checked="todo.isDone"
+ @click="toggleTodo(todo.id)"
/>
<div class="text">{{todo.text}}</div>
</div>
</div>
</template>
<script>
+ import { mapGetters } from 'vuex'
export default {
name: 'List',
- data () {
- /* test data */
- return {
- todos: [
- { id: 1, text: 'aaa', isDone: false },
- { id: 2, text: 'bbb', isDone: true },
- { id: 3, text: 'ccc', isDone: false }
- ]
- }
- }
+ computed: mapGetters('todos', {
+ todos: 'allTodos'
+ }),
+ methods: {
+ toggleTodo (id) {
+ this.$store.commit('todos/toggleTodo', id)
+ }
+ }
}
</script>
ポイントはこの辺。
- @click
でクリック時のイベントをbindする(v-on
の省略記法らしい)
- vuexのmapGetters
で、vueに必要なデータを取り出す
- this.$store.commit
で、vuexのmutationsを呼び出す
続いてForm
<template>
<div class="form">
- <div class="toggle-all" />
+ <div class="toggle-all" @click="toggleAllTodo" />
<div class="input">
- <input type="text" class="new" placeholder="What needs to be done?" />
+ <input
+ type="text"
+ class="new"
+ placeholder="What needs to be done?"
+ :value="text"
+ @input="updateText($event)"
+ @keyup.enter="addTodo()"
+ />
</div>
</div>
</template>
<script>
+ import { mapState } from 'vuex'
+
export default {
name: 'Form',
+ data () {
+ return {
+ text: ''
+ }
+ },
+ computed: mapState({
+ isDoneAll: state => state.todos.isDoneAll
+ }),
+ methods: {
+ updateText (e) {
+ this.text = e.target.value
+ },
+ addTodo (e) {
+ this.$store.commit('todos/addTodo', this.text)
+ this.text = ''
+ },
+ toggleAllTodo () {
+ this.$store.commit('todos/toggleAllTodo', !this.isDoneAll)
+ }
+ }
}
</script>
色々差分出てるけど、ポイントはこの辺
- 入力フォームのinputイベントでVueのローカルステートを更新(
updateText
) - 入力フォームのEnterキー押下イベントでVuexの
addTodo
mutationを呼び出し - toggleAll系の処理(詳細は割愛)
最後にVueからVuexのStoreを見れるようにmain.jsを修正
import Vue from 'vue'
import App from './App'
+ import store from './store'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
+ store,
components: { App },
template: '<App/>'
})
Delete忘れてた(2018/08/14)
<template>
<div class="form">
<div class="toggle-all" @click="toggleAllTodo" />
<div class="input">
<input
type="text"
class="new"
placeholder="What needs to be done?"
:value="text"
@input="updateText($event)"
@keyup.enter="addTodo()"
/>
+ <div class="destroy" @click="deleteTodo(todo.id)">×</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'Form',
data () {
return {
text: ''
}
},
computed: mapState({
isDoneAll: state => state.todos.isDoneAll
}),
methods: {
updateText (e) {
this.text = e.target.value
},
addTodo (e) {
this.$store.commit('todos/addTodo', this.text)
this.text = ''
},
toggleAllTodo () {
this.$store.commit('todos/toggleAllTodo', !this.isDoneAll)
+ },
+ deleteTodo (id) {
+ this.$store.commit('todos/deleteTodo', id)
}
}
}
</script>
まとめ
まだVueの書き方には慣れないけど、ざっと書いてみた感じだとReact + Reduxに比べて楽に書けるように感じた。
おそらく中〜大規模なアプリケーションになると、Redux-sagaのような仕組みが欲しくなると思うけど、軽量アプリケーションならぶっちゃけReact + Reduxを選ぶよりVue + Vuexのほうが楽かもしれない。React + MobXともいい勝負かも。
続くかも。