はじめに
前回の「Vue.jsとVuexとwebpackでTodoListを作ってみた」を単一ファイルコンポーネント化したので書いていきたいと思います。
※かなり苦労をした・・・
機能は前回と同じく「追加」と、「完了」と、「完了から戻す」3つだけ。
Vue.jsとVuexとwebpackの簡単な説明は前回のQiitaに書いたので割愛。
次回 「Vue.jsとVuexとvue-routerとwebpackで単一ファイルコンポーネント化してTodoListを作ってみた」
スペック
- webpackは少しだけ使ったことある
- 今回でいろいろ学んだ
- nodeとnpmはわりと使ったことある
単一ファイルコンポーネント
Vue.jsが提供する、HTMLとCSSとJSを1つのファイルにまとめる事ができる機能。
拡張子は*.vue
。
例
Hello.vue
<template>
<p>{{ greeting }} World!</p>
</template>
<script>
module.exports = {
data: function() {
return {
greeting: 'Hello'
}
}
}
</script>
<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>
cssはscoped
をつけることで単一ファイルコンポーネント内でのみ有効になります。
また、<style lang="sass">
と記述することでSASSを書くことができる。
ソースコード解説
package.json
{
"name": "vuex-webpack-todo-sample",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"vue": "^2.4.2",
"vuex": "^2.4.0"
},
"devDependencies": {
"css-loader": "^0.28.7",
"vue-loader": "^13.0.4",
"vue-template-compiler": "^2.4.2",
"webpack": "^3.5.6"
}
}
前回から追加したものはcss-loader
とvue-loader
とvue-template-compiler
の3つ。
最初公式に書いてあるとおりにvue-loader
だけいれてみた。
しかし、vue-loader
だけではどうやら足りないらしく2つ追加して計3つになった。
※エラーのとおりに2つ追加しているので完全に理解しきっていない
css-loader
はModule not found: Error: Can't resolve 'css-loader' in
とエラーが吐かれたので追加した。
vue-template-compiler
はModule build failed: Error: Cannot find module 'vue-template-compiler'
とエラーが吐かれたので追加した。
Vueの公式のwebpack-simpleのpackage.json
にも追加されているので問題ないはず。
※実はこのQiita書いている時にwebpack-simpleの存在に気づいた・・・もっと早く気づいていればこんなに時間はかからなかった(ry
webpack.config.js
module.exports = {
entry: './src/js/app.js',
output: {
path: __dirname,
filename: './src/js/bundle.js'
},
resolve: {
alias: {
vue: 'vue/dist/vue.esm.js',
vuex: 'vuex/dist/vuex.js',
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
}
]
}
};
前回から変更したのはaliasのvue.js
のファイルをvue.esm.js
に変更した点。
vue.esm.js
はwebpack2などのバンドラーで利用する用とのことなので変更。
参考 さまざまなビルドについて
また、単一ファイルコンポーネントを読み込むためrulesを記述。
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Vuex Todo Sample</title>
<link rel="stylesheet" href="">
<style>
main {
display: flex;
}
</style>
</head>
<body>
<main id="app">
<todo-input></todo-input>
<todo-list></todo-list>
<done-list></done-list>
</main>
<script src="js/bundle.js"></script>
</body>
</html>
ついにスッキリとしたHTMLに・・・!
実装した単一ファイルコンポーネントである<todo-input>
と<todo-list>
と<done-list>
を表示するだけ。
app.js
import Vue from 'vue';
import Vuex from 'vuex';
import todoInput from './components/todoInput.vue';
import todoList from './components/todoList.vue';
import doneList from './components/doneList.vue';
Vue.use(Vuex);
var store = new Vuex.Store({
state: {
todos: [],
dones: []
},
getters: {
todos(state) {
return state.todos;
},
dones(state) {
return state.dones;
}
},
actions: {
addTodo (context, todo) {
context.commit('ADD_TODO',todo.text);
},
done (context, todo) {
context.commit('DONE_TODO',todo.id);
},
reset (context, todo) {
context.commit('RESET_TODO',todo.id);
}
},
mutations: {
ADD_TODO (state, text) {
var todo = {
id: 0,
text: text
}
if (state.todos.length !== 0) {
todo.id = state.todos[state.todos.length -1].id + 1;
}
state.todos.push(todo);
},
DONE_TODO (state, id) {
for (var i = 0; i < state.todos.length; i++) {
if (state.todos[i].id === id) {
state.dones.push(state.todos[i]);
state.todos.splice(i, 1);
break;
}
}
},
RESET_TODO (state, id) {
var todo = {};
for (var i = 0; i < state.dones.length; i++) {
if (state.dones[i].id === id) {
todo = state.dones[i];
state.dones.splice(i, 1);
break;
}
}
state.todos.push(todo);
state.todos.sort(function(a,b){
if(a.id<b.id) return -1;
if(a.id > b.id) return 1;
return 0;
})
}
}
});
new Vue({
el: '#app',
store: store,
components: {
"todo-input": todoInput,
"todo-list": todoList,
"done-list": doneList,
},
});
逆にapp.jsは前回と比べ長くなった。
ストアやアクションやミューテーションを別のJSファイルに分離すればもっと見やすくなると思われる。
最初の方でVueなどの読み込みと、実装した単一ファイルコンポーネントを読み込みをする。
今回app.jsに書いてあるstoreを複数の単一ファイルコンポーネント内で利用するためゲッターを実装しました。
アクションとミューテーションはそのままで、Vueの引数に単一ファイルコンポーネントを指定します。
指定する際にtodo-input
のようにハイフンで小文字の単語を区切り定義します。
どうやらW3Cのルールで小文字とハイフンは決まっているそうです。
※Vueはそれを強制しないとは言ってましたが・・・
todoInput.vue
<template>
<div class="wrap">
<h2>Todo Input</h2>
<input type="text" @keyup.enter="addTodoText"/>
</div>
</template>
<style scoped>
.wrap {
padding: 0 10px;
}
</style>
<script>
export default {
methods: {
addTodoText(e) {
var text = e.target.value;
this.$store.dispatch('addTodo', {
text: text
});
e.target.value = '';
}
}
}
</script>
HTMLとCSSとJSをそのまま持ってきました。
todoList.vue
<template>
<div class="wrap">
<h2>Todo List</h2>
<ul v-cloak>
<li v-for="todo in todos">
<p>ID : {{todo.id}}</p>
<p>Text : {{todo.text}}</p>
<button @click="doneTodo(todo.id)">Done</button>
</li>
</ul>
</div>
</template>
<style scoped>
ul {
list-style: none;
}
.wrap {
padding: 0 10px;
}
[v-cloak] {
display: none;
}
</style>
<script>
export default {
computed: {
todos() {
return this.$store.getters.todos;
}
},
methods: {
doneTodo(id) {
this.$store.dispatch('done', {
id: id
});
}
}
}
</script>
基本的にはそのままもってきています。
変更点としてはcomputedを利用しStoreに定義したゲッターを呼び出すようにしています。
computedで定義するのがベストプラクティスなのかは不明。
もっといい方法があったら教えてくださいm(_ _)m
doneList.vue
<template>
<div class="wrap">
<h2>Done List</h2>
<ul v-cloak>
<li v-for="done in dones">
<p>ID : {{done.id}}</p>
<p>Text : {{done.text}}</p>
<button @click="resetTodo(done.id)">Reset</button>
</li>
</ul>
</div>
</template>
<style scoped>
ul {
list-style: none;
}
.wrap {
padding: 0 10px;
}
[v-cloak] {
display: none;
}
</style>
<script>
export default {
computed: {
dones() {
return this.$store.getters.dones;
}
},
methods: {
resetTodo(id) {
this.$store.dispatch('reset', {
id: id
});
}
}
}
</script>
todoList.vueと同じ構成。
所感
ファイルを分割していけばしていくほど、どこに何を書けばいいのか悩むようになる(当たり前かもしれないが)
また、webpackやVueのビルドの知識が少なかったせいでかなり時間がかかった(ランタイム限定ビルドや完全ビルドとか)
次はvue-routerをいれてみる。