なになれ

IT系のことを記録していきます

Tour of HeroesをVue+Vuexで再実装した

Vue+Vuexを勉強したアウトプットとしてアプリを作りました。
hi1280.hatenablog.com

作ったもの

Tour of HeroesはAngular公式のチュートリアルです。
複数画面があって、WebAPIを呼び出すサンプルになっています。
今回はこのTour of Heroesを題材にして、Vue+Vuexを使ってTour of Heroesを再実装しました。

実際のプログラムはこちらです。
github.com

Angular、React+Reduxと比較しつつ、Vue+Vuexによる実装内容を説明します。

Viewの作成

リスト形式でデータを表示する

VueではHTMLに対してテンプレート構文を書いて動的にHTMLを生成できるようになっています。
v-forというテンプレート構文で繰り返しの記述をサポートしています。
コードを見てもらえば分かるようにAngularにかなり似ています。

Dashboard.vue

<!-- 一部抜粋 -->
<router-link v-for="hero in sliced_heroes" :to="{ name: 'detail', params: {id: hero.id} }" class="col-1-4" :key="hero.id">
  <div class="module hero">
    <h4>{{hero.name}}</h4>
  </div>
</router-link>

ちなみにAngularの場合はngForというテンプレート構文になります。

dashboard.component.html

<!-- 一部抜粋 -->
<a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}">
  <div class="module hero">
    <h4>{{hero.name}}</h4>
  </div>
</a>

フォームを作成する

v-modelというテンプレート構文で双方向データバインディングが可能です。
バインディングするデータの元はVuexで定義したStoreです。

HeroDetail.vue

// 一部抜粋
<template>
  <div v-if="hero">
    <h2>{{hero.name}} Details</h2>
    <div><span>id: </span>{{hero.id}}</div>
    <div>
      <label>name:
        <input v-model="hero.name" placeholder="name"/>
      </label>
    </div>
    <button @click="goBack()">go back</button>
    <button @click="save()">save</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      hero: {
        id: 0,
        name: ''
      }
    }
  },
  created() {
    this.$store.dispatch('fetchHero', this.$route.params.id);
    this.hero = this.$store.getters.heroes.find(element => element.id == this.$route.params.id);
  },
  methods: {
    goBack() {
      this.$router.go(-1);
    },
    save() {
      this.$store.dispatch('updateHero', this.hero);
      this.$router.go(-1);
    }
  }
}
</script>

Angularのチュートリアルでも双方向データバインディングでフィールドの値とフォームの値とを同期しています。

hero-detail.component.html

<!-- 一部抜粋 -->
<input [(ngModel)]="hero.name" placeholder="name"/>

hero-detail.component.ts

// 一部抜粋
export class HeroDetailComponent implements OnInit {
  @Input() hero: Hero;
}

WebAPI呼び出し

VuexのActionでWebAPIを呼び出します。
HTTPのリクエストを実行するのにvue-resourceを使いました。
なお、vue-resourceは公式で推奨するやり方ではなくなっているようです。

modules/heroes.js

// 一部抜粋
const actions = {
  fetchHeroes: ({commit}) => {
    Vue.http.get(`?key=${key}`)
      .then(response => response.json())
      .then(heroes => {
        if(heroes) {
          commit('FETCH_HEROES', heroes);
        }
      });
  },

Actionでの非同期処理の結果をMutationで処理します。

modules/heroes.js

// 一部抜粋
const mutations = {
  'FETCH_HEROES' (state, heroes) {
    state.heroes = heroes;
  },

ちなみにReduxのコードです。
ActionではVuexとは違い、非同期処理によるPromiseオブジェクトをそのまま渡しています。

actions/index.js

// 一部抜粋
export function fetchHeroes() {
  const request = axios.get(`${ROOT_URL}?key=${key}`);
  return {
    type: FETCH_HEROES,
    payload: request
  }
}

ActionからReducerに処理が流れる時に、redux-promiseを使って、PromiseのオブジェクトをJavaScriptのオブジェクトに変換しています。
ReducerではPromiseの値であることを意識せずに処理します。

reducer-heroes.js

// 一部抜粋
export default function(state = {}, action) {
  switch(action.type) {
    case DELETE_HERO:
      return _.omit(state, action.payload);
    case FETCH_HEROES:
      return _.mapKeys(action.payload.data, 'id');
    case CREATE_HERO:
    case FETCH_HERO:
    case UPDATE_HERO:
      return {...state, [action.payload.data.id]: action.payload.data};
    default:
      return state;
  }
}

CSS指定

グローバルなCSSやScopedなCSSを記述することが可能です。
以下のようにしてコンポーネント内で閉じたCSS指定が可能です。

App.vue

/* 一部抜粋 */
<style scoped>
h1 {
  font-size: 1.2em;
  color: #999;
  margin-bottom: 0;
}
h2 {
  font-size: 2em;
  margin-top: 0;
  padding-top: 0;
}
nav a {
  padding: 5px 10px;
  text-decoration: none;
  margin-top: 10px;
  display: inline-block;
  background-color: #eee;
  border-radius: 4px;
}
nav a:visited, a:link {
  color: #607D8B;
}
nav a:hover {
  color: #039be5;
  background-color: #CFD8DC;
}
nav a.active {
  color: #039be5;
}
</style>

Angularでも同様にコンポーネント内に閉じたCSS指定が可能です。

まとめ

今回取り組んでみて、Vue+VuexではAngularやReduxの良いところを取り入れている感じがしました。
ActionやMutationの部分など、VuexはReduxに比べて作りがシンプルな感じがしました。