Что такое компоненты?

Компоненты - это разделение кода, его инкапсулирование, на независимые части позволяющие повторно использовать код. Все компоненты - экзепляры Vue и принимают один и тот же объект параметров.

Глобальная регистрация компонентов

Экземпляры Vue создаются следующим образом:

new Vue({
  el: '#some-element',
  // опции
})

Регистрация глобального компонента Vue производится следующим образом:

Vue.component(tagName, options);
Vue.component('my-component', {
  // опции
})

В именах компонентов могут использоваться только символы нижнего регистра и дефисы.

Зарегистрированный компонент используется в шаблоне экземпляра в виде пользовательского элемента:

<my-component></my-component>

Компонент должен быть зарегистрирован до создания корневого экземпляра Vue:

<div id="example">
  <my-component></my-component>
</div>
// регистрация
Vue.component('my-component', {
  template: '<div>Пользовательский компонент!</div>'
})

// создание корневого экземпляра
new Vue({
  el: '#example'
})

Результат работы кода следующий:

<div id="example">
  <div>Пользовательский компонент!</div>
</div>

Локальные компоненты

Компоненты также можно использовать локально - сделать его доступным только в области видимости другого экземпляра или компонента. Регистрация локального компонента производится опцией components:

var Child = {
  template: '<div>Пользовательский компонент!</div>'
}

new Vue({
  // ...
  components: {
    // <my-component> будет доступен только в шаблоне родителя
    'my-component': Child
  }
})

Особенности парсинга DOM-шаблона

Когда в качестве шаблона используется DOM - в опции el указана точка монтирования, содержащая контент, то появляются ограничения накладываемые самим языком HTML.

При этом содержимое шаблона поступает в Vue, лишь когда браузер распарсит и нормализует HTML-страницу. Определенные элементы могут не соответствовать нормам HTML. Допустим, ограничения, какие элементы могут находиться внутри элементов <ul>, <ol>, <table> и <select>. Для некоторых элементов, например для <option>, подобным образом ограничен список допустимых родительских элементов. Например:

<table>
  <my-row>...</my-row>
</table>

Здесь компонент <my-row> будет воспринят браузером как неправильный, что приведет к ошибке рендинга. Чтобы исправить это, можно использовать атрибут is:

<table>
  <tr is="my-row"></tr>
</table>

Подобные ограничения не действуют, если шаблоны получены из:

  • <script type="text/x-template">
  • inline-строки JavaScript
  • .vue-компоненты

Опция data должна быть функцией

Опции, которые можно передавать в конструктор Vue, можно использовать и в компонентах. Но здесь есть важное исключение.

Опция data должна быть функцией в компоненте.

Если попробовать выполнить код:

Vue.component('my-component', {
  template: '<span>{{ message }}</span>',
  data: {
    message: 'привет!'
  }
})

То Vue прекратит работу и выведет в консоли уведомление, что опция data должна быть в компоненте функцией. Рассмотрим такой пример:

<div id="example-2">
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
</div>
var data = { counter: 0 }

Vue.component('simple-counter', {
  template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  // технически data является функцией, так что Vue
  // не будет жаловаться, но при каждом вызове эта функция
  // возвращает ссылку на один и тот же внешний объект
  data: function () {
    return data
  }
})

new Vue({
  el: '#example-2'
})

Ужас! При увеличении одного счетчика остальные тоже увеличатся, так как все используются один объект data. Исправить это можно так, чтобы функция при каждом вызове возвращала вновь созданный объект data.

data: function () {
  return {
    counter: 0
  }
}

Теперь каждый счетчик подсчитывается независимо от другого.

Композиция компонентов

Компоненты взаимодействуют между друг другом, выстраивая иерархические отношения, в которых каждый компонент-родитель ссылается на компонент-потомок в собственном шаблоне. Это удобно для коммуникации компонентов между собой, для передачи данных из родителя к потомку и уведомления родителя о событиях происходящих с потомком. Заданный интерфейс взаимодействия между компонентами сводится к минимуму и такой подход обеспечивает написание и анализ кода каждого компонента в условиях относительной изоляции. Все это делает поддержку простой и облегчает повторное использование компонентов.

Иерерхические отношения Vue выстраиваются в следующем виде: входные парметры - вниз, события - вверх (props down, events up). Родительский компонент передает данные компоненту потомку через входные параметры, компонент потомок присылает уведомление родителю с помощью событий.

Передача данных через входные параметры

У экземпляра компонента есть своя изолированная область видимости. Обращаться напрямую к данным родительского компонента из шаблона компонента-потомка - нельзя. Данные передаются вниз по иерерхии с помощью входных параметров.

Входной параметр - пользовательский атрибут для передачи данных из родительского компонента.

Входные параметры нужно явно определить в потомке через опцию props:

Vue.component('child', {
  // определяем входной параметр
  props: ['message'],
  // как и другие данные, входной параметр можно использовать
  // внутри шаблонов (а также и в методах, обращаясь через this.message)
  template: '<span>{{ message }}</span>'
})

Передача в компонент строки:

<child message="привет!"></child>

camelCase против kebab-case

Атрибуты HTML регистронезависимые, поэтому при использовании в DOM в качестве шаблона вместо camelCase имен входных параметров приходится применять kebab-case.

Vue.component('child', {
  // camelCase в JavaScript
  props: ['myMessage'],
  template: '<span>{{ myMessage }}</span>'
})
<!-- kebab-case в HTML -->
<child my-message="привет!"></child>

Динамические входные параметры

Директива v-bind позволяет динамически связывать входные параметры с данными родительского компонента, аналогично связыванию атрибутов с выражениями. Обновление данных в родителе будут переданы в компонент-потомок:

<div>
  <input v-model="parentMsg">
  <br>
  <child v-bind:my-message="parentMsg"></child>
</div>

Также можно использовать сокращенную запись:

<child :my-message="parentMsg"></child>

Для передачи всех свойств объекта в качестве входных параметров, можно использовать v-bind без аргументов (v-bind вместо v-bind:prop-name), например:

todo: {
  text: 'Learn Vue',
  isComplete: false
}

Запись:

<todo-item v-bind="todo"></todo-item>

Будет аналогична записи:

<todo-item
  v-bind:text="todo.text"
  v-bind:is-complete="todo.isComplete"
></todo-item>

Различие между литералами и динамическими параметрами

Когда число передается компоненту напрямую в виде константы:

<!-- при такой записи в компонент будет передана строка "1" -->
<comp some-prop="1"></comp>

Так как в параметре передан литерал, компонент получит строку "1", а не число. Чтобы передать число используйте директиву v-bind, значение которой вычисляется как выражение JavaScript:

<!-- этот синтаксис позволит передать в компонент число -->
<comp v-bind:some-prop="1"></comp>

Однонаправленный поток данных

Входные параметры однонаправленно посылают данные от родительского компонента к потомкам. Изменение копмонента-родителя передается потомку, но не наоборот. Если бы потомки могли менять состояние родителя, изменять структуру потомков данных было бы трудно. Однонаправленная отправка данных исключает подобное.

Также, при любом изменении родительского компонента каждый входной параметр потомка обновляется до актуального значения. Поэтому не нужно изменять значения входных параметров внутри компонента расчитывая на их сохранность. 

Новички пытаются изменить значение входного параметра в двух случаях:

  1. Если параметр нужен только для передачи потомку начального значения, после чего переменная планируется использоваться как локальная.
  2. Если значению, которое передаётся как параметр, требуется дальнейшая обработка.

Правильное решение следующее:

1. Объявить локальную переменную, принимающу. значение входного параметра при инициализации:

props: ['initialCounter'],
data: function () {
  return { counter: this.initialCounter }
}

2. Определить вычисляемое свойство, основанное на значении входного параметра:

props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

Объекты и массивы в JavaScript передаются по ссылке.  Если входным параметром является объект или массив, его изменение внутри потомка повлияет на состояние родительского компонента.

Валидация входных параметров

В компонентах можно указывать не только список ожидаемых параметров, но и указать определенные требования к этим параметрам. Если полученные параметры не удовлетворяют требованиям, Vue выведет уведомление. Это полезно при создании компонентов для внешнего использования.
Вместо списка параметров в массиве строк, можно использовать объект с правилами валидации:

Vue.component('example', {
  props: {
    // простая проверка типа (`null` означает допустимость любого типа)
    propA: Number,
    // несколько допустимых типов
    propB: [String, Number],
    // обязательное значение строкового типа
    propC: {
      type: String,
      required: true
    },
    // число со значением по умолчанию
    propD: {
      type: Number,
      default: 100
    },
    // значения по умолчанию для объектов и массивов
    // должны задаваться через функцию
    propE: {
      type: Object,
      default: function () {
        return { message: 'привет!' }
      }
    },
    // пользовательская функция для валидации
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})

В type параметре используются нативные конструкторы:

  • String
  • Number
  • Boolean
  • Function
  • Object
  • Array
  • Symbol

Также, type может быть пользовательской функцией-конструктором. Проверка соответствия выполняется с помощью instanceof.

Входные параметры валидируются до создания экземпляра компонента, поэтому в функциях default или validator свойства экземпляра, такие как data, computed, или methods, будут недоступны.

Передача обычных атрибутов

Обычные атрибуты — это атрибуты, передаваемые в компонент, не имеющие входного параметра в компоненте.

Компоненты могут принимать произвольные атрибуты, добавляемые в корневой элемент компонента, ведь явно определенные свойства предпочтительны для передачи данных дочернему компоненту и не всегда можно предвидеть все способы применения компонентов.

Допустим, при использовании стороннего компонента bs-date-input с плагином Bootstrap требуется указать атрибут data-3d-date-picker на элементе input. Можно добавить этот атрибут к нашему экземпляру компонента:

<bs-date-input data-3d-date-picker="true"></bs-date-input>

Атрибут data-3d-date-picker="true" будет автоматически добавлен в корневой элемент bs-date-input.

Замена и объединение существующих атрибутов

Представьте, что это шаблон для bs-date-input:

<input type="date" class="form-control">

Чтобы добавить тему для плагина выбора даты, может понадобиться добавить определённый класс:

<bs-date-input
  data-3d-date-picker="true"
  class="date-picker-theme-dark"
></bs-date-input>

В этом случае определены два разных значения для class:

  • form-control -задаётся компонентом в его шаблоне
  • date-picker-theme-dark -передаётся компоненту его родителем

Значение предоставляемое компоненту для большинства атрибтов будет заменять значение, заданное компонентом. Например, передача type="large" заменит type="date" и вероятно сломает всё! Но, к счастью, работа с атрибутами class и style немного умнее, поэтому оба значения будут объединены в итоговое значение: form-control date-picker-theme-dark.

Пользовательские события

Компонент-родитель может передавать данные потомкам через входные параметры. Теперь поговорим о системе пользовательских событий для организации связи в обратном направлении.

Использование v-on с пользовательскими событиями

Каждый экземпляр Vue поддерживает интерфейс событий, позволяющий:

  • Отслеживать события через $on(eventName)
  • Порождать события через $emit(eventName)

Система событий Vue отделена от EventTarget API браузера. $on и $emit — это не псевдонимы addEventListener и dispatchEvent.

Родительский компонент может зарегистрировать подписчика событий, с помощью директивы v-on  в шаблоне при создании компонента-потомка.

Нельзя использовать $on для прослушивания событий, генерируемых в потомках. Нужно использовать v-on в шаблоне, как в приведённом ниже примере.

Например:

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
  template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    incrementCounter: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})

new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

Потомок остаётся полностью независимым от всего происходящего снаружи и лишь уведомляет родительский компонент о происходящем с ним.

Подписка на нативные события в компонентах

Для подписки на нативные события браузера в корневом элементе компонента можно применять v-on с модификатором .native:

<my-component v-on:click.native="doTheThing"></my-component>

Модификатор .sync

Для двухсторонней привязки входных данных используется модификатор .sync, который позволяет отобразить в родительском компоненте изменения данных входных параметров дочернего компонента.

Следующий код:

<comp :foo.sync="bar"></comp>

преобразуется в:

<comp :foo="bar" @update:foo="val => bar = val"></comp>

Дочерний компонент должен явно генерировать событие, чтобы обновлять значение foo, вместо изменения параметра входных данных:

this.$emit('update:foo', newValue)

Поля ввода форм с пользовательскими событиями

С помощью пользовательских событий можно создавать пользовательские поля ввода с директивой v-model.
Следующий код:

<input v-model="something">

Это лишь синтаксический сахар для:

<input
  v-bind:value="something"
  v-on:input="something = $event.target.value">

С использованием компонента код упрощается:

<custom-input
  :value="something"
  @input="value => { something = value }">
</custom-input>

Чтобы иметь возможность работать с v-model, компонент может:

  • принимать входной параметр value
  • порождать событие input с новым значением

В качестве примера простое поле ввода денежной суммы с точкой в виде десятичного разделителя:

<currency-input v-model="price"></currency-input>
Vue.component('currency-input', {
  template: '\
    <span>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)"\
      >\
    </span>\
  ',
  props: ['value'],
  methods: {
    // Вместо того, чтобы обновлять значение напрямую,
    // в этом методе мы выполняем нормализацию и форматирование
    // введённого значения, а затем порождаем событие,
    // уведомляющее родительский компонент об изменениях
    updateValue: function (value) {
      var formattedValue = value
        // Удалить пробелы с обеих сторон
        .trim()
        // Сократить до 2 знаков после запятой
        .slice(
          0,
          value.indexOf('.') === -1
            ? value.length
            : value.indexOf('.') + 3
        )
      // Если значение не нормализовано — нормализуем вручную
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // Порождаем событие с обновлённым значением поля ввода
      this.$emit('input', Number(formattedValue))
    }
  }
})

Более расширенный код этого примера:

<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://cdn.rawgit.com/chrisvfritz/5f0a639590d6e648933416f90ba7ae4e/raw/974aa47f8f9c5361c5233bd56be37db8ed765a09/currency-validator.js"></script>

<div id="app">
  <currency-input 
    label="Price" 
    v-model="price"
  ></currency-input>
  <currency-input 
    label="Shipping" 
    v-model="shipping"
  ></currency-input>
  <currency-input 
    label="Handling" 
    v-model="handling"
  ></currency-input>
  <currency-input 
    label="Discount" 
    v-model="discount"
  ></currency-input>
  
  <p>Total: ${{ total }}</p>
</div>
Vue.component('currency-input', {
  template: '\
    <div>\
      <label v-if="label">{{ label }}</label>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)"\
        v-on:focus="selectAll"\
        v-on:blur="formatValue"\
      >\
    </div>\
  ',
  props: {
    value: {
      type: Number,
      default: 0
    },
    label: {
      type: String,
      default: ''
    }
  },
  mounted: function () {
    this.formatValue()
  },
  methods: {
    updateValue: function (value) {
      var result = currencyValidator.parse(value, this.value)
      if (result.warning) {
        this.$refs.input.value = result.value
      }
      this.$emit('input', result.value)
    },
    formatValue: function () {
      this.$refs.input.value = currencyValidator.format(this.value)
    },
    selectAll: function (event) {
      // Workaround for Safari bug
      // http://stackoverflow.com/questions/1269722/selecting-text-on-focus-using-jquery-not-working-in-safari-and-chrome
      setTimeout(function () {
      	event.target.select()
      }, 0)
    }
  }
})

new Vue({
  el: '#app',
  data: {
    price: 0,
    shipping: 0,
    handling: 0,
    discount: 0
  },
  computed: {
    total: function () {
      return ((
        this.price * 100 + 
        this.shipping * 100 + 
        this.handling * 100 - 
        this.discount * 100
      ) / 100).toFixed(2)
    }
  }
})

Настройка v-model у компонента

Модификатор v-model на компоненте по умолчанию использует входной параметр value и событие input. Однако, некоторые типы полей, чекбоксы или радио-кнопки, могут использовать входной параметр value для других целей. Опция model позволит избежать конфликта в таких случаях:

Vue.component('my-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean,
    // это позволит использовать входной параметр `value` для других целей
    value: String
  },
  // ...
})
<my-checkbox v-model="foo" value="some value"></my-checkbox>

Аналогично работает следующий код:

<my-checkbox
  :checked="foo"
  @change="val => { foo = val }"
  value="some value">
</my-checkbox>

Обратите внимание, что все равно нужно явно объявить checked во входных данных.

Коммуникация между компонентами, не связанными иерархически

Для обмена данными между компонентами, которые не состоят в отношениях родитель-потомок, можно использовать пустой экземпляр Vue в качестве глобальной шины событий:

var bus = new Vue()
// в методе компонента A
bus.$emit('id-selected', 1)
// в обработчике created компонента B
bus.$on('id-selected', function (id) {
  // ...
})

Распределение контента слотами

Иногда, хочется вкладывать компоненты друг в друга следующим образом:

<app>
  <app-header></app-header>
  <app-footer></app-footer>
</app>

Здесь нужно обратить внимание на две вещи:

  1. Компонент <app> не знает, какой контент будет ему передан. Это определяется компонентом, использующим <app>.
  2. У компонента <app> есть собственный шаблон.

Чтобы это сработало, нужен метод "переплетения" шаблона компонента в внутреннего содержимого, указанного при его использовании в родителе. Такой процесс называется - распределением контента.

Область видимости при компиляции

Рассмотрим, в какой области видимости компилируется содержимое шаблонов. 

<child-component>
  {{ message }}
</child-component>

Из какого контекста берётся переменная message, контекста родителя или контекста потомка? Он берется - из контекста родителя. Здесь есть простое правило:

Всё в шаблоне родительского компонента компилируется в области видимости родителя; всё в шаблоне потомка — в области видимости потомка.

Часто можно встретить ошибку, когда в шаблоне родителя указывается связывание со свойством компонента-потомка:

<!-- НЕ сработает -->
<child-component v-show="someChildProperty"></child-component>

Если someChildProperty свойство потомка, код выше не будет работать. Шаблон родителя не имеет представления о состоянии компонента-потомка.

Если все же нужно привязывать директивы из области видимости компонента-потомка к корневому элементу, это нужно сделать в его же шаблоне:

Vue.component('child-component', {
  // такой вариант сработает, поскольку мы находимся
  // в правильной области видимости
  template: '<div v-show="someChildProperty">Child</div>',
  data: function () {
    return {
      someChildProperty: true
    }
  }
})

Контент, полученный через систему распределения, будет компилироваться в родительской области видимости.

Вариант с единственным слотом

Если в шаблоне компонента-потомка нет хотя бы одного элемента <slot>, родительский компонент будет отброшен. Если слот один и без атрибутов, то содержимое родительского элемента будет помещено в DOM на место слота, заместив его собой. 

Начальное содержимое <slot> считается резервным контентом и компилируется в области видимости компонента-потомка, отображаясь только если родительский элемент пустой и не содержит никакого контента для потомка.

Например, есть компонент my-component с шаблоном:

<div>
  <h2>Заголовок компонента-потомка</h2>
  <slot>
    Этот текст будет отображён, только если
    не будет передано контента для дистрибьюции.
  </slot>
</div>

А также родитель, использующий этот компонент:

<div>
  <h1>Заголовок компонента-родителя</h1>
  <my-component>
    <p>Немного оригинального контента</p>
    <p>И ещё немного</p>
  </my-component>
</div>

Результат рендинга:

<div>
  <h1>Заголовок компонента-родителя</h1>
  <div>
    <h2>Заголовок компонента-потомка</h2>
    <p>Немного оригинального контента</p>
    <p>И ещё немного</p>
  </div>
</div>

Именованные слоты

Для элементов <slot> можно указать специальный атрибут name, использующийся для более гибкой дистрибьюции контента. Можно создать несколько слотов с разными именами. Именованный слот получит весь контент, находящийся в элементе с соответствующим значением атрибута slot.

Если одному из слотов не задать имя, он станет слотом по умолчанию, в который попадёт весь контент, для которого имя слота не указано. При отсутствии безымянного слота, контент будет отброшен.

Например, есть компонент app-layout с шаблоном:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

Шаблон родителя:

<app-layout>
  <h1 slot="header">Здесь мог бы быть заголовок страницы</h1>

  <p>Абзац основного контента.</p>
  <p>И ещё один.</p>

  <p slot="footer">Вот контактная информация</p>
</app-layout>

Результат рендинга:

<div class="container">
  <header>
    <h1>Здесь мог бы быть заголовок страницы</h1>
  </header>
  <main>
    <p>Абзац основного контента.</p>
    <p>И ещё один.</p>
  </main>
  <footer>
    <p>Вот контактная информация</p>
  </footer>
</div>

Слоты с ограниченной областью видимости

Слот с ограниченной областью видимости — это особый тип слота, применяется как повторно используемый шаблон, в который передаются данные, а не отрендеренные элементы.
В компоненте-потомке нужно передать данные в слот также, как и входные параметры передаются в компонент:

<div class="child">
  <slot text="сообщение от потомка"></slot>
</div>

В родительском элементе должен существовать элемент <template> со специальным атрибутом slot-scope, указывающим, что он является шаблоном для слота с ограниченной областью видимости. 
Значение slot-scope будет использовано в качестве имени временной переменной, содержащей входные параметры, переданные от потомка:

<div class="parent">
  <child>
    <template slot-scope="props">
      <span>сообщение от родителя</span>
      <span>{{ props.text }}</span>
    </template>
  </child>
</div>

Результат рендинга:

<div class="parent">
  <div class="child">
    <span>сообщение от родителя</span>
    <span>сообщение от потомка</span>
  </div>
</div>

Характерное применение для слотов с ограниченной областью видимости — это компонент, выводящий список элементов, где можно переопределить вид элемента:

<my-awesome-list :items="items">
  <!-- слот с ограниченной областью видимости может быть и именованным -->
  <li
    slot="item"
    slot-scope="props"
    class="my-fancy-item">
    {{ props.text }}
  </li>
</my-awesome-list>
<ul>
  <slot name="item"
    v-for="item in items"
    :text="item.text">
    <!-- здесь — контент для резервного отображения -->
  </slot>
</ul>

Деструктурирование

Значение slot-scope является валидным выражением JavaScript и может использоваться в аргументе сигнатуры функции. Поэтому в поддерживаемых окружениях (в однофайловых компонентах или в современных браузерах) можно использовать деструктурирование ES2015 в выражениях:

<child>
  <span slot-scope="{ text }">{{ text }}</span>
</child>

Динамическое переключение компонентов

Чтобы подключить несколько компонентов и динамически переключаться между ними, используется псевдоэлемент <component> и динамическое связывание его атрибута is:

var vm = new Vue({
  el: '#example',
  data: {
    currentView: 'home'
  },
  components: {
    home: { /* ... */ },
    posts: { /* ... */ },
    archive: { /* ... */ }
  }
})
<component v-bind:is="currentView">
  <!-- изменяя vm.currentView можно переключаться между компонентами -->
</component>

Также можно связываться с объектами компонентов напрямую:

var Home = {
  template: '<p>Добро пожаловать домой!</p>'
}

var vm = new Vue({
  el: '#example',
  data: {
    currentView: Home
  }
})

keep-alive - сохранение отключенных компонентов в памяти

Чтобы хранить отключённые компоненты в памяти, не терять их состояния и не выполнять их повторный рендеринг, нужно обернуть динамический компонент в псевдоэлемент <keep-alive>:

<keep-alive>
  <component :is="currentView">
    <!-- неактивные компоненты будут закешированы! -->
  </component>
</keep-alive>

Создание компонентов для повторного использования

Если компоненты одноразовые и нигде больше не будут использоваться, они могут быть и сильно связанными. Если компоненты будут использоваться повторно, то нужно определить четкий публичный интерфейс, без излишних предположений о контексте использования компонента.

API компонентов Vue состоит из 3 частей: входных параметров, событий и слотов:

  • Входные параметры передают в компонент данные извне.
  • События позволяют компонентам воздействовать на внешнее окружение
  • Слоты позволяют внешнему окружению дополнять компоненты новым контентом

С помощью сокращённого синтаксиса v-bind и v-on, назначение компонента можно коротко и ясно выразить в шаблоне:

<my-component
  :foo="baz"
  :bar="qux"
  @event-a="doThis"
  @event-b="doThat"
>
  <img slot="icon" src="...">
  <p slot="main-text">Привет!</p>
</my-component>

Ссылки на компоненты-потомки

Несмотря на входные параметры и события, иногд возникает необходимость обратиться к компонентам-потомкам в JavaScript напрямую. 
Для этого можно с помощью атрибута ref назначить компоненту идентификатор:

<div id="parent">
  <user-profile ref="profile"></user-profile>
</div>
var parent = new Vue({ el: '#parent' })
// получаем экземпляр компонента-потомка
var child = parent.$refs.profile

Когда ref используется вместе с директивой v-for, будет возвращаться массив ссылок на экземпляры, повторяющий исходные данные.

Объект $refs заполняется только после рендеринга компонента и не является реактивным. Это крайнее средство для вмешательства в работу компонента-потомка. Не используйте $refs в шаблонах и вычисляемых свойствах.

Асинхронные компоненты

Очень удобно разделить крупное приложение на части и подгружать компоненты с сервера только тогда, когда в них возникнет необходимость. 
Vue позволяет определить компонент как функцию-фабрику, асинхронно возвращающую определение компонента. Вызов фабричной функции происходит только тогда, когда компонент понадобится, и закеширует результат для дальнейшего использования.

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // Передаём шаблон компонента в коллбэк resolve
    resolve({
      template: '<div>Я — асинхронный!</div>'
    })
  }, 1000)
})

Функция-фабрика принимает параметр resolve — коллбэк, вызываемый после получения определения компонента от сервера. 

Можно вызвать reject(reason), если загрузка по какой-то причине не удалась. 
setTimeout используется исключительно в демонстрационных целях; как именно получать компонент в реальной ситуации — решать только вам самим. 

Один из удачных подходов — это асинхронные компоненты в связке с функциями Webpack по разделению кода:

Vue.component('async-webpack-example', function (resolve) {
  // специальный синтаксис require укажет Webpack
  // автоматически разделить сборку на части
  // для последующей асинхронной загрузки
  require(['./my-async-component'], resolve)
})

Также можно вернуть Promise в функции-фабрике, так что с Webpack 2 и синтаксисом ES2015 можно сделать так:

Vue.component(
  'async-webpack-example',
  // Функция `import` возвращает `Promise`.
  () => import('./my-async-component')
)

При использовании локальных компонентов, можно указывать функцию, которая возвращает Promise:

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }

При использовании Browserify асинхронная загрузка компонентов невозможна, та как создатели Browserify не считают асинхронную загрузку функцией, которую они когда-либо будут поддерживать

Продвинутые асинхронные компоненты

Фабрика асинхронного компонента также может возвращать объект следующего формата:

const AsyncComp = () => ({
  // Загружаемый компонент. Значение должно быть Promise
  component: import('./MyComp.vue'),
  // Компонент загрузки, используемый пока загружается асинхронный компонент
  loading: LoadingComp,
  // Компонент ошибки, используемый при неудачной загрузке
  error: ErrorComp,
  // Задержка перед показом компонента загрузки. По умолчанию: 200мс.
  delay: 200,
  // Компонент ошибки будет отображаться, если таймаут
  // был указан и время ожидания превышено. По умолчанию: Infinity (бесконечное ожидание).
  timeout: 3000
})

При использовании в качестве компонента маршрута в vue-router, эти свойства будут проигнорированы, так как асинхронные компоненты будут разрешаться до того, как будет выполнена маршрутная навигация. 

Соглашения по именованию компонентов

При регистрации компонентов или входных параметров, можно использовать kebab-case, camelCase или PascalCase.

// при определении компонента
components: {
  // регистрация с использованием kebab-case
  'kebab-cased-component': { /* ... */ },
  // регистрация с использованием camelCase
  'camelCasedComponent': { /* ... */ },
  // регистрация с использованием PascalCase
  'PascalCasedComponent': { /* ... */ }
}

В HTML-шаблонах, придется использовать эквивалентный kebab-case:

<!-- всегда используйте kebab-case в HTML-шаблонах -->
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<pascal-cased-component></pascal-cased-component>

При использовании строковых шаблонов ограничения регистронезависимости HTML не действуют. Поэтому даже в шаблоне можно указывать компоненты используя:

  • kebab-case
  • camelCase или kebab-case, если компонент объявлен используя camelCase
  • kebab-case, camelCase или PascalCase, если компонент объявлен используя PascalCase
components: {
  'kebab-cased-component': { /* ... */ },
  camelCasedComponent: { /* ... */ },
  PascalCasedComponent: { /* ... */ }
}
<kebab-cased-component></kebab-cased-component>

<camel-cased-component></camel-cased-component>
<camelCasedComponent></camelCasedComponent>

<pascal-cased-component></pascal-cased-component>
<pascalCasedComponent></pascalCasedComponent>
<PascalCasedComponent></PascalCasedComponent>

PascalCase является наиболее универсальным соглашением объявления, а kebab-case наиболее универсальным соглашением использования.
Если компонент не содержит слотов, его можно сделать самозакрывающимся, указав / после имени:

<my-component/>

Это возможно только при использовании строковых шаблонов, так как самозакрывающие пользовательские элементы не соответствуют нормам языка HTML, и нативные парсеры браузеров не поймут такую запись.

Рекурсивные компоненты

Компоненты могут рекурсивно вызывать самих себя в своих шаблонах. Но это возможно только при указании опции name:

name: 'unique-name-of-my-component'

Если компонент регистрируется глобально через Vue.component, то опция name компонента автоматически становится равной его глобальному ID:

Vue.component('unique-name-of-my-component', {
  // ...
})

Рекурсивные компоненты могут привести к появлению бесконечных циклов:

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

Использование такого компонента приведёт к ошибке переполнения стека, поэтому следите, чтобы директива v-if в рекурсивном вызове когда-нибудь стала false.

Циклические ссылки между компонентами

Рассмотрим пример, когда проектируется каталог файлов в виде дерева и используется компонент tree-folder с шаблоном:

<p>
  <span>{{ folder.name }}</span>
  <tree-folder-contents :children="folder.children"/>
</p>

Затем компонент tree-folder-contents с таким шаблоном:

<ul>
  <li v-for="child in children">
    <tree-folder v-if="child.children" :folder="child"/>
    <span v-else>{{ child.name }}</span>
  </li>
</ul>

Посмотрите на пример. Каждый из этих компонентов одновременно является и потомком, и родителем другого компонента! При глобальной регистрации компонентов через Vue.component это будет разрешено автоматически. 

С другой стороны, если компоненты импортируются с помощью модульного сборщика - Webpack или Browserify, возникнет ошибка.

Failed to mount component: template or render function not defined.

Давайте назовём наши компоненты A и B. Модульный сборщик видит, что ему нужен компонент A, но A сперва нужен B, но B нужен A, и т.д. Сборщик застревает в цикле, не зная как разрешить оба компонента. Чтобы это исправить, нужно указать сборщику точку, в которой он сможет сказать: “Рано или поздно для разрешения A нужно разрешить B, но нет необходимости разрешать B прямо сейчас.”

Для этого сделаем такой точкой компонент tree-folder. Ккомпонент-потомок, порождающий парадокс — это tree-folder-contents. Поэтому не будем его регистрировать, пока не наступит событие жизненного цикла beforeCreate.

beforeCreate: function () {
  this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue')
}

Ура! Проблема решена.

Inline-шаблоны

Если у компонента-потомка указан специальный атрибут inline-template, содержимое элемента будет использовано не для распределения контента, а в качестве шаблона этого компонента. 

<my-component inline-template>
  <div>
    <p>Этот шаблон будет скомпилирован в области видимости компонента-потомка.</p>
    <p>Доступа к данным родителя нет.</p>
  </div>
</my-component>

Использование inline-template затрудняет понимание происходящего в шаблонах. Желательно задавать шаблоны внутри компонента с помощью опции template или в элементе template файла .vue.

Определение шаблонов через X-Template

Ещё один способ задания шаблонов — это элементы script с типом text/x-template и идентификатором, на который можно сослаться при регистрации шаблона.

<script type="text/x-template" id="hello-world-template">
  <p>Привет привет привет</p>
</script>
Vue.component('hello-world', {
  template: '#hello-world-template'
})

Эта полезно для демонстрационных приложений с большими шаблонами или для очень маленьких приложений. Но такого подхода следует избегать, так как шаблоны отделяются от остальных частей определения компонента.

Дешёвые статические компоненты с использованием v-once

Рендеринг простых элементов HTML в Vue быстрый, но иногда встречаются компоненты, в которых статических данных очень много. 
Поэтому можно указать в корневом элементе директиву v-once и компонент будет вычислен только в первый раз, а потом с кэшированной версией.

Vue.component('terms-of-service', {
  template: '\
    <div v-once>\
      <h1>Условия Использования</h1>\
      ... много-много статического контента ...\
    </div>\
  '
})