LoginSignup
41
30

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-09-14

はじめに

前回の「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-loadervue-loadervue-template-compilerの3つ。

最初公式に書いてあるとおりにvue-loaderだけいれてみた。

しかし、vue-loaderだけではどうやら足りないらしく2つ追加して計3つになった。

※エラーのとおりに2つ追加しているので完全に理解しきっていない

css-loaderModule not found: Error: Can't resolve 'css-loader' inとエラーが吐かれたので追加した。

vue-template-compilerModule build failed: Error: Cannot find module 'vue-template-compiler'とエラーが吐かれたので追加した。

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

ゲッター・Vuex

アクションとミューテーションはそのままで、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をいれてみる。

41
30
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
30