Vue.jsチートシート(コンポーネントと構成編)


関連: [Vue.jsチートシート(基礎編)](https://qiita.com/morrr/items/289b6e7ac82e969a7a58)

基礎編に引き続き、公式チュートリアルからの抜粋です。

参照
------------------------------------------------------------
- [Vue.js公式](https://jp.vuejs.org/)
    - [ガイド](https://jp.vuejs.org/v2/guide/)
    - [APIリファレンス](https://jp.vuejs.org/v2/api/)


コンポーネント
------------------------------------------------------------
### 前提
- コンポーネント = VueにおけるUIの単位
    - W3CによるWeb Componentsの概念に影響されている
    - コンポーネントはツリー状に配置される

- コンポーネントは再利用可能なVueインスタンスである
    - HTML側においては、定義した名前のカスタムタグとして利用できる


- コンポーネントは原則的にVueインスタンスをnewする場合と同じオプションを受容する
    - `template` を定義して使い回せる

        ```html
        <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を渡す必要がある(インスタンスごとのデータの独立を保つため)

        ```js
        Vue.component('foo', {
          data: function(){
            return {
              // ...
            }
          },
        })
        ```

- コンポーネントのテンプレート内の要素は **単一のルートを持つ** という制約がある

    ```html
    // 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>` と書いてもいい


#### グローバル/ローカル登録
- グローバル登録は `Vue.component()` で行う

    ```js
    Vue.component({
      // ...
    })
    ```

    - グローバルに登録したコンポーネントは、ルートインスタンス以下で任意に利用できる
    - グローバルなコンポーネントはお互いにお互いを利用できる
    - 不必要なコンポーネントもロードされてしまうため、パフォーマンスの観点から不必要なグローバル登録は望ましくない


- ローカル登録は(ルート)Vueインスタンス内の `components` にコンポーネントを定義する

    ```js
    var vm = new Vue({

      // ...

      components: {
        'foo-component': {
          // ...
        },
        'bar-component': {
          // ...
        },
      }
    })
    ```

    - ローカル登録すると、定義したインスタンス以外でコンポーネントを利用できないようになる
        - 子同士であっても相互に呼び出すことはできない

    - 入れ子にすることもできる

        ```js
        var fooComponent = {
          ...
        }

        var barComponent = {
          components: {
            'foo-component': fooComponent
          }
        }

        var vm = new Vue({
          components: {
            'bar-component': barComponent
          }
        })
        ```

#### モジュールシステム(ES Module)の利用
- ESMを使う場合、コンポーネントごとにファイルを分散することができる

    ```js
    import FooComponent from './FooComponent'
    import BarComponent from './BarComponent'

    export default{
      components: {
        'foo-component': FooComponent
        'bar-component': FooComponent
      }
    }
    ```

- 汎用的なコンポーネントをディレクトリごと自動的にグローバルに登録されるようにしておくと便利

    ```js
    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として静的に渡すことができる

    ```js
    Vue.component('message', {
      props: ['label'],
      template: '<p>{{ label }}</p>',
    })
    ```

    ```html
    <message label="Alpha"></message>
    <message label="Bravo"></message>
    <message label="Charlie"></message>
    ```

- v-bindと組み合わせてルートインスタンスのdataとpropsをバインドできる

    ```html
    <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はオブジェクトを渡すこともできる

    ```html
    <card v-bind:data="item"></card>
    ```

    ```js
    Vue.component('card', {
      props: ['data'],
      template: `
        <div>
          <div>id = {{ data.id }}</div>
          <div>name = {{ data.name }}</div>
        </div>
      `
    })
    ```

- propsのキー名がCamelCaseの場合、HTML側ではkebab-caseに変換する必要がある

    ```js
    Vue.component('some-component', {
      props: ['someProp'],
      template: '<span>{{ someProp }}</span>'
    })
    ```

    ```html
    <some-component some-prop="foo"></some-component>
    ```

- propsは型を指定することができる
    - 指定と異なる値が渡された場合はコンソール上で警告される

        ```js
        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を受け渡す必要がある

        ```html
        <!-- 固定値であってもv-bindする -->
        <some-component v-bind:num="123"></some-component>

        <!-- bindしないと検証されない -->
        <some-component text="123"></some-component>

        <!-- 真偽値に値を渡さないとtrue扱いになる -->
        <some-component flag></some-component>
        ```

    - バリデーションを指定することもできる

        ```js
        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に移し替えるか算出プロパティを使う

        ```js
        Vue.component('some-component', {
          props: ['fooOrg'],
          data: {
            foo: this.fooOrg,
          },
          computed: {
            fooLarge: function(){
              this.fooOrg.toUpperCase()
            },
          },
        })
        ```

#### 属性を使った受け渡し
- コンポーネントに設定した属性は、そのテンプレートにマージされる

    ```js
    Vue.component('some-component', {
      template: '<span class="foo-class">component</span>
    })
    ```

    ```html
    <!-- <span class="foo-class bar-class">component</span> としてレンダリングされる -->
    <some-component class="bar-class"></some-component>
    ```

### emit: イベントの伝播

- `$emit()` によって子コンポーネントのイベントを親側に伝達できる

    ```js
    // 子コンポーネントのイベントを送る
    Vue.component('foo', {
      template: `
        <button v-on:click="$emit('callback')">click!</button>
      `
    })
    ```

    ```html
    <!-- 親コンポーネント側でイベントを捕捉 -->
    <foo v-on:callback="parentEvent()"></foo>
    ```

- パラメータ付きのイベント伝達もできる

    ```js
    Vue.component('foo', {
      template: `
        <button v-on:click="$emit('click-event', 'foo clicked')">click!</button>
      `
    })
    ```

    ```html
    <!-- $eventは子側で渡した「
    <foo v-on:click-event="alert($event)"></foo>
    ```

- コンポーネントに対してv-modelでデータバインドさせることができる
    - `value` propsを使うこと、変更を `input` イベントでemitすることが条件となる

    ```js
    Vue.component('custom-input', {
      props: ['value'],
      template: `
        <input
          v-bind:value="value"
          v-on:input="$emit('input', $event.target.value)"
        >
      `
    })
    ```

    ```html
    <custom-input v-model="customInputText"></custom-input>
    ```

- `.sync` を使うとイベントによる双方向バインディングを簡略化できる

    ```html
    <!-- 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の値を簡単に取り込める

    ```js
    Vue.component('message', {
      template: '<p><slot></slot></p>
    })
    ```

    ```html
    <message>slotタグがここに記述されたテキストに置き換わる</message>
    <message><span>slotはHTMLタグを含めることができる</span></message>
    ```

- slotにはデフォルト値を設定できる

    ```js
    Vue.component('message', {
      template: '<slot>default message</slot>'
    })
    ```

- slotに名前を付けて複数利用できる

    ```js
    Vue.component('message', {
      template: `
        <div>
          <slot name="id"></slot>
          <slot name="name"></slot>

          <slot></slot>
        </div>
      `
    })
    ```

    ```html
    <message>
      <template v-slot:id>123</template>
      <template v-slot:name>Alpha</template>

      <!-- 名前なしのslotはdefaultとなる -->
      <!-- v-slotは#と書ける -->
      <template #default>...</template>
    </message>
    ```


- slotコンテンツは、Slotを提供する側のコンポーネントのスコープには入れない

    ```js
    <!-- messageコンポーネントのスコープであるnameにアクセスすることはできない -->
    <message name="">Message from: {{ name }}</message>
    ```

    - アクセスするためにはslotに対してデータバインドする

        ```html
        <!-- template側 -->
        <span v-bind:user="user">{{ user.defaultName }}</span>

        <!-- 利用する側 -->
        <message>
          <template v-slot:default="user">
            {{ user.name }}
          </template>
        </message>
        ```

### is: 動的コンポーネント
- コンポーネントそのものを動的に切り替えることができる:
    - bindしたis属性の値にコンポーネント名(か、コンポーネントオブジェクトそのもの)を設定することで表示を切り替えられる

    ```html
    <component v-bind:is="componentType"></component>
    ```

- `is` を指定することで、DOMのパースアラートが出てしまうような場面でもこれを抑制できる

    ```html
    <table>
      <!-- some-componentが描画されるがDOMパース上はtrとして扱われる(のでアラートが出ない) -->
      <tr is="some-component"></tr>
    </table>
    ```

- `<keep-alive>` を使うと、isで切り替えてもコンポーネントの状態が保持されるようになる

    ```html
    <keep-alive>
      <component v-bind:is="componentType"></component>"
    </keep-alive>
    ```

### 非同期描画
- コンポーネントのオプションとしてファクトリ関数を渡すことで、その描画タイミングを制御できる

    ```js
    Vue.component('async-component', function(resolve, reject){
      
      // ...

      // 必要なタイミングでresolveを呼んでオプションを返す
      resolve({
        props: [...],
        template: '...',
      })
    })
    ```

- ES6の場合はimportを直接返すことで分割ロードできる

    ```js
    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` によってルートインスタンスにアクセスできる

    ```js
    // 原則的に使うべきでない
    this.$root.someGlobalValue
    ```

- `$parent` によって親コンポーネントにアクセスできる

    ```js
    // これも同様に使うべきでない
    this.$parent.someParams
    ```

- 子のコンポーネントに `ref` 属性を指定することで、親側からアクセスできる

    ```html
    <child-component ref="childAlpha"></child-component>
    ```

    ```js
    // `$refs` へのアクセスはリアクティブでない(バインドされてない)ため注意
    $this.$refs.childAlpha.someParams
    ```

- provide/injectオプションにより親子コンポーネント間でデータを受け渡せる

    ```js
    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` により動的にイベントを設定できる

    ```js
    // beforeDestroyイベント発火時に一度だけ呼ばれる
    this.$once('hook:beforeDestroy', function(){
      // ...
    })
    ```

#### 再帰的な呼び出し
- コンポーネントは自身をテンプレートに含めることができる

    - 無条件に自己参照させると無限ループになるので注意

        ```js
        // この例だと無限ループする
        Vue.component('some-component', {
          template: '<some-component></some-component>',
        })
        ```

#### テンプレートを別途定義する
- `inline-template` 属性を指定すると、そのコンポーネントのinner-htmlがテンプレートとして利用される

    ```html
    <some-component inline-template>
      <div>
        <p>div以下はslotではなくテンプレートとして扱われる</p>
        <p>templateなのでコンポーネントのスコープを利用できる: {{ someProp }}</p>
      </div>
    </some-component>
    ```

- `text/x-template` のtypeを指定したscriptタグ内にテンプレートを定義して呼び出すことができる

    ```html
    <script type="text/x-template" id="some-template">
      <span>some-template</span>
    </script>
    ```

    ```js
    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を使える


    ```vue
    <template>
      <span>template</span>
    </template>

    <script>
    module.exports = {
      data: {
        ...
      },
    }
    </scripts>

    <style scoped>
    span {
      // ...
    },
    </style>
    ```


### Mix-in
- Mix-inを使うとコンポーネント間でオプションを共有できる
    - Mixinとコンポーネント別のオプションに重複があった場合は自動的に「よしなに」マージされる
    - オリジナルのオプションの場合はオプション丸ごと上書きされる

    ```js
    // 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)
    ```

### ディレクティブの定義
- ディレクティブを独自に定義することができる

    ```js
    // グローバルな登録
    Vue.directive('foo', {
      inserted: function(el){
        // ...
      }
    })

    // ローカルに登録
    new Vue({
      directives: {
        foo: {
          inserted: function(el){
            // ...
          }
        },
      }
    })
    ```

    ```html
    <!-- 定義したディレクティブはv-*の形で利用できる -->
    <span v-foo></span>
    ```

- ディテクティブは様々なイベントを検知できる

    ```js
    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を構築して返すことで、それを描画させるイメージ

    ```js
    Vue.component('some-component', {
      // 以下のrenderとtemplateが等価

      render: function(createElement){
        return createElement('p', this.$slots.default)
      },

      template: '<p><slot></slot></p>'
    })
    ```

- ReactのようにJSXを使うこともできる
    - 利用するためには [Babelによるトランスパイラの設定](https://github.com/vuejs/jsx) が必要になる

    ```jsx
    Vue.component('some-component', {
      render: function(h){
        return (
          <span>JSX</span>
        )
      })
    })
    ```

### プラグイン
- Vue.jsに対するプラグインを利用して機能を拡張できる


- プラグインは `Vue.use()` により利用開始できる

    ```js
    Vue.use(SomePlugin, {someOption: someValue})
    ```

    - `vue-router` などVue公式のプラグインの場合は、Vueオブジェクトがロード済であれば自動的にuse()を呼ぶ


- プラグインを提供する場合、 `install()` を提供する必要がある

    ```js
    SomePlugin.install = function(Vue, options){
      // mixinしたり、directiveを定義したり、グローバルメソッドを定義したり...
      Vue.mixin({
        // ...
      })
    }
    ```

### filters: 文字列に対するフィルタ
- `filters` オプションによって文字列に対するフィルタを定義できる

    ```js
    var vm = new Vue({
      filters: {
        foo: function(value){
          return value + ' foo!'
        }
      }
    })
    ```

- 定義したフィルタはpipe `|` によって利用できる

    ```html
    <!-- Mustacheやv-bindで利用できる -->
    <span>{{ name | foo }}</span>

    ```