1. Qiita
  2. 投稿
  3. vue.js

React、Angularになじめなかった僕に手を差し伸べてくれたVue.js

  • 270
    いいね
  • 0
    コメント

ギークハウス Advent Calendar 2016の12月22日の記事です。

他の方とは、全然違う雰囲気になってしまいましたが、読んでいただけると幸いです。

なぜVue.js??

普段の仕事では、Ruby/Railsなので、フロントエンド周りは、jQueryにCoffeeScriptで片手間感覚...
    ↓
しかし最近のフロントエンド界隈は、良くも悪くも盛り上がっていて楽しそうだなあと思う日々。
    ↓
いろいろ、ググって調べてみると、ES6、Babel、Reactふむふむ...🤔
ん?? Webpack? JSX?? Flux?? Redux??
「落ち着け!とりあえず日本語でOK」状態。。正にこの記事で書かれている状態そのものでした。
    ↓
Reactとかでイケてるフロントエンド開発をちょっと試したいと思っても、BabelやWebpackの設定など環境構築でつまづき、肝心のアプリケーション開発に着手できず、疲弊していく自分。。
pose_zetsubou_man.png

それでも、諦めたくなかった日々を過ごし....

Vue.js!! 君と出会ったよ🤗

figure_ta-da.png

ということで、Vue.jsのご紹介

Vue.jsとは?

The Progressive JavaScript Framework
「親しみやすい」 「融通がきく」 「高性能」

だそうです。
https://jp.vuejs.org/

ポイントとしては、以下の3点が主な特徴

  • シンプル
  • リアクティブ
  • コンポーネント指向

ひとつずつ見ていきましょう。

シンプル

宣言的にビューを作ることができる

new Vueというインスタンスを作り、ビューで指定したIDを元に呼び出すというイメージで割りと違和感なく理解できた。

index.html
<div id="app">
  {{ message }}
</div>
app.js
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

他にも、ビュー上で、条件分岐やループを使うこともできる
v-ifv-forがVue.jsのAPI

index.html
<div id="app">
  <ol>
    <li v-for="todo in todos">
      <p v-if="todo.seen">
        {{ todo.text }}
      </p>
    </li>
  </ol>
</div>
app.js
var app = new Vue({
  el: '#app',
  data: {
    todos: [
      { text: 'Learn JavaScript', seen: true },
      { text: 'Learn Vue', seen: true },
      { text: 'Build something awesome', seen: false }
    ]
  }
})

ビューに表示されるのは、seenがtrueであるLearn JavaScriptLearn Vue

他にも便利な機能がたくさん。詳細は、Vue.js公式APIドキュメント

リアクティブ

そもそもの「リアクティブ」自体の定義が、難しいが、Vue.jsにおける「リアクティブ」は、JavaScript上で、定義したデータが何か変更されたと同時にビューも同期的(synchronize)に変更されるシステムだと解釈する(もっと抽象化したよい表現があるかもしれない)。

自分もまだまだよくわかっていないが、Vue.jsのおけるリアクティブシステムは以下の図で表すことができ、データの状態を監視するWatcher(ウォッチャー)というのが、欠かせない存在らしい。
ここらへんはまだよくわかってないですね、すみません🙇

image

Vuex公式ドキュメントより引用

コンポーネント指向

DOMを構造的に管理するためのアーキテクチャ
イメージとしては、以下の図がわかりやすい。

image

Vue.js公式ドキュメント「コンポーネントによる構成」より引用

ツリー構造としてDOMをコンポーネントという1つの単位で、作成し他のコンポーネントと組み合わせることで、

  • 疎結合
  • 再利用可能
  • データの状態管理を整理できる

といった恩恵が受けられる。

実装例として、ちょっとしたTodoアプリで、Todo一覧を表示しようとする例で考察してみる。
まず、todo-itemというコンポーネントを作成する。
このコンポーネントの役割としては、todoというオブジェクトが持つtextをリスト形式で表示することである。

次にgroceryListのような実際のデータを持つVueインスタンスを作成する。

todo_component.js
Vue.component('todo-item', {
  props: ['todo'],
  template: '<li>{{ todo.text }}</li>'
})

var app = new Vue({
  el: '#app',
  data: {
    groceryList: [
      { text: 'Vegetables' },
      { text: 'Cheese' },
      { text: 'Whatever else humans are supposed to eat' }
    ]
  }
})

上記で作成したVueインスタンス変数を<div id="app">のDOMとして指定する。
これで、このDOMにおいてVueインスタンス変数であるappのデータを呼び出すことができる。
そして、作成したコンポーネントをこのDOMに注入し、v-bindでVueインスタンスで定義したtodoをループして呼び出すことができる。

しかし、実際に、ビューにデータを受け渡しているのはコンポーネント内にあるpropsである。
これは、親となるスコープ(この場合は、Vueインスタンス変数app)から子コンポーネントであるtodo-itemへとデータを渡す。

index.html
<div id="app">
  <ol>
    <!-- todo オブジェクトによって各 todo-item を提供します。それは、内容を動的にできるように表します。-->
    <todo-item v-for="item in groceryList" v-bind:todo="item"></todo-item>
  </ol>
</div>

この例から言えることは、

  • アプリケーションを小さな単位で分割できる
  • 疎結合な子コンポーネントの作成ができる
  • 親コンポーネントに影響を与えず改良できる

つまり、jQueryでは難しかったDOMの構造化をフレームワークとして担保することができると言える。

結果

1. Vegetables
2. Cheese
3. Whatever else humans are supposed to eat

他にも、フォーム入力のバリデーション機能やアニメーション機能もあるが、ここでは、Vue.jsがどのような思想で、UIを構築していくかについて、触れたかったため、詳細は、素晴らしい公式ドキュメント:thumbsup:

動くものを作ってみる

月並みですが、題材は、Todoアプリにしました。
対象のフレームワークがどのようにデータの入力・出力を行うかを知れば、基本的なフレームワークの理解は進むと思ったためです。

ただ、せっかくなので、なるべくモダンで普段使いを意識した構成で開発してみようと思います。

構成

  • Fluxアーキテクチャに基づいたアプリケーション作成
  • 実装は、ES6
  • Electronでデスクトップアプリ化

Fluxアーキテクチャに基づいたアプリケーション作成

DOMをMVCで管理するのは辛いよねというところから、提唱されたアーキテクチャー
Fluxの概要については、漫画で説明するFluxがわかりやすかったので、これを読むと良いかも:metal:

簡単に言うと、

  • MVCだと、規模が大きくなり、どのモデルがどのイベントによっていつ何にデータが変更されたのかが把握することが難しい
  • Fluxによって、データの変更を受け付けつけるもの、変更を監視するもの、実際に変更を行うものなどのように責務を分けるようにした

そして、このFluxアーキテクチャーに基づいたライブラリが各JSフレームワーク毎に用意されている。
Reactなら「Redux」
Vue.jsなら 「Vuex」 である。

Vuexとは?

Vue.jsにおける状態管理パターンを実現するためのアーキテクチャであり、ライブラリ。
アーキテクチャの思想としては、、FluxRedux そして The Elm Architectureから影響を受けている。

状態管理パターンとは?

まず、状態管理パターンの必要性として、比較的ビュー部分というのは変更が激しい部分という前提がある。
そのため、ビューの表示を構成するDOMオブジェクトの状態が、いつどのように変更したのかを把握するためにこのパターンが、一つの解法アプローチとなっている。

具体的には、以下の3つの概念が状態管理パターンの中心となる。

  • 状態(state)
    • そのアプリがどういうAPIあるいはデータ属性を持っているかを情報源として集約させた場所
  • ビュー
    • 実際にユーザーに表示される画面。そこから、ユーザーからの入力など何らかのリクエストを受け付ける場所
  • アクション
    • ビューからのユーザー入力に反応して、状態の変更を行う。

これら3つの概念によるデータの流れは、 単方向データフロー と呼ばれる。
Kobito.3OWVgi.png

Vuex公式ドキュメント「Vuexとは何か?」より引用

これで、単方向データフローにもとづいて、ビューをコンポーネントとして分ければ、管理しやすくなりそうだ。これだけ見ると、非常にシンプルと言える。

しかし、複数の異なるビューやアクションによって、共通の状態を変更したり、共有しようとすると、状態管理はたちまち大変になり辛みが増してくる。

このような課題を解決するため、Vuexでは、共有されている状態を、一箇所に集約し、それをグローバルシングルトンで管理するという方法が取られている。

この方法により、コンポーネントツリーが大きなビューとなり、どのコンポーネントからでも状態へのアクセスやアクションをトリガーできる。
Kobito.dVhfmv.png

Vuex公式ドキュメントより引用

さらなる詳細は、公式ドキュメントがよく整理されていて良い!!しかも日本語訳されているのはありがたい:smile:

実装

環境構築

ES6で実装したいので、トランスパイラーとして、Babelを使用するが、バンドルツールとして、Webpackも必要になってくる。
しかし、自分は、Webpackの使い方があまりわかっておらず、あまりこういうところで時間もかけたくない。

そんなニーズを満たせるのが、 vue-cli である。
https://github.com/vuejs/vue-cli

これは、ドキュメントにある指示通りにコマンドを叩けば、BabelやWebpackの設定を自動で構築してくれる優れものである。

ただ、さらに欲張りな自分は、Electron上で、このような環境の自動構築もできたら最高だよなあと思っていた矢先に!!

なんと、 electron-vueなるものもある😀

今回は、これで環境構築をしてみる。

# Install vue-cli and scaffold boilerplate
npm install -g vue-cli
vue init simulatedgreg/electron-vue my-project

# Install dependencies and run your app
cd my-project
npm install
npm run dev

vue initのコマンドを実行するとこんな感じで、いろいろとオプションの設定を聞かれるので、お好みで!
今回は、vuexなどを使うので、ひたすらEnter
スクリーンショット 2016-12-30 17.31.05.png

ファイル構成は、このように自動生成され、基本的には、app以下のファイルを実装していく。
スクリーンショット 2016-12-30 17.34.55.png

index.ejsが最終的にページを描画するHTMLだが、特に編集する必要はない

index.ejs
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="app"></div>
    <!-- webpack builds are automatically injected -->
  </body>
</html>

main.jsで、各種モジュールをインポートし、Vueインスタンスを作成する。

main.js
import Vue from 'vue'
import Electron from 'vue-electron'
import Router from 'vue-router'

import App from './App'
import routes from './routes'

Vue.use(Electron)
Vue.use(Router)
Vue.config.debug = true

const router = new Router({
  scrollBehavior: () => ({ y: 0 }),
  routes
})

/* eslint-disable no-new */
new Vue({
  router,
  ...App
}).$mount('#app')

実際にビューを作っていくのは.vueという拡張子がついたファイルで実装していく
まずは、ベースとなるLandingPageView.vueからいじっていく

LandingPageView.vue
<template>
  <div>
    <taskList></taskList>
  </div>
</template>

<script>
import TaskList from './LandingPageView/TaskList'

export default {
  components: {
    TaskList
  },
  name: 'landing-page'
}
</script>

<style lang=scss>

</style>

個人的には、このvueというファイルに、それぞれhtml部分のtemplate、js部分のscript、CSS部分のstyleという構成がすごく気に入っている。

<taskList></taskList>というタグは、親コンポーネントであるLandingPaageView.vueの子コンポーネントを呼び出している部分である。

次にVuexによる状態管理を実装していく

まずは、アプリケーションの状態を保持するコンテナであるStoreを作っていく。
このStoreで宣言されたミューテーション(mutations)を介して、状態の変更が、リアクティブに更新される。

store.js
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mutations from './mutations'
import * as actions from './actions'
import plugins from './plugins'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  mutations,
  actions,
  plugins,
  strict: true
})

次に、mutationnsでどのような変更を行うかを宣言的に記述していく。
しかし、オプションであるが、mutation-type.jsというファイルを介してのmutationnsの実装を行うとさらに良い。

理由として、自分以外の誰かが、アプリケーションの実装を読んだ際、mutation-type.jsさえ読めば、そのアプリケーションがどんな動作をするのかを理解することが簡単になるからである。

今回で言えば、Todoアプリケーションであるため、以下のようなミューテーションとなる。

mutation-type.js
export const ADD_TASK = 'ADD_TASK'
export const TOGGLE_TASK = 'TOGGLE_TASK'
export const DELETE_TASK = 'DELETE_TASK'
export const EDIT_TASK = 'EDIT_TASK'
export const TOGGLE_ALL_TASK = 'TOGGLE_ALL_TASK'
export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'

そして、実際にミューテーションを実装していく。

mutations.js
import { ADD_TASK, TOGGLE_TASK, DELETE_TASK, EDIT_TASK, TOGGLE_ALL_TASK, CLEAR_COMPLETED } from './mutation-types'

export default {
  [ADD_TASK] (state, { text }) {
    state.tasks.push({
      text,
      done: false
    })
  },
  [TOGGLE_TASK] (state, { task }) {
    task.done = !task.done
  },
  [DELETE_TASK] (state, { task }) {
    state.tasks.splice(state.tasks.indexOf(task), 1)
  },
  [EDIT_TASK] (state, { task, value }) {
    task.text = value
  },
  [TOGGLE_ALL_TASK] (state, { done }) {
    state.tasks.forEach((task) => {
      task.done = done
    })
  },
  [CLEAR_COMPLETED] (state) {
    state.tasks = state.tasks.filter(task => !task.done)
  }
}

Vuexにおいて、状態の変更を行う際には、ミューテーションをコミットすることで初めて状態の変更が可能となる。具体的なコミットの実装は以下のとおりである。

actions.js
import { ADD_TASK, TOGGLE_TASK, DELETE_TASK, EDIT_TASK, TOGGLE_ALL_TASK, CLEAR_COMPLETED } from './mutation-types.js'

export const addTask = ({ commit }, text) => {
  if (text) {
    commit(ADD_TASK, {
      text
    })
  }
}

export const toggleTask = ({ commit }, task) => {
  commit(TOGGLE_TASK, {
    task
  })
}

export const deleteTask = ({ commit }, task) => {
  commit(DELETE_TASK, {
    task
  })
}

export const editTask = ({ commit }, task) => {
  commit(EDIT_TASK, {
    task
  })
}

export const toggleAllTask = ({ commit }, task) => {
  commit(TOGGLE_ALL_TASK, {
    task
  })
}

export const clearCompleted = ({ commit }, task) => {
  commit(CLEAR_COMPLETED, {
    task
  })
}

ここまでをおさらいすると、
1. 状態管理に必要な各種モジュールをインポートしたStoreを作成
2. 状態を変更するためのミューテーションを宣言的に定義
3. 実際に状態を変更するためにミューテーションをコミットする

そして最後に実際のTodoアプリケーションの実装を進めていく。

全ソースはこちら
https://github.com/samuraikun/electron-vue-todo

今回のTodoアプリの要件としては、大雑把にこんな感じ

  • 基本的なTodoの追加、編集、削除
  • Todoが終わったかどうかはチェックのON/OFF
  • 終わったTodoとまだ終わっていないTodoをフィルタリングして表示/非表示を切り替え

まずは、Todoの一覧リストそのものを実装していく。

全ての実装を載せると、記事が長くなりすぎるので(すでに長いが)、一部抜粋していくと、

まずテンプレート部分

TaskList.vue
<template>
...
...

<!-- main section-->
    <div v-show="tasks.length" class="main">
      <input type="checkbox" :checked="allChecked" @change="TOGGLE_ALL_TASK({ done: !allChecked })" class="toggle-all"/>
      <ul class="task-list">
        <task v-for="task in filteredTasks" :task="task"></task>
      </ul>
      <!-- footer -->
      <footer v-show="tasks.length" class="footer">
        <span class="task-count">
          <strong>{{ remaining }}</strong>
          {{ remaining | pluralize('item') }} left
        </span>
        <ul class="filters">
          <li v-for="(val, key) in filters">
            <a :href="'#/' + key" :class="{ selected: visibility === key }" @click="visibility = key">{{ key | capitalize }}</a>
          </li>
        </ul>
        <button v-show="tasks.length &gt; remaining" @click="CLEAR_COMPLETED" class="clear-completed">Clear completed</button>
      </footer>
    </div>
...
...
</template>

v-forで複数あるTodoを表示したり、Todoをフィルタリングするためのメソッドを呼び出している。

フィルタリングのロジックは、<script></sript>タグ内で実装していく。

TaskList.vue
<script>
  import { mapMutations } from 'vuex'
  import Task from './Task'

  const filters = {
    all: tasks => tasks,
    active: tasks => tasks.filter(task => !task.done),
    completed: tasks => tasks.filter(task => task.done)
  }

  export default {
    name: 'TaskList',
    components: { Task },
    data () {
      return {
        visibility: 'all',
        filters: filters
      }
    },
    computed: {
      tasks () {
        return this.$store.state.tasks
      },
      allChecked () {
        return this.tasks.every(task => task.done)
      },
      filteredTasks () {
        return filters[this.visibility](this.tasks)
      },
      remaining () {
        return this.tasks.filter(task => !task.done).length
      }
    },
    methods: {
      addTask (e) {
        var text = e.target.value
        if (text.trim()) {
          this.$store.commit('ADD_TASK', { text })
        }
        e.target.value = ''
      },
      ...mapMutations([
        'TOGGLE_ALL_TASK',
        'CLEAR_COMPLETED'
      ])
    },
    filters: {
      pluralize: (n, w) => n === 1 ? w : (w + 's'),
      capitalize: s => s.charAt(0).toUpperCase() + s.slice(1)
    }
  }
</script>

実装を読むと、なんとなくその意味がわかる。特に特徴的なのは、computedだが、これは算出型プロパティと呼ばれるVue.jsの機能の1つである。
一見すると、methodsと同じように何かしらの処理を実装しており、その違いは何だろうか?という疑問が出るかもしれない。

違いとしては、

算出プロパティは依存関係にもとづきキャッシュされる

という点である。
つまり、再度、computedで定義された処理を即座に呼び出すことができる。
これは、巨大な配列をループしたり多くの計算を必要とする際に、便利な機能と言える。

Todo一覧のコンポーネントができたので、その子コンポーネントであるTask.vueを実装していく。

テンプレート部分でTodoのフォームを作成

Task.vue
<template>
  <li :class="{ completed: task.done, editing: editing }" class="task">
    <div class="view">
      <input type="checkbox" :checked="task.done" @change="TOGGLE_TASK({ task: task })" class="toggle"/>
      <label v-text="task.text" @dblclick="editing = true"></label>
      <button @click="DELETE_TASK({ task: task })" class="destroy"></button>
    </div>
    <input v-show="editing" v-focus="editing" :value="task.text" @keyup.enter="doneEdit" @keyup.esc="cancelEdit" @blur="doneEdit" class="edit"/>
  </li>
</template>

Todoの編集、削除機能のロジックを実装していく
ちなみにmapMutationsは、コンポーネント内でミューテーションをコミットできるようにするヘルパーである。

Task.vue
<script>
import { mapMutations } from 'vuex'

export default {
  name: 'Task',
  props: ['task'],
  data () {
    return {
      editing: false
    }
  },
  directives: {
    focus (el, { value }, { context }) {
      if (value) {
        context.$nextTick(() => {
          el.focus()
        })
      }
    }
  },
  methods: {
    ...mapMutations([
      'EDIT_TASK',
      'TOGGLE_TASK',
      'DELETE_TASK'
    ]),
    doneEdit (e) {
      const value = e.target.value.trim()
      const { task } = this
      if (value) {
        this.DELETE_TASK({
          task
        })
      } else if (this.editing) {
        this.EDIT_TASK({
          task,
          value
        })
        this.editing = false
      }
    },
    cancelEdit (e) {
      e.target.value = this.task.text
      this.editing = false
    }
  }
}
</script>

これにて完成!!

スクリーンショット 2016-12-30 22.57.11.png

感想

  • 前評判通り、学習コストはフルスタックなAnguar、JSXを強いられるReactと違いシンプル
  • 公式のドキュメントが日本語訳もされており、わかりやすい
  • Reactのようにコンポーネントによる開発、Anguar1.xのように、ディレクティブやバインディングができるなど、他のフレームワークの良いところを上手く導入できている印象

今後

PHPフレームワークのLaravelがVue.jsを導入したり、私たちはなぜReactではなくVue.jsを選んだのかの記事で言われているように、React、Angularで違和感を感じた部分をVue.jsが補っている部分があるから人気が出ているかなあと感じた。

もしもだけれど、RailsもVue.jsを公式に導入してくれたら嬉しいなあ😆