LoginSignup
56
65

More than 3 years have passed since last update.

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

Posted at

関連: Vue.jsチートシート(基礎編)

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

参照

コンポーネント

前提

  • コンポーネント = 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> と書いてもいい

グローバル/ローカル登録

  • グローバル登録は 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を使うこともできる

    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>
    
    
56
65
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
56
65