Основы компонентов
Компоненты позволяют нам разделить пользовательский интерфейс на независимые и многократно используемые части и думать о каждой части отдельно. Обычно приложение организовано в виде дерева вложенных друг в друга компонентов:
Это очень похоже на то, как мы вкладываем родные HTML-элементы, но Vue реализует собственную модель компонентов, которая позволяет нам инкапсулировать пользовательский контент и логику в каждый компонент. Vue также отлично сочетается с нативными веб-компонентами. Если вам интересно, как соотносятся компоненты Vue и нативные веб-компоненты, подробнее читайте здесь.
Определение компонента
При использовании этапа сборки мы обычно определяем каждый компонент Vue в отдельном файле с расширением .vue
— так называемый однофайловый компонент (сокращённо SFC):
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Ты нажал на меня {{ count }} раз.</button>
</template>
Если не использовать этап сборки, компонент Vue можно определить как обычный объект JavaScript, содержащий специфические для Vue параметры:
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 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
, мы должны объявить его в списке принимаемых компонентом пропсов, используя макрос defineProps
:
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
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
через ref:
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
.
Мы можем опционально объявить испускаемые события с помощью макроса defineEmits
:
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="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 самостоятельно — построить что-нибудь интересное или посмотреть некоторые из примеров, если вы этого ещё не сделали.
Как только вы почувствуете себя комфортно после полученных знаний, переходите к изучению руководства, чтобы узнать больше о компонентах.