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

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

Пример компонента 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 Основы компонентов 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 Основы компонентов: переиспользование компонентов by Vue (@Vue) on CodePen.

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

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

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

Дерево компонентов

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

Чтобы использовать компоненты в шаблонах, сначала их нужно зарегистрировать, чтобы 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 Основы компонентов: передача входных параметров 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 Основы компонентов: прослушивание событий 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 в компоненте будет использование вычисляемых свойств и их возможностей определить геттер и сеттер. Метод 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 Основы компонентов: слоты 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 Основы компонентов: динамическое переключение компонентов 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-шаблона

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

Совет

Следует отметить, что ограничения, обсуждаемые ниже, применимы только в том случае, если пишете шаблоны непосредственно в DOM. Таких ОГРАНИЧЕНИЙ НЕ БУДЕТ при использовании строковых шаблонов из следующих источников:

# Ограничение по расположению элементов

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

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

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

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

<table>
  <tr is="vue:blog-post-row"></tr>
</table>
1
2
3

Совет

При использовании на нативных HTML-элементах значение is должно начинаться с префикса vue:, чтобы интерпретироваться как компонент Vue. Это нужно чтобы избежать путаницы с нативными пользовательскими встроенными элементами (opens new window).

# Отсутствие чувствительности к регистру

Имена атрибутов 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 и самостоятельно сделать что-нибудь интересное.

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