ギークハウス Advent Calendar 2016の12月22日の記事です。
他の方とは、全然違う雰囲気になってしまいましたが、読んでいただけると幸いです。
なぜVue.js??
普段の仕事では、Ruby/Railsなので、フロントエンド周りは、jQueryにCoffeeScriptで片手間感覚...
↓
しかし最近のフロントエンド界隈は、良くも悪くも盛り上がっていて楽しそうだなあと思う日々。
↓
いろいろ、ググって調べてみると、ES6、Babel、Reactふむふむ...🤔
ん?? Webpack? JSX?? Flux?? Redux??
「落ち着け!とりあえず日本語でOK」状態。。正にこの記事で書かれている状態そのものでした。
↓
Reactとかでイケてるフロントエンド開発をちょっと試したいと思っても、BabelやWebpackの設定など環境構築でつまづき、肝心のアプリケーション開発に着手できず、疲弊していく自分。。
それでも、諦めたくなかった日々を過ごし....
Vue.js!! 君と出会ったよ🤗
ということで、Vue.jsのご紹介
Vue.jsとは?
The Progressive JavaScript Framework
「親しみやすい」 「融通がきく」 「高性能」
だそうです。
https://jp.vuejs.org/
ポイントとしては、以下の3点が主な特徴
- シンプル
- リアクティブ
- コンポーネント指向
ひとつずつ見ていきましょう。
シンプル
宣言的にビューを作ることができる
new Vue
というインスタンスを作り、ビューで指定したIDを元に呼び出すというイメージで割りと違和感なく理解できた。
<div id="app">
{{ message }}
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
他にも、ビュー上で、条件分岐やループを使うこともできる
v-if
やv-for
がVue.jsのAPI
<div id="app">
<ol>
<li v-for="todo in todos">
<p v-if="todo.seen">
{{ todo.text }}
</p>
</li>
</ol>
</div>
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 JavaScript
とLearn Vue
他にも便利な機能がたくさん。詳細は、Vue.js公式APIドキュメント
リアクティブ
そもそもの「リアクティブ」自体の定義が、難しいが、Vue.jsにおける「リアクティブ」は、JavaScript上で、定義したデータが何か変更されたと同時にビューも同期的(synchronize)に変更されるシステムだと解釈する(もっと抽象化したよい表現があるかもしれない)。
自分もまだまだよくわかっていないが、Vue.jsのおけるリアクティブシステムは以下の図で表すことができ、データの状態を監視するWatcher(ウォッチャー)というのが、欠かせない存在らしい。
ここらへんはまだよくわかってないですね、すみません🙇
Vuex公式ドキュメントより引用
コンポーネント指向
DOMを構造的に管理するためのアーキテクチャ
イメージとしては、以下の図がわかりやすい。
ツリー構造としてDOMをコンポーネントという1つの単位で、作成し他のコンポーネントと組み合わせることで、
- 疎結合
- 再利用可能
- データの状態管理を整理できる
といった恩恵が受けられる。
実装例として、ちょっとしたTodoアプリで、Todo一覧を表示しようとする例で考察してみる。
まず、todo-item
というコンポーネントを作成する。
このコンポーネントの役割としては、todo
というオブジェクトが持つtext
をリスト形式で表示することである。
次にgroceryList
のような実際のデータを持つVueインスタンスを作成する。
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
へとデータを渡す。
<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を構築していくかについて、触れたかったため、詳細は、素晴らしい公式ドキュメントで
動くものを作ってみる
月並みですが、題材は、Todoアプリにしました。
対象のフレームワークがどのようにデータの入力・出力を行うかを知れば、基本的なフレームワークの理解は進むと思ったためです。
ただ、せっかくなので、なるべくモダンで普段使いを意識した構成で開発してみようと思います。
構成
- Fluxアーキテクチャに基づいたアプリケーション作成
- 実装は、ES6
- Electronでデスクトップアプリ化
Fluxアーキテクチャに基づいたアプリケーション作成
DOMをMVCで管理するのは辛いよねというところから、提唱されたアーキテクチャー
Fluxの概要については、漫画で説明するFluxがわかりやすかったので、これを読むと良いかも
簡単に言うと、
- MVCだと、規模が大きくなり、どのモデルがどのイベントによっていつ何にデータが変更されたのかが把握することが難しい
- Fluxによって、データの変更を受け付けつけるもの、変更を監視するもの、実際に変更を行うものなどのように責務を分けるようにした
そして、このFluxアーキテクチャーに基づいたライブラリが各JSフレームワーク毎に用意されている。
Reactなら「Redux」
Vue.jsなら 「Vuex」 である。
Vuexとは?
Vue.jsにおける状態管理パターンを実現するためのアーキテクチャであり、ライブラリ。
アーキテクチャの思想としては、、Flux、 Redux そして The Elm Architectureから影響を受けている。
状態管理パターンとは?
まず、状態管理パターンの必要性として、比較的ビュー部分というのは変更が激しい部分という前提がある。
そのため、ビューの表示を構成するDOMオブジェクトの状態が、いつどのように変更したのかを把握するためにこのパターンが、一つの解法アプローチとなっている。
具体的には、以下の3つの概念が状態管理パターンの中心となる。
- 状態(state)
- そのアプリがどういうAPIあるいはデータ属性を持っているかを情報源として集約させた場所
- ビュー
- 実際にユーザーに表示される画面。そこから、ユーザーからの入力など何らかのリクエストを受け付ける場所
- アクション
- ビューからのユーザー入力に反応して、状態の変更を行う。
これら3つの概念によるデータの流れは、 単方向データフロー と呼ばれる。
これで、単方向データフローにもとづいて、ビューをコンポーネントとして分ければ、管理しやすくなりそうだ。これだけ見ると、非常にシンプルと言える。
しかし、複数の異なるビューやアクションによって、共通の状態を変更したり、共有しようとすると、状態管理はたちまち大変になり辛みが増してくる。
このような課題を解決するため、Vuexでは、共有されている状態を、一箇所に集約し、それをグローバルシングルトンで管理するという方法が取られている。
この方法により、コンポーネントツリーが大きなビューとなり、どのコンポーネントからでも状態へのアクセスやアクションをトリガーできる。
Vuex公式ドキュメントより引用
さらなる詳細は、公式ドキュメントがよく整理されていて良い!!しかも日本語訳されているのはありがたい
実装
環境構築
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
ファイル構成は、このように自動生成され、基本的には、app
以下のファイルを実装していく。
index.ejs
が最終的にページを描画するHTMLだが、特に編集する必要はない
<!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インスタンスを作成する。
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
からいじっていく
<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
)を介して、状態の変更が、リアクティブに更新される。
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アプリケーションであるため、以下のようなミューテーションとなる。
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'
そして、実際にミューテーションを実装していく。
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において、状態の変更を行う際には、ミューテーションをコミットすることで初めて状態の変更が可能となる。具体的なコミットの実装は以下のとおりである。
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
})
}
ここまでをおさらいすると、
- 状態管理に必要な各種モジュールをインポートしたStoreを作成
- 状態を変更するためのミューテーションを宣言的に定義
- 実際に状態を変更するためにミューテーションをコミットする
そして最後に実際のTodoアプリケーションの実装を進めていく。
全ソースはこちら
https://github.com/samuraikun/electron-vue-todo
今回のTodoアプリの要件としては、大雑把にこんな感じ
- 基本的なTodoの追加、編集、削除
- Todoが終わったかどうかはチェックのON/OFF
- 終わったTodoとまだ終わっていないTodoをフィルタリングして表示/非表示を切り替え
まずは、Todoの一覧リストそのものを実装していく。
全ての実装を載せると、記事が長くなりすぎるので(すでに長いが)、一部抜粋していくと、
まずテンプレート部分
<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 > remaining" @click="CLEAR_COMPLETED" class="clear-completed">Clear completed</button>
</footer>
</div>
...
...
</template>
v-for
で複数あるTodoを表示したり、Todoをフィルタリングするためのメソッドを呼び出している。
フィルタリングのロジックは、<script></sript>
タグ内で実装していく。
<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のフォームを作成
<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
は、コンポーネント内でミューテーションをコミットできるようにするヘルパーである。
<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>
これにて完成!!
感想
- 前評判通り、学習コストはフルスタックなAnguar、JSXを強いられるReactと違いシンプル
- 公式のドキュメントが日本語訳もされており、わかりやすい
- Reactのようにコンポーネントによる開発、Anguar1.xのように、ディレクティブやバインディングができるなど、他のフレームワークの良いところを上手く導入できている印象
今後
PHPフレームワークのLaravelがVue.jsを導入したり、私たちはなぜReactではなくVue.jsを選んだのかの記事で言われているように、React、Angularで違和感を感じた部分をVue.jsが補っている部分があるから人気が出ているかなあと感じた。
もしもだけれど、RailsもVue.jsを公式に導入してくれたら嬉しいなあ😆