Vue.jsとVuexとwebpackで単一ファイルコンポーネント化してTodoListを作ってみた

# はじめに

前回の「[Vue.jsとVuexとwebpackでTodoListを作ってみた](https://qiita.com/tiwu_official/items/998fd4bd84bbda470ec4)」を単一ファイルコンポーネント化したので書いていきたいと思います。

※かなり苦労をした・・・

[成果物](https://theimaginationwasunbroken.com/vue-sample/src/vuex-webpack-components-todo-sample/src/)

機能は前回と同じく「追加」と、「完了」と、「完了から戻す」3つだけ。

Vue.jsとVuexとwebpackの簡単な説明は前回のQiitaに書いたので割愛。

次回 「[Vue.jsとVuexとvue-routerとwebpackで単一ファイルコンポーネント化してTodoListを作ってみた](http://qiita.com/tiwu_official/items/0b681d59c4c67dbad42c)」

# スペック

- webpackは少しだけ使ったことある
    - 今回でいろいろ学んだ
- nodeとnpmはわりと使ったことある

# 単一ファイルコンポーネント

[公式](https://jp.vuejs.org/v2/guide/single-file-components.html)

Vue.jsが提供する、HTMLとCSSとJSを1つのファイルにまとめる事ができる機能。

拡張子は`*.vue`。

例
Hello.vue

```html
<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を書くことができる。

# ソースコード解説


[全体](https://github.com/tiwuofficial/vue-sample/tree/master/src/vuex-webpack-components-todo-sample)

## package.json


```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](https://github.com/vuejs-templates/webpack-simple)の`package.json`にも追加されているので問題ないはず。

※実はこのQiita書いている時にwebpack-simpleの存在に気づいた・・・もっと早く気づいていればこんなに時間はかからなかった(ry


## webpack.config.js

```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などのバンドラーで利用する用とのことなので変更。

参考 [さまざまなビルドについて](https://jp.vuejs.org/v2/guide/installation.html#さまざまなビルドについて)

また、単一ファイルコンポーネントを読み込むためrulesを記述。

## index.html

```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

```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を複数の単一ファイルコンポーネント内で利用するためゲッターを実装しました。

[ゲッター・Vuex](https://vuex.vuejs.org/ja/getters.html)

アクションとミューテーションはそのままで、Vueの引数に単一ファイルコンポーネントを指定します。

指定する際に`todo-input`のようにハイフンで小文字の単語を区切り定義します。

どうやら[W3Cのルール](https://www.w3.org/TR/custom-elements/#concepts)で小文字とハイフンは決まっているそうです。

※Vueはそれを強制しないとは言ってましたが・・・

## todoInput.vue

```html
<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

```html
<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

```html
<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をいれてみる。