2016年9月、人気のJavaScriptフレームワークVue.jsがv2をリリースしました。それ以来ぜひ使ってみたい、どのようなものか知りたいと思っていました。AngularとReactを使い慣れた者の1人としては、Vueが似ているところや違うところも知りたかったのです。
Vue.js 2.0はすばらしいパフォーマンスを誇ります。データサイズが比較的小さく(バンドルされるVueのランタイム版は一度最小化してgzip圧縮したら16KBしかありません)、Vueのvuexや、vue-routerのような付属の状態管理ライブラリーもアップデートされました。1つの記事ではとても収まりませんが、コアフレームワークと組み合わせて使えるライブラリーを今後、機会があれば詳しく取り上げたいと考えています。
ほかのライブラリーからの影響
記事を読み進めると、Vueの持つ機能のうち明らかにほかのフレームワークから着想を得たものがたくさんあるのに気が付くでしょう。これは良いことで、新しいフレームワークがほかのライブラリーからアイデアを得て改良されるのはすばらしいことです。特にVueのテンプレート機能はAngularによく似ていることに気付くでしょうが、一方でVueのコンポーネントとライフサイクルメソッドはReactとAngular 2に似ています。
一例として、ReactやそのほかのほぼすべてのJavaScriptフレームワークと同様に、Vueはレンダリング効率化のために仮想DOMの考え方を採用しています。Vueでは人気の仮想DOMライブラリーの1つsnabbdomのフォークを使っています。Vueのサイトには仮想DOMのレンダリングに関するドキュメントがありますが、ユーザーが知っておけばよいことは、Vueは高速なレンダリングが得意で(実際、多くの場合でReactよりも優れている)信頼性の高いプラットホームなので安心だ、ということです。
コンポーネント、コンポーネント、コンポーネント
ほかのフレームワークのように、Vueの核となる部分はコンポーネント(HTMLカスタム要素)です。Vueアプリケーションは、最終的なアプリを作るために積み重ねた一連のコンポーネント群なのです。Vueでは複数のコンポーネントを、ビルドツール(後述)で分解される1つの.vueファイル内で定義することを推奨(強制ではありません)している点で、一歩進んでいます。この記事の狙いはVueとその使い勝手がどのようなものかをくまなく探ることですので、サンプルアプリケーションでも推奨通りに進めていきます。
Vueのファイルは次のようになっています。
<template>
<p>This is my HTML for my component</p>
</template>
<script>
export default {
// all code for my component goes here
}
</script>
<style scoped>
/* CSS here
* by including `scoped`, we ensure that all CSS
* is scoped to this component!
*/
</style>
もしコンポーネントのすべてを1ファイルに含めるのが嫌ならば、それぞれの要素にsrc属性を加えて別のHTML、JS、CSSのファイルを指定する方法もあります。
プロジェクトのセットアップ
プロジェクト全体のセットアップを楽にするために優れたVue CLIが用意されていますが、個人的には新しいライブラリーを使い始めるときにはより理解を深めるためにすべてをゼロから作ることにしています。
最近のお気に入りのビルドツールはWebpackで、Vue.jsのコンポーネント形式をサポートするためにvue-loaderプラグインを組み合わせます。最後に必要なものはBabelとES2015プリセットで、すべてのコードをすてきなES2015の構文で書けるようになります。
Webpackの設定は、突き詰めれば以下のようになります。
- src/main.jsをWebpackでビルドし、build/main.jsに出力
- Webpackに対して、どの.vueファイルにもvue-loaderの使用を通知
- Webpackに対して、どの.jsファイルにもBabelの使用を通知
- vue-loaderに対して、すべてのJavaScriptのトランスパイルでBabelの使用を通知。したがって*.vueファイルがJavaScriptを含む場合、Babelで変換されコンパイルされる
module.exports = {
entry: './src/main',
output: {
path: './build',
filename: 'main.js',
},
module: {
loaders: [
{
test: /\.vue$/,
loader: 'vue',
},
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/,
},
],
},
vue: {
loaders: {
js: 'babel',
},
},
}
ようやくHTMLファイルを作る準備ができました。
<!DOCTYPE html>
<html>
<head>
<title>My Vue App</title>
</head>
<body>
<div id="app">
</div>
<script src="/build/main.js"></script>
</body>
</html>
appというIDを付けた空のdiv要素を作りましたが、この要素こそが今回のVueアプリケーションを配置する場所になります。ページの残りの部分もコントロールできるように、body要素ではなくdivを好んで使っています。
はじめてのVue.js 2.0アプリを書く
従来のすべてのプログラミングチュートリアルの慣例に倣って、複雑なプログラミングをする前に、スクリーンに「Hello World」を表示するVueアプリケーションを作ります。
各Vueアプリは、ライブラリーをインポートして新規にVueインスタンスを生成して作成します。
import Vue from 'vue'
const vm = new Vue({
el: '#app',
})
ページに表示するための要素をVueに渡せば、これでVueアプリケーションが作成できます。このアプリケーションに置き換えたい、HTML要素のセレクタをVueに渡しているわけです。つまりVueが実行されると、先に作ったdiv#appの部分がアプリケーションに置き換わります。
変数名をvmとした理由は「View Model」の略だからです。厳密には「モデル・ビュー・ビューモデル(MVVM)パターン」に則してはいないものの、Vueはある程度この考えを取り入れており、Vueアプリケーションの変数名にvmを使う慣例は定着しています。もちろん変数名は好きなように名付けてかまいません。
しかし、このアプリケーションは、今のところなにもしません。実際になにかをページに表示する最初のコンポーネントApp.vueを作ります。
Vueはアプリケーションはどのような構成かを規定しませんので、開発者次第です。例では結局、1つのコンポーネントにつき1つのフォルダーを作りました。Appフォルダー(これがコンポーネントであることを示すため大文字を使うことにしています)には4つのファイルが入っています。
- index.vue
- template.html
- script.js
- style.css
App/index.vueは単純にほかの3つのファイルをインポートします。
<template src="./template.html"></template>
<script src="./script.js"></script>
<style scoped src="./style.css"></style>
index.vueと名付けていますが、人によっては探しやすいapp.vueと名付けたいかもしれません。コード上でインポートするときApp/index.vueのほうがApp/app.vueよりも好きなので、繰り返しますがほかの人はそうでないかもしれませんから、開発者とそのチームがベストと思う名前を付けてかまいません。
いまのところテンプレートは<p>Hello World</p>だけで、CSSファイルは空白のままにします。メインの作業はscript.jsに移ります。内容は次のようになっています。
export default {
name: 'App',
data() {
return {}
},
}
この間に、後述する主にデバッグの目的で、Appと名付けたコンポーネントを作り、保有して管理するデータを定義します。いまはなにもデータはないので、Vueに空のオブジェクトを返すようするだけにします。のちほどデータを使ったコンポーネントの例も紹介します。
src/main.jsに戻って、VueインスタンスにAppコンポーネントをレンダリングするように通知します。
import Vue from 'vue'
import AppComponent from './App/index.vue'
const vm = new Vue({
el: '#app',
components: {
app: AppComponent,
},
render: h => h('app'),
})
始めにコンポーネントをインポートします。Webpackとvue-loaderにコンポーネントの解析をします。これは重要なステップです。Vueコンポーネントは初期状態ではグローバルで使えるようになっていないからです。各コンポーネントは、使いたいほかのコンポーネントすべてのリストと、名前に対応したタグ(キー)を持つ必要があります。今回、コンポーネントを以下ように登録しました。
components: {
app: AppComponent,
}
テンプレートでこのコンポーネントを参照するにはapp要素を使えばよい、ということになります。
最後にrender関数を定義します。この関数は、要素を生成するヘルパー(主にhと称される)と共に呼び出されます。Reactで使うReact.createElement関数と大きくは異なりません。今回はレンダリングしたいコンポーネントがappというタグを持つので、文字列'app'を引数とします。
以降、HTMLテンプレートを定義するので、ほかのコンポーネントではrender関数を使わないでしょう。しかし、もっと深く知りたい場合は、Vue.jsのrender関数のガイドを一読すると良いでしょう。
完了したら、画面に「Hello World」が現れるはずです。Vue.jsでアプリができました。
Vue Devtools
もう少し複雑なVueアプリに入る前に強調しておきたいのが、Vue Devtoolsは間違いなくインストールしたほうが良い、ということです。Chromeのデベロッパーツールでアドオンすると、作ったアプリの中身とやり取りされるプロパティ、各コンポーネントの状態、そのほかいろいろなことが確認できるのでとても便利です。
アプリを作成する
サンプルアプリとして、GitHub APIを使って、ユーザー名を入力するとそのユーザーに関するGitHub上の統計を見られるアプリケーションを作ります。GitHub APIを取り上げた理由は、使ったことのある人も多く、認証無しで使用でき、情報量も十分だからです。
アプリケーションの作成を始める前に考えたいのが、どのようなコンポーネントが必要かということです。今回のAppコンポーネントは、これまでに加えて2つの別のコンポーネントを使います。ユーザーからの入力を受け取るためのGithubInput、ユーザー情報を画面に表示するGithubOutputです。入力についてから説明していきます。
Vue.jsの入力フォーム
はじめに、コンポーネントのテンプレート、スクリプト、CSSファイルをロードするsrc/GithubInput/index.vueを作ります。このコンポーネントを作る際に1つだけ通常と異なるのは、コンポーネントに結びついたデータを1つ作成している点です。Reactのstateの考え方によく似ています。
export default {
name: 'GithubInput',
data() {
return {
username: '',
}
}
}
このコンポーネントがusernameというデータを持ち、管理することを示しています。のちほどユーザーの入力でデータが更新されます。
現在、テンプレートsrc/GithubInput/template.htmlには<p>github input</p>があるだけです。のちほどこれを正しく埋めていきます。新しいコンポーネントを作るときは、ダミーHTMLを入れて正しくテンプレートができあがったかチェックするのが良いでしょう。
最後に、このコンポーネントを画面に表示するためAppコンポーネントに登録します。レンダリングするのはAppコンポーネントだからです。
登録するため、src/App/script.jsを書き換えてGithubInputを読み込みます。
import GithubInput from '../GithubInput/index.vue'
export default {
name: 'App',
components: {
'github-input': GithubInput,
},
data() {
return {}
},
}
アプリのコンポーネントのテンプレートを書き換えます。
<div>
<p>Hello World</p>
<github-input></github-input>
</div>
Vueコンポーネントの制約(AngularとReactでも同様)として、各コンポーネントは1つのルートノードを持っていなければなりません。コンポーネントが複数の要素に対してレンダリングする際、通常divタグで、すべてを1つに囲んでおくことが重要です。
フォームへの入力の監視
GithubInputコンポーネントの役割は以下の2つです。
- 入力欄の現在の値を監視する
- 値が変更されたことを通知して、ほかのコンポーネントがそれに合わせて状態を更新できるようにする
input要素が入ったformを作成して最初のバージョンを作ります。Vueに組み込まれている命令でフォームの値を監視できます。GithubInputのテンプレートは次のようになります。
<form v-on:submit.prevent="onSubmit">
<input type="text" v-model="username" placeholder="Enter a github username here" />
<button type="submit">Go!</button>
</form>
2つの重要な属性が登場しました。v-onとv-modelです。
v-onは、VueとDOMイベントを結びつけて関数を呼び出すための手段です。たとえば<p v-on:click="foo">Click me!</p>なら、クリックされるたびにこのコンポーネントのfooメソッドが呼ばれます。もしイベントハンドリングについての詳細を確認したいならVue公式ドキュメントのイベントハンドリングの項がおすすめです。
v-modelはフォームの入力とコンポーネントの持つデータとの間で双方向のデータバインディングをします。サイトの裏では、v-modelが効果的にフォームの変更イベントを監視し、入力内容に合わせてVueコンポーネントのデータを変更しているのです。
上のテンプレートを考慮して、例ではフォームのデータを扱うためにv-onとv-modelを下のように使います。
- v-on:submit.prevent="onSubmit"は、フォームが送信されたときにonSubmitメソッドが実行されるようにセットする。.preventを加えでデフォルト処理の実行を防ぐ。もしVueに.preventが無かったら、event.preventDefault()を使うが、Vueの機能を使うほうが良い
- v-model:usernameは、入力された値をコード中のusernameの値とバインドしている。Angularに慣れていればng-modelとよく似ていことに気付くかもしれない。先にGithubInputを作ったとき、usernameというデータを持つよう宣言したが、いまここでデータと入力欄とが結びついた。これでこの2つは常に自動で同期される
コンポーネントのJavaScriptに戻ってonSubmitメソッドの宣言をします。名前は完全に任意ですので好きなようにできますが、慣例通り関数名は起動するイベントから取った名前を付けたほうが良いでしょう。
name: 'GithubInput',
methods: {
onSubmit(event) {
if (this.username && this.username !== '') {
}
}
},
データは直接thisで参照でるので、this.usernameとすればテキストボックスの最新の値が得られます。もし空でなければデータが変更されたことをほかのコンポーネントに通知したいからです。そのためにはメッセージバス(bus)を使います。コンポーネントがイベントを実行したり、ほかのイベントを監視するのに使えるオブジェクトです。アプリケーションが大きくなってきたら、機会があれば紹介したいVuexのような、もっと構造的なアプローチを検討したほうが良いでしょう。今回は、メッセージバスで十分です。
ここで良いお知らせです。メッセージバスとして空のVueインスタンスが使用でき、Vueのドキュメントでも推奨しています。単にVueインスタンスを生成するだけのsrc/bus.jsを作りエクスポートします。
import Vue from 'vue'
const bus = new Vue()
export default bus
GithubInput内でbusモジュールをインポートし、ユーザー名が変更されたときに$emit()メソッドでイベントを起動して使用します。
import bus from '../bus'
export default {
...,
methods: {
onSubmit(event) {
if (this.username && this.username !== '') {
bus.$emit('new-username', this.username)
}
}
},
...
}
これでフォームが完成したので、実行結果のデータを使う用意が整いました。
GitHubから返ってきた実行結果を表示する
前と同じ構造を使ってGithubOutputコンポーネントを作ります。ユーザー名の変更を知る必要があるので、GithubOutput/script.jsの中ではbusモジュールもインポートします。このコンポーネントが持つデータは「GitHubユーザー名」とそれに該当する「GitHub APIから得たデータ」との対応がマッピングされているオブジェクトです。つまり、APIにリクエストを毎回送信しなくても良いのです。もし以前にデータを取得していれば、単に再利用すれば良いのです。また、最後に取得したユーザー名を保存しておけば、どのデータを画面に表示するかを決められます。
import bus from '../bus'
export default {
name: 'GithubOutput',
data() {
return {
currentUsername: null,
githubData: {}
}
}
}
コンポーネントができたら、メッセージバス上で実行されたどのnew-usernameイベントもキャッチしたいわけです。ありがたいことにVueでは、createdを含むたくさんのライフサイクルフックをサポートしています。責任ある開発者なら、destroyedイベントでこのコンポーネントが破壊されたら、今度はイベントの監視を停止しましょう。
export default {
name: 'GithubOutput',
created() {
bus.$on('new-username', this.onUsernameChange)
},
destroyed() {
bus.$off('new-username', this.onUsernameChange)
},
...
}
呼び出されるonUsernameChangeメソッドを定義してcurrentUsernameプロパティをセットします。
methods: {
onUsernameChange(name) {
this.currentUsername = name
}
},
現在のスコープにonUsernameChangeメソッドを明示的にバインドする必要はありません。Vueコンポーネントにメソッドを定義すると、Vueは自動でmyMethod.bind(this)をコールするのでメソッドは常にコンポーネントにバインドされます。これが、コンポーネントのメソッドをmethodsオブジェクト内で定義する理由の1つです。Vueが把握し合わせてセットアップできるのです。
条件付きレンダリング
コンポーネントが最初に作られたときのように、ユーザー名がまだ無いときにはユーザーに対しメッセージを表示します。Vueにはたくさんの条件付きレンダリング方法がありますが、一番簡単なのはv-ifディレクティブで、条件を設定して要素が存在するときだけレンダリングする方法です。v-elseと組み合わせても使えます。
<div>
<p v-if="currentUsername == null">
Enter a username above to see their Github data
</p>
<p v-else>
Below are the results for {{ currentUsername }}
</p>
</div>
ここもまた、Angularの開発者にとってとてもなじみのあるものでしょう。トリプルイコール(===)ではなくダブルイコール(==)を使うのは、currentUsernameがnullのときだけでなく未定義(undefined)のときにも条件式がtrueになるようにしたいからです。null==undefinedならばtrueです。
GitHubからデータを取得する
Vue.jsには組み込みのHTTPライブラリーは含まれていませんが、ちゃんとした理由があります。最近は多くのブラウザーでfetch APIをネイティブサポートしています(執筆時点でIE11、Safari、iOSのSafariは未対応)。記事ではポリフィルを使用しませんが、もし必要ならブラウザー間のAPI対応の差を埋めるポリフィルが使えます。もし、fetch APIがいやならば数多くのサードパーティ製のHTTP向けライブラリーがあります。VueのドキュメントではAxiosが紹介されています。
Vueのようなフレームワークに、HTTPライブラリーを含めないことを強くおすすめします。バンドルサイズを小さくできて、開発者が一番良いと思うライブラリーを選べ、APIとのやり取りで必要ならリクエストをカスタマイズできます。記事ではfetch APIを使いますが、好きなAPIと取り替えてもかまいません。
もしfetch APIについて知りたいならLudovico Fischerの『Introduction to the Fetch API』を読めばきっと理解できるでしょう。
HTTPリクエストを送信するためにコンポーネントに別のメソッドfetchGithubDataを加えます。GitHub APIにリクエストを送り、結果を保存します。また、すでにそのユーザーのデータが存在するかどうかをチェックして、もしもあればリクエストを送りません。
fetchGithubData(name) {
// if we have data already, don't request again
if (this.githubData.hasOwnProperty(name)) return
const url = `https://api.github.com/users/${name}`
fetch(url)
.then(r => r.json())
.then(data => {
// in here we need to update the githubData object
})
}
ここまで来れば、ユーザー名が変更されたときにこのメソッドを実行するだけです。
onUsernameChange(name) {
this.currentUsername = name
this.fetchGithubData(name)
}
もう1つ気に留めるべき別のポイントは、Vueが取り扱うデータがいつ更新されたかを知るためのデータ監視方法です。これについて詳しく説明したリアクティビティ(Reactivity)ガイドがありますが、本質的にVueは開発者がオブジェクトのプロパティを追加したり削除したりしたことを魔法のように知ることはできないのです。だからもし以下のようにすると、Vueはこれに気付かず、ビューを更新しません。
this.githubData[name] = data
代わりに特別なメソッドVue.setを使いキーを追加したことをVueに対して明示的に通知します。上のコードは、下のようになります。
Vue.set(this.githubData, name, data)
このコードはthis.githubDataに変更を加え、ここで渡したキーと値を追加します。またVueに変更を通知してレンダリングできるようにします。
コードは次のようになります。
const url = `https://api.github.com/users/${name}`
fetch(url)
.then(r => r.json())
.then(data => {
Vue.set(this.githubData, name, data)
})
画面にデータを表示するためのビューコードをまだ書いていないものの、フォームにユーザー名を入力でき、Vueのdevtoolsを見ればGitHubからリクエストされたデータが見られるはずです。これは、このツールがいかに便利で強力なものかを示しています。どのコンポーネントのローカル状態も調べられ、なにが起きているのかを正確に把握できます。
ビューに統計データを表示する
データを表示するために、テンプレートを更新します。次のコードを別のv-ifディレクティブで囲んで、リクエストが終わったときにだけデータを表示するようにします。
<div v-if="githubData[currentUsername]">
<h4>{{ githubData[currentUsername].name }}</h4>
<p>{{ githubData[currentUsername].company }}</p>
<p>Number of repos: {{ githubData[currentUsername].public_repos }}
</div>
これでGitHubから得た詳細情報を画面に表示できました、アプリの完成です。
コードのリファクタリング
明らかに改良できる点があります。GitHubのデータを表示するHTMLは、データのほんの一部のみ、現在のユーザー統計しか使いませんでした。しかし、ほかのコンポーネントでもユーザーデータさえ渡せば表示できる完璧な状態です。
例で作ったほかのコンポーネントと同じ構造で、GithubUserDataコンポーネントを作ります。小さな違いは、このコンポーネントはユーザーのデータであるdataというプロパティがあるだけです。このプロパティ(コードではprops)はコンポーネントが親オブジェクトから渡されるデータで、Vueでもその挙動はReactのものとほぼ同じです。Vueではコンポーネントが必要とする各プロパティを明示的に宣言します。このコンポーネントは1つのプロパティdataを持つと宣言します。
export default {
name: 'GithubUserData',
props: ['data'],
data() {
return {}
}
}
Vueのとても良いところの1つは、明示的でなければならないという点です。すべてのプロパティ、データ、コンポーネントが使うほかのコンポーネントは、明示的に宣言されます。そのためコードが分かりやすく、プロジェクトがもっと大きく複雑になったときにとても助かるでしょう。
新しいテンプレートでは前回とまったく同じHTMLですが、githubData[currentUsername]ではなくdataを参照できます。
<div v-if="data">
<h4>{{ data.name }}</h4>
<p>{{ data.company }}</p>
<p>Number of repos: {{ data.public_repos }}
</div>
このコンポーネントを使うにはGithubOutputコンポーネントの変更が必要です。最初にGithubUserDataをインポートして登録します。
import bus from '../bus'
import Vue from 'vue'
import GithubUserData from '../GithubUserData/index.vue'
export default {
name: 'GithubOutput',
components: {
'github-user-data': GithubUserData,
},
...
}
コンポーネントの宣言時にはどのような名前でも付けられるので、github-user-dataの部分は好きなようにしてかまいません。アドバイスを1つすると、コンポーネントの名前にはダッシュ(-)を含めるとよいでしょう。Vueでは強制していませんが、カスタム要素に関するW3Cの仕様では、HTMLの将来のバージョンで加わるかもしれない要素名との重複を避けるため、ダッシュを含まなければならないと書かれています。
一度コンポーネントを宣言したら、テンプレート内で使用できます。
<p v-else>
Below are the results for {{ currentUsername }}:
<github-user-data :data="githubData[currentUsername]"></github-user-data>
</p>
重要なポイントは、コンポーネントにdataプロパティを渡す方法です。
:data="githubData[currentUsername]"
属性の、はじめのコロン(:)が大切です。渡す属性が動的でデータが変更されるたびに更新しなければならない、とVueに通知しているのです。VueはgithubData[currentUsername]の値を調べて、データが変更されてもGithubUserDataコンポーネントが最新のデータになっているようにします。
もし:dataが短すぎて魔術みたいだと感じるなら、もっと長いv-bind構文も使えます。
v-bind:data="githubData[currentUsername]"
この2つは同じことなので、好きなほうを使ってかまいません。
最後に
これでVue.js 2.0 GitHubアプリケーションはかなり良い感じになりました。すべてのコードはGitHubで入手可能で、アプリがオンラインで稼働している状態も確認できます。
Vueの良い評判ばかり耳にしていたので、大きな期待をしていました。そして期待通りだったのでとてもうれしく思います。Vueで開発するのは、Reactの良い部分を抜き出してAngularの良い部分と併せたような感覚です。ディレクティブ(たとえばv-if、v-else、v-modelなど)は本当に取り掛かりやすいものです。そしてReactのJSX構文で条件式を作るよりも理解しやすいです。しかも、Vueのコンポーネントの仕組みは、Reactにとても似ています。
開発するシステムを小さなコンポーネントに分解するように推奨されていますが、全体的に見てとてもシームレスな使い勝手でした。またドキュメント類に関してもVueチームを称賛せずにはいられません。本当にすばらしいドキュメントです。ガイダンス類が非常に良くまとまっていて、APIリファレンスも詳細に書かれていますが、それでいて求めているものをずばり見つけやすいのです。
この記事を楽しく読めて、さらに学びたいなら、Vue.js公式サイトから始めるのがベストでしょう。
※本記事はVildan Softicが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します!
※本記事は、ゲストライターJack Franklinによるものです。SitePointのゲスト投稿では、Webコミュニティの著名な執筆者や講演者の魅力的なコンテンツの提供を目指しています。
(原文:Getting up and Running with the Vue.js 2.0 Framework)
[翻訳:西尾健史/編集:Livit]