基礎編に引き続き、公式チュートリアルからの抜粋です。
参照
コンポーネント
前提
-
コンポーネント = VueにおけるUIの単位
- W3CによるWeb Componentsの概念に影響されている
- コンポーネントはツリー状に配置される
-
コンポーネントは再利用可能なVueインスタンスである
- HTML側においては、定義した名前のカスタムタグとして利用できる
-
コンポーネントは原則的にVueインスタンスをnewする場合と同じオプションを受容する
-
template
を定義して使い回せる<div id="app"> <message></message> <message></message> </div> <script> Vue.component('message', { template: '<p>Message</p>' }) var vm = new Vue({ el: '#app', }) </script>
-
data
はfunctionを渡す必要がある(インスタンスごとのデータの独立を保つため)Vue.component('foo', { data: function(){ return { // ... } }, })
-
-
コンポーネントのテンプレート内の要素は 単一のルートを持つ という制約がある
// 2つのrootが存在してしまうため不可 Vue.component('foo', { template: ` <p>foo</p> <p>bar</p> ` }) // 全体をラップしてあげれば可 Vue.component('foo', { template: ` <div> <p>foo</p> <p>bar</p> </div> ` })
コンポーネントの登録
-
コンポーネントは登録することでHTML上で利用できるようになる
- 登録にはグローバル/ローカルそれぞれの方法がある
-
コンポーネント名は原則的に
lower-kebab-case
が推奨される- この場合はDOMに
<lower-kebab-case>
と書くことになる -
UpperCamelCase
を使うこともできる- この場合は
<UpperCamelCase>
以外にも<upper-camel-case>
と書いてもいい
- この場合は
- この場合はDOMに
グローバル/ローカル登録
-
グローバル登録は
Vue.component()
で行うVue.component({ // ... })
- グローバルに登録したコンポーネントは、ルートインスタンス以下で任意に利用できる
- グローバルなコンポーネントはお互いにお互いを利用できる
- 不必要なコンポーネントもロードされてしまうため、パフォーマンスの観点から不必要なグローバル登録は望ましくない
-
ローカル登録は(ルート)Vueインスタンス内の
components
にコンポーネントを定義するvar vm = new Vue({ // ... components: { 'foo-component': { // ... }, 'bar-component': { // ... }, } })
-
ローカル登録すると、定義したインスタンス以外でコンポーネントを利用できないようになる
- 子同士であっても相互に呼び出すことはできない
-
入れ子にすることもできる
var fooComponent = { ... } var barComponent = { components: { 'foo-component': fooComponent } } var vm = new Vue({ components: { 'bar-component': barComponent } })
-
モジュールシステム(ES Module)の利用
-
ESMを使う場合、コンポーネントごとにファイルを分散することができる
import FooComponent from './FooComponent' import BarComponent from './BarComponent' export default{ components: { 'foo-component': FooComponent 'bar-component': FooComponent } }
-
汎用的なコンポーネントをディレクトリごと自動的にグローバルに登録されるようにしておくと便利
import Vue from 'vue' import upperFirst from 'lodash/upperFirst' import camelCase from 'lodash/camelCase' const requireComponent = require.context( './components', false, // 再帰的に検索しない /Base[A-Z]\w+\.(vue|js)$/ // マッチしたファイルを読み込む ) requireComponent.keys().forEach(fileName => { // lodashでコンポーネント名を加工する const componentName = upperFirst( camelCase( fileName.split('/').pop().replace(/\.\w+$/, '') ) ) // グローバル登録する const componentConfig = requireComponent(fileName) Vue.component( componentName, componentConfig.default || componentConfig ) })
データの受け渡し
props
-
props
によってコンポーネントが外部から受け入れるパラメータを定義できる -
propsはhtml attribnとして静的に渡すことができる
Vue.component('message', { props: ['label'], template: '<p>{{ label }}</p>', })
<message label="Alpha"></message> <message label="Bravo"></message> <message label="Charlie"></message>
-
v-bindと組み合わせてルートインスタンスのdataとpropsをバインドできる
<div id="app"> <message v-for="item in list" v-bind:data="item"></message> </div> <script> Vue.component('message', { props: ['data'], template: '<p>{{ data.label }}</p>' }) var app = new Vue({ el: '#app', data: { list: [ { label: 'Alpha' }, { label: 'Bravo' }, { label: 'Charlie' }, ] } }) </script>
-
propsはオブジェクトを渡すこともできる
<card v-bind:data="item"></card>
Vue.component('card', { props: ['data'], template: ` <div> <div>id = {{ data.id }}</div> <div>name = {{ data.name }}</div> </div> ` })
-
propsのキー名がCamelCaseの場合、HTML側ではkebab-caseに変換する必要がある
Vue.component('some-component', { props: ['someProp'], template: '<span>{{ someProp }}</span>' })
<some-component some-prop="foo"></some-component>
-
propsは型を指定することができる
-
指定と異なる値が渡された場合はコンソール上で警告される
Vue.component('some-component', { props: { num: Number, text: String, flag: Boolean, list: Array, hash: Object, func: Function, promise: Promise, multi: [Number, String], // 複数指定もできる } })
-
型を指定する場合は
v-bind
でpropsを受け渡す必要がある<!-- 固定値であってもv-bindする --> <some-component v-bind:num="123"></some-component> <!-- bindしないと検証されない --> <some-component text="123"></some-component> <!-- 真偽値に値を渡さないとtrue扱いになる --> <some-component flag></some-component>
-
バリデーションを指定することもできる
Vue.component('some-component', { props: { flag: { type: Boolean, required: true, // 必須とする }, num: { type: Number, default: 123, // 初期値を指定する }, text: { type: String, validator: function(){ // バリデータを独自に定義する return (string == 'foo') }, }, } })
-
-
コンポーネントに対するデータフローは 単方向 (親から子)が維持される
-
親側でpropsにバインドしたデータを更新すると子に反映されるが、その逆はない
-
子側でpropsを変更したい場合は、dataに移し替えるか算出プロパティを使う
Vue.component('some-component', { props: ['fooOrg'], data: { foo: this.fooOrg, }, computed: { fooLarge: function(){ this.fooOrg.toUpperCase() }, }, })
-
属性を使った受け渡し
-
コンポーネントに設定した属性は、そのテンプレートにマージされる
Vue.component('some-component', { template: '<span class="foo-class">component</span> })
<!-- <span class="foo-class bar-class">component</span> としてレンダリングされる --> <some-component class="bar-class"></some-component>
emit: イベントの伝播
-
$emit()
によって子コンポーネントのイベントを親側に伝達できる// 子コンポーネントのイベントを送る Vue.component('foo', { template: ` <button v-on:click="$emit('callback')">click!</button> ` })
<!-- 親コンポーネント側でイベントを捕捉 --> <foo v-on:callback="parentEvent()"></foo>
-
パラメータ付きのイベント伝達もできる
Vue.component('foo', { template: ` <button v-on:click="$emit('click-event', 'foo clicked')">click!</button> ` })
<!-- $eventは子側で渡した「 <foo v-on:click-event="alert($event)"></foo>
-
コンポーネントに対してv-modelでデータバインドさせることができる
-
value
propsを使うこと、変更をinput
イベントでemitすることが条件となる
Vue.component('custom-input', { props: ['value'], template: ` <input v-bind:value="value" v-on:input="$emit('input', $event.target.value)" > ` })
<custom-input v-model="customInputText"></custom-input>
-
-
.sync
を使うとイベントによる双方向バインディングを簡略化できる<!-- v-bindで子にデータを渡し、子側の変更をv-onで受け取っている --> <some-component v-bind:foo="foo" v-on:update:foo="foo = $event" ></some-component> <!-- 上と同じになる --> <some-component v-bind:foo.sync="foo"></some-component>
slot: inner-htmlによる受け渡し
-
<slot>
を使うことで、そのコンポーネントのinner htmlの値を簡単に取り込めるVue.component('message', { template: '<p><slot></slot></p> })
<message>slotタグがここに記述されたテキストに置き換わる</message> <message><span>slotはHTMLタグを含めることができる</span></message>
-
slotにはデフォルト値を設定できる
Vue.component('message', { template: '<slot>default message</slot>' })
-
slotに名前を付けて複数利用できる
Vue.component('message', { template: ` <div> <slot name="id"></slot> <slot name="name"></slot> <slot></slot> </div> ` })
<message> <template v-slot:id>123</template> <template v-slot:name>Alpha</template> <!-- 名前なしのslotはdefaultとなる --> <!-- v-slotは#と書ける --> <template #default>...</template> </message>
-
slotコンテンツは、Slotを提供する側のコンポーネントのスコープには入れない
<!-- messageコンポーネントのスコープであるnameにアクセスすることはできない --> <message name="">Message from: {{ name }}</message>
-
アクセスするためにはslotに対してデータバインドする
<!-- template側 --> <span v-bind:user="user">{{ user.defaultName }}</span> <!-- 利用する側 --> <message> <template v-slot:default="user"> {{ user.name }} </template> </message>
-
is: 動的コンポーネント
-
コンポーネントそのものを動的に切り替えることができる:
- bindしたis属性の値にコンポーネント名(か、コンポーネントオブジェクトそのもの)を設定することで表示を切り替えられる
<component v-bind:is="componentType"></component>
-
is
を指定することで、DOMのパースアラートが出てしまうような場面でもこれを抑制できる<table> <!-- some-componentが描画されるがDOMパース上はtrとして扱われる(のでアラートが出ない) --> <tr is="some-component"></tr> </table>
-
<keep-alive>
を使うと、isで切り替えてもコンポーネントの状態が保持されるようになる<keep-alive> <component v-bind:is="componentType"></component>" </keep-alive>
非同期描画
-
コンポーネントのオプションとしてファクトリ関数を渡すことで、その描画タイミングを制御できる
Vue.component('async-component', function(resolve, reject){ // ... // 必要なタイミングでresolveを呼んでオプションを返す resolve({ props: [...], template: '...', }) })
-
ES6の場合はimportを直接返すことで分割ロードできる
Vue.component( 'async-component', ()=> import('./path/to/component') ) // 細かい条件を指定できる Vue.component( 'async-component', ()=> { component: import('./path/to/component') loading: LoadingComponent, // ロード中に表示するコンポーネント error: ErrorComponent, // エラー時に表示する delay: 200, // loadingの表示までの時間 timeout: 3000 // errorの表示までの時間 } )
エッジケース
スコープを無視したアクセス
-
$root
によってルートインスタンスにアクセスできる// 原則的に使うべきでない this.$root.someGlobalValue
-
$parent
によって親コンポーネントにアクセスできる// これも同様に使うべきでない this.$parent.someParams
-
子のコンポーネントに
ref
属性を指定することで、親側からアクセスできる<child-component ref="childAlpha"></child-component>
// `$refs` へのアクセスはリアクティブでない(バインドされてない)ため注意 $this.$refs.childAlpha.someParams
-
provide/injectオプションにより親子コンポーネント間でデータを受け渡せる
Vue.component('child-component', { inject: ['foo'], template: '<span>foo</span>', }) Vue.component('parent-component', { template: '<child-component></child-component>' provide: function(){ return { foo: this.foo } } })
動的なイベントリスナ
-
$on
,$once
,$off
により動的にイベントを設定できる// beforeDestroyイベント発火時に一度だけ呼ばれる this.$once('hook:beforeDestroy', function(){ // ... })
再帰的な呼び出し
-
コンポーネントは自身をテンプレートに含めることができる
-
無条件に自己参照させると無限ループになるので注意
// この例だと無限ループする Vue.component('some-component', { template: '<some-component></some-component>', })
-
テンプレートを別途定義する
-
inline-template
属性を指定すると、そのコンポーネントのinner-htmlがテンプレートとして利用される<some-component inline-template> <div> <p>div以下はslotではなくテンプレートとして扱われる</p> <p>templateなのでコンポーネントのスコープを利用できる: {{ someProp }}</p> </div> </some-component>
-
text/x-template
のtypeを指定したscriptタグ内にテンプレートを定義して呼び出すことができる<script type="text/x-template" id="some-template"> <span>some-template</span> </script>
Vue.component('some-component', { template: '#some-template' })
更新のコントロール
-
$forceUpdate
によってコンポーネントを強制的に再描画させられる- コンポーネントがリアクティブでないように作ってしまっている、ということなので多様すべきだない
-
v-once
を指定することで大きな静的なHTMLの再描画を抑止できる
モジュールおよびコンポーネントの構成
.vue: 単一ファイルコンポーネント
-
単純にjsファイルでコンポーネントの定義することによって様々な不利益が生まれる
- グローバルなスコープが汚染されたり
- templateに単に文字列としてのHTMLを書くことになったり
- Babel/ES6やTypeScriptが使えなかったり
-
Webpack(あるいはBrowserifyなど)により、
.vue
ファイルを使うことができる- vueファイルはHTML、CSS、JSをコンポーネント単位でひとつのファイルにまとめて記述できる
- テンプレートやスタイルシートにHTML/CSS以外を選択することもできる
- コードについてもES6やTypeScriptを使える
<template> <span>template</span> </template> <script> module.exports = { data: { ... }, } </scripts> <style scoped> span { // ... }, </style>
Mix-in
-
Mix-inを使うとコンポーネント間でオプションを共有できる
- Mixinとコンポーネント別のオプションに重複があった場合は自動的に「よしなに」マージされる
- オリジナルのオプションの場合はオプション丸ごと上書きされる
// Mix-inを定義する var mixin = { methods: { foo: function(){ return 'foo'; } } } // Mix-inをもとにコンポーネントを作る var SomeComponent = Vue.extend({ mixins: [mixin], }) var someComponent = new SomeComponent() // ルートインスタンスにMix-inする var vm = new Vue({ mixins: [mixin], data: { // ... }, }) // 全てのコンポーネントにMix-inする Vue.mixin(mixin)
ディレクティブの定義
-
ディレクティブを独自に定義することができる
// グローバルな登録 Vue.directive('foo', { inserted: function(el){ // ... } }) // ローカルに登録 new Vue({ directives: { foo: { inserted: function(el){ // ... } }, } })
<!-- 定義したディレクティブはv-*の形で利用できる --> <span v-foo></span>
-
ディテクティブは様々なイベントを検知できる
Vue.directive('foo', { // el以外の引数もある bind: function(el){ /* ディレクティブが設定された場合 */ }, inserted: function(el){ /* ディレクティブを含む要素がDOMに挿入された場合 */ }, update: function(el){ /* ディレクティブを含む要素を含むコンポーネントの更新時 */ }, componentUpdated: function(el){ /* ディレクティブを含む要素を含むコンポーネントと、その要素が含むコンポーネントの更新時 */ }, unbind: function(el){ /* ディレクティブが設定解除された場合 */ }, })
仮想ノードと描画関数, JSX
-
VNode = 実際のDOM要素に対して加える変更を表現する記述のこと
-
仮想ノード = VNodeによって表現されたツリー状のコンポーネントの集まりのこと
-
render
オプションによって動的にテンプレートを生成できる- renderは部分的なVNodeを構築して返すことで、それを描画させるイメージ
Vue.component('some-component', { // 以下のrenderとtemplateが等価 render: function(createElement){ return createElement('p', this.$slots.default) }, template: '<p><slot></slot></p>' })
-
ReactのようにJSXを使うこともできる
- 利用するためには Babelによるトランスパイラの設定 が必要になる
Vue.component('some-component', { render: function(h){ return ( <span>JSX</span> ) }) })
プラグイン
-
Vue.jsに対するプラグインを利用して機能を拡張できる
-
プラグインは
Vue.use()
により利用開始できるVue.use(SomePlugin, {someOption: someValue})
-
vue-router
などVue公式のプラグインの場合は、Vueオブジェクトがロード済であれば自動的にuse()を呼ぶ
-
-
プラグインを提供する場合、
install()
を提供する必要があるSomePlugin.install = function(Vue, options){ // mixinしたり、directiveを定義したり、グローバルメソッドを定義したり... Vue.mixin({ // ... }) }
filters: 文字列に対するフィルタ
-
filters
オプションによって文字列に対するフィルタを定義できるvar vm = new Vue({ filters: { foo: function(value){ return value + ' foo!' } } })
-
定義したフィルタはpipe
|
によって利用できる<!-- Mustacheやv-bindで利用できる --> <span>{{ name | foo }}</span>