Skip to content

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

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

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

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

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

При использовании шага сборки мы обычно определяем каждый компонент Vue в отдельном файле с расширением .vue - это называется однофайловый компонент (по англ. Single-File Component или, сокращённо, 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>`
  // Can also target an in-DOM template:
  // 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.

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

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

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

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

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

Когда значение передается атрибуту prop, оно становится свойством этого экземпляра компонента. Значение этого свойства доступно в шаблоне и в контексте компонента 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:

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.

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

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> и не требует импорта. defineEmits возвращает функцию 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>

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

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

Произошло что-то плохое.

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

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

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

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

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

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

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

Это возможно сделать с помощью элемента <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-шаблонов, необходимо указывать имена входных параметров в camelCase и обработчики событий в kebab-case (разделённые дефисом) эквивалентах:

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>hello</span>

будет распарсено как:

template
<my-component>
  <span>hello</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 самостоятельно - построить что-нибудь интересное или ознакомиться с примерами, если вы еще этого не сделали.

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

Основы компонентовУже загружено