Перейти к содержанию

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

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

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

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

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

При использовании этапа сборки мы обычно определяем каждый компонент Vue в отдельном файле с расширением .vue — так называемый однофайловый компонент (сокращённо SFC):

vue
<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <button @click="count++">Ты нажал на меня {{ count }} раз.</button>
</template>
vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">Ты нажал на меня {{ count }} раз.</button>
</template>

Если не использовать этап сборки, компонент Vue можно определить как обычный объект JavaScript, содержащий специфические для Vue параметры:

js
export default {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      Ты нажал на меня {{ count }} раз
    </button>`
}
js
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return { count }
  },
  template: `
    <button @click="count++">
      Ты нажал на меня {{ count }} раз
    </button>`
  // Также можно настроить таргетинг на шаблон в DOM:
  // template: '#my-template-element'
}

Шаблон вставляется сюда в виде строки JavaScript, которую Vue компилирует на лету. Вы также можете использовать селектор ID, указывающий на элемент (обычно это собственные элементы <template>) — Vue будет использовать его содержимое в качестве источника шаблона.

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

Использование компонента

Примечание

До конца этого руководства мы будем использовать синтаксис SFC — независимо от того, используете вы этап сборки или нет. В разделе Примеры показано использование компонентов в обоих сценариях.

Чтобы использовать дочерний компонент, нам нужно импортировать его в родительский компонент. Если мы поместили наш компонент счётчика в файл ButtonCounter.vue, то компонент будет экспортирован в файл по умолчанию:

vue
<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  }
}
</script>

<template>
  <h1>Вот дочерний компонент!</h1>
  <ButtonCounter />
</template>

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

vue
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

<template>
  <h1>Вот дочерний компонент!</h1>
  <ButtonCounter />
</template>

С помощью <script setup> импортированные компоненты автоматически становятся доступными для шаблона.

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

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

template
<h1>Здесь много дочерних компонентов!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

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

В SFC рекомендуется использовать имена тегов в регистре PascalCase для дочерних компонентов, чтобы отличить их от собственных HTML-элементов. Хотя в родном HTML имена тегов не чувствительны к регистру, Vue SFC — это скомпилированный формат, поэтому мы можем использовать в нем имена тегов, чувствительные к регистру. Мы также можем использовать /> для закрытия тега.

Если вы создаете свои шаблоны непосредственно в DOM (например, как содержимое собственного элемента <template>), шаблон будет подчиняться собственному поведению браузера при разборе HTML. В таких случаях необходимо использовать kebab-case и явные закрывающие теги для компонентов:

template
<!-- если этот шаблон записан в DOM -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

Подробнее см. в разделе Предостережения по разбору шаблонов в DOM.

Передача пропсов

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

Пропсы — это пользовательские атрибуты, которые вы можете зарегистрировать для компонента. Чтобы передать заголовок компоненту BlogPost, мы должны объявить его в списке принимаемых компонентом пропсов, используя свойство propsмакрос defineProps:

vue
<!-- BlogPost.vue -->
<script>
export default {
  props: ['title']
}
</script>

<template>
  <h4>{{ title }}</h4>
</template>

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

vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>

defineProps — это макрос компилятора, который доступен только внутри <script setup> и не нуждается в явном импорте. Объявленные пропсы автоматически отображаются в шаблоне. defineProps также возвращает объект, содержащий все переданные компоненту пропсы, чтобы при необходимости мы могли получить к ним доступ в JavaScript:

js
const props = defineProps(['title'])
console.log(props.title)

Смотрите также: Типизация пропсов компонента

Если вы не используете <script setup>, пропсы должны быть объявлены с помощью свойства props, и объект props будет передан в setup() в качестве первого аргумента:

js
export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

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

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

template
<BlogPost title="Мое путешествие с Vue" />
<BlogPost title="Ведение блога с помощью Vue" />
<BlogPost title="Почему Vue так интересен" />

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

js
export default {
  // ...
  data() {
    return {
      posts: [
        { id: 1, title: 'Мое путешествие с Vue' },
        { id: 2, title: 'Ведение блога с помощью Vue' },
        { id: 3, title: 'Почему Vue так интересен' }
      ]
    }
  }
}
js
const posts = ref([
  { id: 1, title: 'Мое путешествие с Vue' },
  { id: 2, title: 'Ведение блога с помощью Vue' },
  { id: 3, title: 'Почему Vue так интересен' }
])

Затем нужно вывести компонент для каждого из них, используя v-for:

template
<BlogPost
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
 />

Обратите внимание, как синтаксис v-bind (:title="post.title") используется для передачи динамических значений пропсов. Это особенно полезно, когда вы не знаете заранее, какой именно контент вы собираетесь отображать.

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

Прослушивание событий

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

В родителе мы можем поддержать эту возможность, добавив свойство postFontSize через dataref:

js
data() {
  return {
    posts: [
      /* ... */
    ],
    postFontSize: 1
  }
}
js
const posts = ref([
  /* ... */
])

const postFontSize = ref(1)

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

template
<div :style="{ fontSize: postFontSize + 'em' }">
  <BlogPost
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
   />
</div>

Теперь давайте добавим кнопку в шаблон компонента <BlogPost>.:

vue
<!-- BlogPost.vue, пропускаем <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button>Увеличить текст</button>
  </div>
</template>

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

template
<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

Затем дочерний компонент может вызвать событие на себя, вызвав встроенный метод $emit, передав имя события:

vue
<!-- BlogPost.vue, пропускаем <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Увеличить текст</button>
  </div>
</template>

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

Мы можем опционально объявить испускаемые события с помощью свойства emitsмакроса defineEmits:

vue
<!-- BlogPost.vue -->
<script>
export default {
  props: ['title'],
  emits: ['enlarge-text']
}
</script>
vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

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

Как и defineProps, defineEmits используется только в <script setup> и не нуждается в импорте. Он возвращает функцию emit, которая эквивалентна методу $emit. Его можно использовать для эмиссии событий в секции <script setup> компонента, где $emit недоступен напрямую:

vue
<script setup>
const emit = defineEmits(['enlarge-text'])

emit('enlarge-text')
</script>

Смотрите также: Типизация событий компонента

Если вы не используете <script setup>, вы можете объявить испускаемые события с помощью опции emits. Вы можете получить доступ к функции emit как к свойству контекста установки (передается в setup() в качестве второго аргумента):

js
export default {
  emits: ['enlarge-text'],
  setup(props, ctx) {
    ctx.emit('enlarge-text')
  }
}

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

Распространение контента с помощью слотов

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

template
<AlertBox>
  Случилось что-то плохое.
</AlertBox>

Что может выглядеть примерно так:

Это ошибка для демонстрационных целей

Случилось что-то плохое.

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

vue
<!-- AlertBox.vue -->
<template>
  <div class="alert-box">
    <strong>Это ошибка для демонстрационных целей</strong>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  /* ... */
}
</style>

Как вы видите выше, мы используем <slot> как место, куда мы хотим поместить содержимое — и это всё. Мы закончили!

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

Динамические компоненты

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

Вышеописанное стало возможным благодаря элементу Vue <component> со специальным атрибутом is:

template
<!-- Компонент изменяется при изменении CurrentTab -->
<component :is="currentTab"></component>
template
<!-- Компонент изменяется при изменении CurrentTab -->
<component :is="tabs[currentTab]"></component>

В приведённом выше примере значение, переданное в :is, может содержать либо:

  • строку с именем зарегистрированного компонента, ИЛИ
  • фактический импортированный объект компонента

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

При переключении между несколькими компонентами с помощью <component :is="..."> каждый компонент будет размонтирован при переключении на следующий. Мы можем заставить неактивные компоненты оставаться «живыми» с помощью встроенного компонента <KeepAlive>.

Предостережения по разбору шаблонов в DOM

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

Примечание

Следует отметить, что ограничения, рассмотренные ниже, применимы только в том случае, если вы пишете свои шаблоны непосредственно в DOM. Они НЕ применяются, если вы используете шаблоны строк из следующих источников:

  • Однофайловые компоненты
  • Вложенные строки шаблонов (например, template: '...')
  • <script type="text/x-template">

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

HTML-теги и имена атрибутов нечувствительны к регистру, поэтому браузеры интерпретируют любые символы верхнего регистра как строчные. Это означает, что когда вы используете шаблоны в DOM, имена компонентов PascalCase и имена свойств в верблюжьем регистре или имена событий v-on должны использовать их эквиваленты в кебабном регистре (с разделителями-дефисами):

js
// camelCase (верблюжий регистр) в JavaScript
const BlogPost = {
  props: ['postTitle'],
  emits: ['updatePost'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
}
template
<!-- kebab-case в HTML -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

Самозакрывающиеся теги

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

template
<MyComponent />

Это связано с тем, что анализатор шаблонов Vue рассматривает /> как указание на завершение любого тега, независимо от его типа.

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

template
<my-component></my-component>

Это связано с тем, что спецификация HTML позволяет опускать закрывающие теги только для нескольких определённых элементов, наиболее распространёнными из которых являются <input> и <img>. Для всех остальных элементов, если вы опустите закрывающий тег, родной HTML-парсер будет считать, что вы не завершили открывающий тег. Например, следующий фрагмент:

template
<my-component /> <!-- мы намерены закрыть тег здесь... -->
<span>привет</span>

будет разобран как:

template
<my-component>
  <span>привет</span>
</my-component> <!-- но браузер закроет его здесь. -->

Ограничения на размещение элементов

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

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

template
<table>
  <blog-post-row></blog-post-row>
</table>

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

template
<table>
  <tr is="vue:blog-post-row"></tr>
</table>

Примечание

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

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

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

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