# Основы компонентов

# Базовый пример

Вот пример компонента Vue:

// Создаём приложение Vue
const app = Vue.createApp({})

// Определяем новый глобальный компонент с именем button-counter
app.component('button-counter', {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      Счётчик кликов — {{ count }}
    </button>`
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Информация

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

Компоненты — переиспользуемые экземпляры со своим именем. В примере выше это <button-counter>. Его можно использовать как пользовательский тег внутри корневого экземпляра:

<div id="components-demo">
  <button-counter></button-counter>
</div>
1
2
3
app.mount('#components-demo')
1

See the Pen Component basics by Vue (@Vue) on CodePen.

Так как компоненты это переиспользуемые экземпляры, то у них могут быть те же опции что и у корневого экземпляра, например data, computed, watch, methods, хуки жизненного цикла.

# Переиспользование компонентов

Компоненты можно переиспользовать столько раз, сколько захочется:

<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>
1
2
3
4
5

See the Pen Component basics: reusing components by Vue (@Vue) on CodePen.

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

# Организация компонентов

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

Component Tree

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

Чтобы использовать компоненты в шаблонах, сначала их нужно зарегистрировать, чтобы Vue узнал о них. Есть два типа регистрации компонентов: глобальная и локальная. Ранее компоненты регистрировались только глобально, используя метод приложения component:

const app = Vue.createApp({})

app.component('my-component-name', {
  // ... опции ...
})
1
2
3
4
5

Зарегистрированные глобально компоненты можно использовать в шаблоне любого компонента в приложении.

На данный момент это всё, что нужно знать о регистрации компонентов. Но когда закончите изучение этой страницы и разберётесь со всей информацией представленной здесь — рекомендуем вернуться позднее и прочитать полное руководство по регистрации компонентов.

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

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

Входные параметры — пользовательские атрибуты, которые указываются на компоненте. Чтобы передавать заголовок в компонент записи блога, нужно включить его в список входных параметров, которые принимает компонент, с помощью опции props:

const app = Vue.createApp({})

app.component('blog-post', {
  props: ['title'],
  template: `<h4>{{ title }}</h4>`
})

app.mount('#blog-post-demo')
1
2
3
4
5
6
7
8

Когда значение передаётся атрибуту входного параметра, оно станет свойством данного экземпляра. Значение этого свойства доступно в шаблоне, как и любое другое свойство компонента.

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

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

<div id="blog-post-demo" class="demo">
  <blog-post title="My journey with Vue"></blog-post>
  <blog-post title="Blogging with Vue"></blog-post>
  <blog-post title="Why Vue is so fun"></blog-post>
</div>
1
2
3
4
5

See the Pen Component basics: passing props by Vue (@Vue) on CodePen.

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

const App = {
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}

const app = Vue.createApp(App)

app.component('blog-post', {
  props: ['title'],
  template: `<h4>{{ title }}</h4>`
})

app.mount('#blog-posts-demo')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

А значит нужно отрисовать компонент для каждого из элементов массива:

<div id="blog-posts-demo">
  <blog-post
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
  ></blog-post>
</div>
1
2
3
4
5
6
7

Как видно выше, можно использовать v-bind для динамической передачи входных параметров. Это полезно, когда точное содержимое для отрисовки заранее неизвестно.

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

# Прослушивание событий из дочерних компонентов в родительских компонентах

По мере доработки компонента <blog-post> для некоторых возможностей может потребоваться передавать данные обратно в родительский компонент. Например, для доступности позволить увеличивать размер текста записей блога, оставляя остальную часть страницы с размером текста по умолчанию.

В родительском компоненте можно добавить свойство postFontSize для этого:

const App = {
  data() {
    return {
      posts: [
        /* ... */
      ],
      postFontSize: 1
    }
  }
}
1
2
3
4
5
6
7
8
9
10

Которым воспользуемся в шаблоне для управления размером шрифта всех записей блога:

<div id="blog-posts-events-demo">
  <div :style="{ fontSize: postFontSize + 'em' }">
    <blog-post
      v-for="post in posts"
      :key="post.id"
      :title="post.title"
    ></blog-post>
  </div>
</div>
1
2
3
4
5
6
7
8
9

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

app.component('blog-post', {
  props: ['title'],
  template: `
    <div class="blog-post">
      <h4>{{ title }}</h4>
      <button>
        Увеличить размер текста
      </button>
    </div>
  `
})
1
2
3
4
5
6
7
8
9
10
11

Но проблема пока в том, что эта кнопка ничего не делает:

<button>
  Увеличить размер текста
</button>
1
2
3

При нажатии на кнопку нужно сообщить родительскому компоненту, чтобы увеличил размер текста для всех записей блога. Для решения этой проблемы, экземпляры компонента предоставляют собственную систему событий. Родительский компонент может прослушивать любые события на экземпляре дочернего компонента с помощью v-on или @, аналогично отслеживанию нативных событий DOM:

<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>
1

Тогда дочерний компонент может сгенерировать событие с помощью встроенного метода $emit, передавая ему имя события:

<button @click="$emit('enlargeText')">
  Увеличить размер текста
</button>
1
2
3

Благодаря прослушиванию события @enlarge-text="postFontSize += 0.1", родительский компонент отследит событие и обновится со значением postFontSize.

See the Pen Component basics: emitting events by Vue (@Vue) on CodePen.

Все генерируемые компонентом события можно перечислить в опции emits:

app.component('blog-post', {
  props: ['title'],
  emits: ['enlargeText']
})
1
2
3
4

Это позволит проверять все события, которые генерирует компонент, и опционально валидировать их.

# Передача данных вместе с событием

Иногда может потребоваться отправить определённые данные вместе с событием. Например, если захотим, чтобы компонент <blog-post> отвечал за то, насколько нужно увеличивать текст. В таком случае, для указания этого значения можно передавать второй параметр $emit:

<button @click="$emit('enlargeText', 0.1)">
  Увеличить размер текста
</button>
1
2
3

Тогда, при прослушивании события в родителе, можно получить доступ к переданным с событием данным через $event:

<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>
1

Или, если обработчик события будет методом:

<blog-post ... @enlarge-text="onEnlargeText"></blog-post>
1

То значение будет передано в него первым аргументом:

methods: {
  onEnlargeText(enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}
1
2
3
4
5

# Использование v-model на компонентах

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

<input v-model="searchText" />
1

делает то же самое, что и:

<input :value="searchText" @input="searchText = $event.target.value" />
1

Использование v-model на компоненте будет делать следующее:

<custom-input
  :model-value="searchText"
  @update:model-value="searchText = $event"
></custom-input>
1
2
3
4

ПРЕДУПРЕЖДЕНИЕ

Обратите внимание, что model-value указываем в kebab-case, потому что работаем с DOM-шаблонами. Подробное объяснение использования kebab-case или camelCase атрибутов в разделе особенностей парсинга DOM-шаблона

Чтобы всё вместе заработало, внутри компонента элемент <input> должен:

  • Привязывать значение атрибута value к входному параметру modelValue
  • По событию input генерировать событие update:modelValue с новым значением

Соберём всё вместе:

app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    />
  `
})
1
2
3
4
5
6
7
8
9
10

Теперь v-model будет прекрасно работать с этим компонентом:

<custom-input v-model="searchText"></custom-input>
1

Другим способом реализации v-model для компонента является использование возможностей свойств computed для определения геттера и сеттера. Метод get должен возвращать свойство modelValue, а метод set генерировать соответствующее событие:

app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input v-model="value">
  `,
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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

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

Как и с обычными HTML-элементами, часто бывает полезным передать компоненту содержимое, например:

<alert-box>
  Произошло что-то плохое.
</alert-box>
1
2
3

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

See the Pen Component basics: slots by Vue (@Vue) on CodePen.

Это можно достичь с помощью пользовательского элемента <slot> у Vue:

app.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Ошибка!</strong>
      <slot></slot>
    </div>
  `
})
1
2
3
4
5
6
7
8

Как видно выше, слот используется в качестве места, куда требуется подставлять контент — и это всё. Готово!

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

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

Иногда может потребоваться динамически переключаться между компонентами, например в интерфейсе с вкладками:

See the Pen Component basics: dynamic components by Vue (@Vue) on CodePen.

Это можно реализовать с помощью элемента <component> со специальным атрибутом is:

<!-- Компонент меняется при изменении currentTabComponent -->
<component :is="currentTabComponent"></component>
1
2

В примере выше currentTabComponent может содержать:

  • имя зарегистрированного компонента, или
  • объект с настройками компонента

Посмотрите этот sandbox (opens new window) чтобы поэкспериментировать с полным кодом, или эту версию (opens new window) для примера привязки к объекту с настройками компонента вместо указания его имени.

Также можно использовать атрибут is для создания обычных HTML-элементов.

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

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

У некоторых HTML-элементов, таких как <ul>, <ol>, <table> и <select>, есть ограничения на то, какие элементы могут отображаться внутри них, или например элементы <li>, <tr> и <option> могут появляться только внутри других определённых элементов.

Это приведёт к проблемам при использовании компонентов с элементами, которые имеют такие ограничения. Например:

<table>
  <blog-post-row></blog-post-row>
</table>
1
2
3

Пользовательский компонент <blog-post-row> будет поднят выше, поскольку считается недопустимым содержимым, приводя к ошибкам при отрисовке. Для решения этой проблемы существует специальная директива v-is:

<table>
  <tr v-is="'blog-post-row'"></tr>
</table>
1
2
3

ВНИМАНИЕ

Директива v-is работает как динамическая привязка в 2.x :is — поэтому для отрисовки компонента по его зарегистрированному имени значение должно быть строковым литералом JavaScript:

<!-- НЕПРАВИЛЬНО, ничего не будет отрисовано -->
<tr v-is="blog-post-row"></tr>

<!-- Правильно -->
<tr v-is="'blog-post-row'"></tr>
1
2
3
4
5

Кроме того, имена атрибутов HTML не чувствительны к регистру, поэтому браузеры будут интерпретировать любые заглавные символы как строчные. А значит, при использовании DOM-шаблонов, необходимо указывать имена входных параметров в camelCase и обработчики событий в kebab-case (разделённые дефисом) эквиваленте:

// camelCase в JavaScript
app.component('blog-post', {
  props: ['postTitle'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
})
1
2
3
4
5
6
7
<!-- kebab-case в HTML -->
<blog-post post-title="hello!"></blog-post>
1
2

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

На данный момент это всё, что нужно знать об особенностях парсинга DOM-шаблонов — и на самом деле это окончание раздела Основы документации Vue. Наши поздравления! Ещё есть чему поучиться, но рекомендуем сначала отвлечься и попробовать поиграться с Vue и самостоятельно построить что-нибудь интересное.

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

Deployed on Netlify.
Последнее обновление: 2021-01-07, 09:28:57 UTC