コンポーネント
コンポーネントとは何か?
コンポーネントは Vue.js の最も強力な機能の1つです。基本的な HTML 要素を拡張して再利用可能なコードのカプセル化を助けます。高度なレベルでは、コンポーネントは Vue.js のコンパイラが指定された振舞いをアタッチするカスタム要素です。場合によっては、特別な is
属性で拡張されたネイティブな HTML 要素の姿をとることもあります。
コンポーネントの使用
登録
以前のセクションで Vue.extend()
を使用してコンポーネントコンストラクタを作成できることを学習しました:
var MyComponent = Vue.extend({ |
このコンストラクタをコンポーネントとして使用するためには、 Vue.component(tag, constructor)
で登録する必要があります:
// グローバルに my-component タグでコンポーネントを登録する |
カスタムタグの名前について W3C ルール (全て小文字で、ハイフンが含まれている必要がある)にしたがうことは良い取り組みと考えられますが、Vue.js はそれを強制しないことを覚えておいてください。
一度登録すると、コンポーネントはカスタム要素 <my-component>
として親のインスタンスのテンプレートで使用できます。コンポーネントは root の Vue インスタンスをインスタンス化する前に登録しているか確認してください。ここに完全な例を示します:
<div id="example"> |
// 定義する |
レンダリングされる内容は以下になります:
<div id="example"> |
カスタム要素はマウントポイントとして機能するだけで、コンポーネントのテンプレートはそれに取って代わることに注意してください。この振舞いは、replace
インスタンスオプションを使用することで設定できます。
また、コンポーネントは el
オプションによるマウントの代わりにテンプレートが提供されるということに注意してください。root な Vue インスタンス (new Vue
を使用して定義された) だけは、マウントするために el
を含みます。
ローカル登録
グローバルに全てのコンポーネントを登録する必要はありません。別のコンポーネントのインスタンスオプションの components
に登録することで、そのコンポーネントのスコープ内でのみ利用可能なコンポーネントを作成できます:
var Child = Vue.extend({ /* ... */ }) |
同じカプセル化は、ディレクティブ、フィルタ、そしてトランジションのようなアセットタイプに対して適用されます。
簡単な登録
物事を簡単にするため、実際のコンストラクタの代わりに Vue.component()
と component
オプションにオプションオブジェクトで直接渡すことができます。Vue.js は内部で自動的に Vue.extend()
を呼びます:
// 1 ステップで extend と登録します |
コンポーネントオプションの注意事項
Vue コンストラクタに渡すことができるほとんどのオプションは、 Vue.extend()
で使用できます。しかし data
と el
の2つの特別なケースは異なります。純粋に Vue.extend()
へ data
としてオブジェクトを渡すことを考えてみてください:
var data = { a: 1 } |
これに伴う問題は、同じ data
オブジェクトは MyComponent
の全てのインスタンス間で共有されるということです!これは望むところではまずないでしょうから、data
オプションとして、新たなオブジェクトを返す関数を使用しましょう:
var MyComponent = Vue.extend({ |
全く同じ理由で、el
オプションも Vue.extend()
で使用した場合、関数の値が必要です。
テンプレートの構文解析
Vue.js のテンプレートエンジンは DOM ベースで、独自のものではなくブラウザに付属するネイティブの構文解析を使用します。文字列ベースのテンプレートと比べたときこのアプローチは利点がありますが、注意事項もあります。テンプレートは各自妥当な HTML の集まりでなければなりません。 HTML 要素の中には、その内部にどの要素が表示できるかの制限を持つものがあります。これらの制限の代表的なものは:
a
は他のインタラクティブな要素に含むことができません (例、ボタンや他のリンク)li
はul
またol
の直接的な子になるべきで、そしてul
とol
はどちらもli
だけを含めることができますoption
はselect
の直接的な子になるべきで、そしてselect
はoption
(そしてoptgroup
) だけを含めることができますtable
はthead
、tbody
、tfoot
そしてtr
だけを含めることができ、さらにこれらの要素はtable
の直接的な子になるべきですtr
はth
そしてtd
だけを含めることができ、そしてこれらの要素はtr
の直接的な子になるべきです
実行時にこれらの制限が予期せぬ動作を引き起こす可能性があります。単純なケースでは動作するように見えるかもしれませんが、カスタム要素がブラウザによる検証前に展開されることを当てにしてはいけません。例えば、<my-select><option>...</option></my-select>
は、最終的には <select>...</select>
に展開されるとしても、妥当なテンプレートではありません。
また、select
、table
そして他の同様の制限を持つ要素の内部でカスタムタグ(カスタム要素と <component>
、<template>
や <partial>
のような特別なタグを含む)を使用することはできません。カスタム要素は外に押し出され、正しく表示されないでしょう。
カスタム要素の代わりに、特別な属性 is
を使いましょう:
<table> |
<table>
内で <template>
を使う代わりに、<tbody>
を使いましょう。テーブルは複数の tbody
を持つことを許されていますから:
<table> |
Props
Props によるデータ伝達
全てのコンポーネントインスタンスは、各自の隔離されたスコープ (isolated scope) を持ちます。つまり、子コンポーネントのテンプレートで親データを直接参照できない(そしてすべきでない)ということです。データは props を使用して子コンポーネントに伝達できます。
“prop” は、コンポーネントデータ上のフィールドで、そのコンポーネントデータは親コンポーネントから伝えられることを想定しています。子コンポーネントは、props
オプションを利用して、伝達を想定する props を明示的に宣言する必要があります:
Vue.component('child', { |
すると以下のようにプレーン文字列を渡すことができます:
<child msg="hello!"></child> |
結果:
キャメルケース 対 ケバブケース
HTML の属性は大文字と小文字を区別しません。キャメルケースされた prop 名を属性として使用するとき、それらをケバブケース (kebab-case: ハイフンで句切られた) にして使用する必要があります:
Vue.component('child', { |
<!-- HTML ではケバブケース --> |
動的な Props
式に通常の属性をバインディングするのと同様に、 v-bind
を使用して親のデータに props を動的にバインディングすることもできます。親でデータが更新される度に、そのデータが子に流れ落ちます:
<div> |
v-bind
のための省略記法を使用するとよりシンプルです:
<child :my-message="parentMsg"></child> |
結果:
リテラル 対 動的
初心者にありがちな誤りは、リテラル構文を使用して数を渡そうとすることです:
<!-- これは純粋な文字列"1"を渡します --> |
しかしながら、これはリテラルな prop なので、その値は実際に数の代わりに純粋な文字列 "1"
が渡されています。実際に JavaScript の数を渡したい場合は、その値が JavaScript 式として評価されるよう、動的な構文で使用する必要があります:
<!-- これは実際の数を渡します --> |
Prop バインディングタイプ
デフォルトで、全ての props は子プロパティと親プロパティとの間で one way down バインディングを形成します:親プロパティが更新するとそれは子へと流れ落ちますが、その逆はありません。このデフォルトは、子コンポーネントが誤って親の状態を変更しないようにするためで、そうしないとアプリケーションのデータフローが推理しづらくなってしまいます。しかしながら、.sync
そして .once
バインディングタイプ修飾子 (binding type modifier) による two-way または one-time バインディングを明示的に強いることも可能です:
構文の比較:
<!-- デフォルトは one-way-down バインディング --> |
two-way バインディングは子の msg
プロパティの変更を親の parentMsg
プロパティに返して同期します。one-time バインディングは、一度セットアップしたら、その先の変更を親子間で同期しません。
もし、渡される prop がオブジェクトまたは配列ならば、それは参照渡しであることに注意してください。子の内部でオブジェクトまたは配列そのものを変更することは、使用しているバインディングのタイプに関係なく、親の状態に影響を与えます。
Prop 検証
コンポーネントは受け取る props に対する必要条件を指定することができます。これらの検証要件は実質的にコンポーネントの API を構成し、ユーザーがコンポーネントを正しく使用していることを確実にするので、他の人に使用されることを意図したコンポーネントを作成するときに便利です。文字列の配列として props を定義する代わりに、検証要件を含んだオブジェクトハッシュフォーマットを使用できます:
Vue.component('example', { |
type
は次のネイティブなコンストラクタのいずれかになります:
- String
- Number
- Boolean
- Function
- Object
- Array
加えて、type
はカスタムコンストラクタ関数とすることもでき、アサーションは instanceof
チェックで作成できるでしょう。
prop 検証が失敗すると、Vue は子コンポーネントへの値のセットを拒否します。そしてもし開発ビルドを使用している場合は警告を出します。
親子間の通信
親チェーン
子コンポーネントは this.$parent
として親コンポーネントへのアクセスを保持しています。root な Vue インスタンスは this.$root
として子孫の全てにおいて利用できます。各親コンポーネントは全ての子コンポーネントを含んだ this.$children
という配列を持っています。
親チェーンであらゆるインスタンスにアクセスできますが、子コンポーネント内で親データに直接依存するのは避け、props を明示的に使用してデータを渡すようにすべきです。さらに、子コンポーネントから親状態を変化させるのは非常にまずい考えです。なぜなら:
親と子を密結合にしてしまいます。
親単体を見てその状態を推理することをとても困難にします。なぜならその状態が全ての子によって変更される可能性があるからです!理想的には、コンポーネントそれ自身のみに、自身の状態の変更を許すべきです。
カスタムイベント
全ての Vue インスタンスはコンポーネントツリー内の通信を容易にするカスタムイベントのインタフェースを実装します。このイベントシステムはネイティブの DOM イベントからは独立しており、動作が異なります。
各 Vue インスタンスは Event Emitter で、以下が可能です:
$on()
を使用してイベントをリッスンします。$emit()
を使用して自身にイベントをトリガーします。$dispatch()
を使用して親のチェーンに沿って上方に伝ぱするイベントを送出します。$broadcast()
を使用して全ての子孫に下方に伝ぱするイベントをばらまきます。
DOM イベントとは異なり、Vue のイベントは、コールバックが明示的に true
を返さない限り、伝ぱ経路に沿って初めてコールバックをトリガした後、自動的に伝搬を停止します。
シンプルな例:
<!-- 子向けのテンプレート --> |
// 現在のメッセージでイベントを送出する子を登録します |
Messages: {{ messages | json }}
カスタムイベントに対する v-on
上記の例はかなりいいですが、親コンポーネントのコードを見ている時、"child-msg"
イベントがどこから来るのかあまりはっきりしません。テンプレートの、ちょうど子コンポーネントが使用されている場所でイベントハンドラを宣言することができれば、なおよいでしょう。これを可能にするために、v-on
は子コンポーネントで使用されたとき、カスタムイベントをリッスンするために使用することができます:
<child v-on:child-msg="handleIt"></child> |
これで非常に明確になります。子が "child-msg"
イベントをトリガーすると、親の handleIt
メソッドが呼び出されます。親の状態に影響を与えるあらゆるコードは親メソッドの handleIt
内だけに存在し、子コンポーネントはイベントのトリガーにかかわるのみです。
子コンポーネントの参照
props やイベントの存在にもかかわらず、時には子コンポーネントに JavaScript で直接アクセスする必要があるかもしれません。それを実現するためには v-ref
を用いて子コンポーネントに対して参照 ID を割り当てる必要があります。例えば:
<div id="parent"> |
var parent = new Vue({ el: '#parent' }) |
v-ref
が v-for
と共に使用された時は、得られる値はデータソースをミラーリングした子コンポーネントが格納されている配列またはオブジェクトになります。
スロットによるコンテンツ配信
コンポーネントを使用するとき、それは、しばしばこのようにコンポーネントを構成することが望まれます:
<app> |
ここに言及すべきことが2つあります:
<app>
コンポーネントはどのコンテンツがそのマウント対象内部に存在しているか分かりません。<app>
を使用している親コンポーネントが何があれ、親コンポーネントが内部コンテンツを決定します。<app>
コンポーネントはほぼ必ず独自のテンプレートを持っています。
コンポーネントの構造を動作させるためには、親の”コンテンツ”とそのコンポーネント自身のテンプレートを織り交ぜる方法が必要です。これは”コンテンツ配信”(または、Angular に精通している場合は “transclusion”)と呼ばれるプロセスです。Vue.js はオリジナルコンテンツに対する配信アウトレットとして機能する特別な <slot>
要素を使用して、現行の Web Components spec draft にならったコンテンツ配信 API を実装します。
コンパイルスコープ
API を掘り下げる前に、はじめにコンテンツがコンパイルされているスコープを明確にしましょう。このようなテンプレートを考えてみてください:
<child-component> |
msg
は親のデータと子のデータのどちらにバインドされるべきでしょうか?答えは親です。コンポーネントスコープに対するシンプルな経験則は:
親テンプレート内の全てのものは親のスコープでコンパイルされ、子テンプレート内の全てものは子のスコープでコンパイルされる
よくある間違いは、親テンプレート内の子のプロパティ/メソッドにディレクティブをバインドしようとすることです:
<!-- 動作しません --> |
someChildProperty
は子コンポーネントのプロパティと仮定すると、上記例は意図したように動作しないでしょう。親のテンプレートは子コンポーネントの状態について認識しているべきではありません。
コンポーネントで子スコープのディレクティブにバインドする必要がある場合、子コンポーネント自身のテンプレートにおいてそうすべきです:
Vue.component('child-component', { |
同様に、配信コンテンツは親スコープでコンパイルされます。
単一スロット
親コンテンツは子コンポーネントのテンプレートが少なくとも1つの <slot>
アウトレットを含んでいない限り破棄されます。属性なしのスロットが1つだけあるときは、全コンテンツはスロットそのものを置き換え、DOM 内のその位置に挿入されます。
<slot>
タグ内に元々あった全てのものは、フォールバックコンテンツと見なされます。フォールバックコンテンツは子スコープでコンパイルされ、ホストしている要素が空で挿入されるコンテンツがない場合にのみ、表示されます。
以下のテンプレートによるコンテンツがあるとします:
<div> |
このコンポーネントを使用した親のマークアップは以下になります:
<my-component> |
レンダリング結果は以下になります:
<div> |
名前付きスロット
<slot>
要素は特別な属性 name
を持ち、コンテンツを配信する方法をカスタマイズするために使用できます。異なる名前で複数のスロットを持つことができます。名前付きスロットは、コンテンツ内の対応する slot
属性を持つ任意の要素にマッチします。
マッチしなかったコンテンツのためのキャッチオールアウトレットの機能を持つデフォルトスロットとして、名前無しのスロットを残すことができます。デフォルトスロットがない場合は、マッチしなかったコンテンツは破棄されます。
例として、以下のテンプレートのような、多数のコンポーネント挿入のテンプレートがあると仮定します:
<div> |
親のマークアップは以下です:
<multi-insertion> |
レンダリングされる結果は以下になります:
<div> |
コンテンツ配信 API は、組み合わせて使うことを意図したコンポーネントを設計する際に、非常に便利なメカニズムです。
動的コンポーネント
予約された <component>
要素と、その is
属性に動的にバインドすることで、同じマウントポイントで複数のコンポーネントを動的に切り替えることができます:
new Vue({ |
<component :is="currentView"> |
keep-alive
状態を保持したり再レンダリングを避けたりするために、もし切り替えで取り除かれたコンポーネントを生きた状態で保持したい場合は、ディレクティブのパラメータ keep-alive
を追加することができます:
<component :is="currentView" keep-alive> |
activate
フック
コンポーネントを切り替えるとき、後任のコンポーネントは、前もって何らかの非同期操作を実行する必要があるかもしれません。コンポーネントの交換のタイミングを制御するには、後任のコンポーネントで activate
フックを実装します:
Vue.component('activate-example', { |
activate
フックが有効なのは動的コンポーネントの切り替えの間、または静的コンポーネントの初回レンダリング時だけということに注意してください。インスタンスメソッドによる手動挿入には作用しません。
transition-mode
transition-mode
パラメータ属性は、2つの動的コンポーネント間でのトランジションがどう実行されるかを指定できます。
デフォルトでは、入ってくるコンポーネントと出て行くコンポーネントのトランジションが同時に起こります。この属性によって、もう2つのモードを設定することができます:
in-out
: 新しいコンポーネントのトランジションが初めに起こり、そのトランジションが完了した後に現在のコンポーネントの出て行くトランジションが開始します。out-in
: 現在のコンポーネントが出て行くトランジションが初めに起こり、そのトランジションが完了した後に新しいコンポーネントのトランジションが開始します。
例
<!-- 先にフェードアウトし, その後フェードインします --> |
.fade-transition { |
その他
コンポーネントと v-for
通常の要素のように、カスタムコンポーネントで v-for
を直接使用することができます:
<my-component v-for="item in items"></my-component> |
しかしながら、コンポーネントは各自隔離されたスコープを持っているので、これではコンポーネントにデータが渡りません。コンポーネントに反復されたデータを渡すために、props を使用する必要があります:
<my-component |
コンポーネントに item
を自動的に注入しない理由は、それをするとコンポーネントが v-for
の動作に密結合されるからです。データがどこから来たものかを明示することが、コンポーネントを他の状況で再利用可能なものにします。
再利用可能なコンポーネントの作成
コンポーネントを作成するとき、このコンポーネントをどこかで再利用するつもりかどうかを心に留めておくとよいでしょう。一度限りのコンポーネントが互いに密結合を持つことはよしとしても、再利用可能なコンポーネントはきれいな公開インタフェースを定義するべきです。
Vue.js コンポーネントのための API は、本質的に、props 、events 、slots の3つの部分からなります:
Props 外部環境がコンポーネントにデータを供給することを可能にします。
Events コンポーネントが外部環境のアクションをトリガーすることを可能にします。
Slots 外部環境がコンポーネントの view 構造にコンテンツを挿入することを可能にします。
v-bind
と v-on
用の省略記法を使うと、意図を明確かつ簡潔にテンプレート内で伝えることができます:
<my-component |
非同期コンポーネント
大規模アプリケーションでは、アプリケーションを小さな塊に分割して、実際に必要になったときにサーバからコンポーネントをロードするだけにする必要があるかもしれません。それを簡単にするために、Vue.js ではコンポーネント定義を非同期的に解決するファクトリ関数としてコンポーネントを定義することができます。Vue.js はコンポーネントが実際に描画が必要になるとファクトリ関数のトリガだけ行い、将来の再描画のために結果をキャッシュします。例えば:
Vue.component('async-example', function (resolve, reject) { |
ファクトリ関数は、サーバからコンポーネント定義を取得した後で呼ばれる resolve
コールバックを引数に持ちます。ロードが失敗したことを示すために、reject(reason)
を呼びだすこともできます。ここでの setTimeout
は単にデモのためのものです。どうやってコンポーネントを取得するかは完全にあなた次第です。推奨されるアプローチの1つは Webpack のコード分割機能で非同期コンポーネントを使うことです。
Vue.component('async-webpack-example', function (resolve) { |
アセットの命名規則
コンポーネントやディレクティブのようなあるアセットは、HTML 属性または HTML カスタムタグの形でテンプレートに表示されます。HTML 属性名とタグ名は大文字と小文字を区別しない (case-insensitive) ため、しばしばキャメルケースの代わりにケバブケースを使用してアセットに名前をつける必要がありますが、これは少し不便です。
Vue.js は実はキャメルケースまたはパスカルケース (PascalCase) を使用してのアセットの命名をサポートし、それらをテンプレート内ではケバブケースとして自動的に理解します (props の名前変換と似ています):
// コンポーネント定義します |
<!-- テンプレートではダッシュケースを使用します --> |
これは ES6 オブジェクトリテラル省略記法 でうまく動作します:
// PascalCase |
再帰的なコンポーネント
コンポーネントはそのテンプレートで自分自身を再帰的に呼びだすことができます。ただし、それができるのは name
オプションがあるときだけです:
var StackOverflow = Vue.extend({ |
上記のようなコンポーネントは、”max stack size exceeded” エラーに想定されるため、再帰呼び出しは条件付きになるようにしてください。Vue.component()
を使用してグローバルなコンポーネントを登録するとき、そのグローバル ID が自動的にコンポーネントの name
オプションとして設定されます。
フラグメントインスタンス
template
オプションを使用するとき、テンプレートのコンテンツは Vue インスタンスがマウントされている要素を置き換えます。それゆえ、テンプレート内に常に単一の ルートレベル要素を持つように推奨されます。
このようなテンプレートの代わりに:
<div>root node 1</div> |
こうするようにしてください:
<div> |
Vue インスタンスをフラグメントインスタンスに変えるいくつかの状況があります:
- 複数のトップレベル要素を含むテンプレート
- プレーンなテキストだけを含むテンプレート
- 他のコンポーネント(それ自体がフラグメントインスタンスかもしれない)だけを含むテンプレート
- エレメントディレクティブだけ含むテンプレート、例えば
<partial>
や vue-router の<router-view>
- ルートノードがフロー制御ディレクティブ、例えば
v-if
やv-for
を持つテンプレート
上記全ては、インスタンスに未知数のトップレベル要素を持たせることになり、フラグメントとして DOM コンテンツを管理しなければならなくなります。フラグメントインスタンスはそれでも正常にコンテンツをレンダリングするでしょう。しかしながら、それは root なノードを持っておらず、その $el
は空のテキストノード(またはデバッグモードではコメントノード)で”アンカーノード”を指すようになります。
しかしさらに重要なのは、フロー制御しないディレクティブ、prop ではない属性、そしてコンポーネント要素でのトランジションは、無視されることで、というのもそれらをバインドする root な要素がないためです:
<!-- root 要素がないため動作しません --> |
フラグメントインスタンスに対して有効なユースケースはもちろんありますが、一般的にはコンポーネントテンプレートに単一の root 要素を与えるのが、よい考え方です。それはコンポーネント要素のディレクティブや属性の正しい動作を確保し、わずかなパフォーマンスの向上にもつながります。
インラインテンプレート
特別な属性 inline-template
が子コンポーネントに存在するとき、配信コンテンツとして扱うよりむしろ、コンポーネントはそれをテンプレートとして内部コンテンツを使用します。これは、より柔軟なテンプレートを作成可能にします。
<my-component inline-template> |
しかしながら、inline-template
はテンプレートのスコープを推理するのが難しくなり、コンポーネントのテンプレートコンパイルがキャッシュできなくなります。ベストプラクティスとして、template
オプションを使用して、コンポーネント内部でテンプレートを定義するようにしてください。