Разобравшись с реактивностью и взаимодействием компонентов, давайте посмотрим, какие основные функции остались. Возьмем слоты и атрибуты. Мы используем их в основном в шаблонах. Но что, если вам нужно получить к ним доступ в скрипте? Скажем, чтобы написать функциональный компонент?
Функциональные компоненты
Официальная документация рекомендует использовать HTML-шаблоны всегда, когда это возможно. И в большинстве случаев вам не придется обращаться к атрибутам и слотам программно.
При использовании функциональных компонентов вы, по сути, пропускаете процесс компиляции шаблонов в Vue. Вместо HTML вы передаете объявления виртуальных узлов в конвейер рендеринга фреймворка. А без шаблонов нет места для вложения атрибутов и слотов.
Вы заметите, что в итоге мы все равно будем использовать шаблоны. Потерпите меня.
Это может быть очень полезно в случаях, когда вам нужен низкоуровневый контроль над поведением вашего приложения. Но за это приходится платить расширенным шаблоном.
Атрибуты и слоты
Атрибуты в Vue 3
Если вы внимательно читали мою предыдущую статью о v-model – binding, то, вероятно, заметили директиву v-bind
, которая осталась без объяснения:
<input
class="input__field"
v-bind="$attrs"
:placeholder="label ? label : ''"
/>
$attrs
включает карту всех HTML – fallthrough-атрибутов, которые были переданы в компонент. Она автоматически предоставляется Vue при установке компонента. Используя v-bind
, мы можем декларативно привязать карту к элементу ввода. В противном случае Vue привяжет их к внешнему div
. Особенно для атрибутов доступности мы предпочтем первый вариант.
Атрибуты доступны только в шаблоне. Если мы хотим получить к ним программный доступ, мы должны использовать помощник useAttrs
.
import { useAttrs } from 'vue';
const attrs = useAttrs();
Слоты в Vue 3
В какой-то момент реквизиты не удовлетворят потребности вашего приложения в динамическом контенте. Тогда в игру вступают слоты. Они позволяют внедрять целые шаблоны в дочерние компоненты.
Предположим, у вас есть компонент AppCard.vue
. Он действует как внешний, стилизованный слой для своего содержимого и выглядит примерно так:
<template>
<section class="card">
<header class="card__header">
Some card header
</header>
<main class="card__body">
This is some card content
</main>
<footer class="card__footer">
This is a card footer
</footer>
</section>
</template>
Мы можем заменить статическое содержимое именованными слотами:
(вернуться к разделу “Функциональный компонент контейнера”)
<template>
<section class="card">
<header class="card__header">
<h3>
<slot name="header" />
</h3>
</header>
<main class="card__body">
<slot name="default" />
</main>
<footer class="card__footer">
<slot name="footer" />
</footer>
</section>
</template>
А во внешнем компоненте передайте слоты с помощью синтаксиса v-slot:<slotname>
:
<template>
<app-card>
<template v-slot:header> Card header</template>
<template v-slot:default>
<p>
Lorem ipsum, dolor sit amet consectetur adipisicing elit.
Enim ipsa ullam culpa explicabo amet alias nemo!
</p>
</template>
<template v-slot:footer> Card footer </template>
</app-card>
</template>
Чтобы получить программный доступ к slots
, мы должны использовать помощник useSlots
, экспортируемый Vue:
import { useAttrs } from 'vue';
const attrs = useAttrs();
Функциональный компонент-контейнер
Давайте пока остановимся на примере карточки. Что если вы захотите, чтобы карточка была обернута другим тегом? Или меньший заголовок для заголовка? Скажем, h4
вместо h3
?
Подобные проблемы вряд ли можно решить в статичном шаблоне. Функциональные компоненты сияют здесь – они обеспечивают необходимую гибкость и не требуют (почти) никакого шаблона.
Давайте продемонстрируем это. Мы захотим создать более гибкий компонент-контейнер. Он всегда должен:
- иметь поля слева и справа
- заполнять 100% горизонтального пространства просмотра, но не более 1200px.
Кроме того, он должен быть в двух комбинируемых вариантах:
- центрированный – все элементы внутри контейнера центрируются по горизонтали и вертикали
- page – контейнер заполняет 100% вертикального пространства просмотра.
Результат будет выглядеть следующим образом:
Начните с этого репозитория Github. Клонируйте его на свою локальную машину и:
- создайте компонент
AppContainer.vue
вsrc/components/
. - (необязательно): создайте компонент
AppCard.vue
вsrc/components/
и заполните его динамическим компонентом ‘Slots in Vue 3’ - возьмите приведенный ниже код шаблона и добавьте его в соответствующий файл
AppContainer.vue
<script setup lang="ts">
import { h, Component, useSlots, useAttrs } from 'vue';
interface AppContainerProps {
tag: keyof HTMLElementTagNameMap;
page?: boolean;
centered?: boolean;
}
type AppContainerClass = 'container--centered' | 'container--page';
const props = withDefaults(defineProps<AppContainerProps>(), {
tag: 'div',
centered: false,
page: false,
});
const propClassMap: {
prop: keyof AppContainerProps;
class: AppContainerClass;
}[] = [
{
prop: 'centered',
class: 'container--centered',
},
{
prop: 'page',
class: 'container--page',
},
];
const assembleContainerClasses = () => {
let containerClasses = ['container'];
propClassMap.forEach((entry) => {
if ([props[entry.prop]]) {
containerClasses.push(entry.class);
}
});
return containerClasses;
};
</script>
<style scoped>
.container {
background-color: var(--background-color-tartiary);
margin: auto;
width: 100vw;
max-width: 1200px;
padding-left: 5rem;
padding-right: 5rem;
}
.container--centered {
display: flex;
flex-grow: 0;
flex-direction: column;
align-items: center;
justify-content: center;
}
.container--page {
min-height: 100vh;
}
</style>
Здесь происходит много всего. Давайте рассмотрим все с самого начала.
- Мы импортируем необходимые помощники из ‘vue’.
- Мы объявляем интерфейс и тип. Они обеспечат форму реквизитам и утилитам нашего функционального компонента.
- Объявляем реквизиты компонента
- Мы создаем
propClassMap
. Она создает связь между свойствами компонента и классами CSS, к которым они должны применяться. - Объявляем служебную функцию
assembleContainerStyles
. Она будет вызываться один раз при создании компонента и применять необходимые CSS-классы. - Наконец, мы объявляем сами стили CSS под тегом
script
.
Объявление атрибутов и слотов
Добавьте эти строки кода прямо под реквизитами компонента:
const slots = useSlots();
const attrs = useAttrs();
Объявите компонент
Добавьте следующие строки кода прямо под assembleContainerStyles
– функцией:
Внутри тега script
const AppContainer: Component = () => {
return h(props.tag, attrs, slots);
};
Под тегом script
<template>
<app-container :class="assembleContainerStyles()">
<slot />
</app-container>
</template>
Добавьте компонент к родительскому компоненту
Осталось добавить следующий код в файл App.vue
:
<template>
<app-container tag="main" :page="true" :centered="true">
<app-card>
<template v-slot:header> Functional components</template>
<template v-slot:default>
<p>
This card element is a default HTML template while the outer container is rendered
dynamically. You're free to toggle its tag, whether it should fill the whole page or
whether this card is centered.
</p>
</template>
<template v-slot:footer> Card footer </template>
</app-card>
</app-container>
</template>
Опять же, если у вас включено расширение Vue Language Features в VSCode, вы будете рады увидеть, что интеллектуальное автозавершение кода работает и для функциональных компонентов:
Подождите. Но почему шаблон в AppContainer?
Вы можете задаться вопросом: Почему все еще есть шаблон? Разве мы не создаем функциональный компонент?
Это совершенно верно. Вы можете добиться того же результата, используя функциональный синтаксис. Лично я предпочитаю использовать однофайловые компоненты в качестве основы проекта. Но с Vue 3 добиться того же результата с помощью функционального подхода в .ts
– файле стало проще, чем когда-либо прежде.
Обратите внимание, что в официальной документации говорится, что в Vue 3 производительность, полученная при использовании синтаксиса функций, стала пренебрежимо мала. Подробнее об этом можно прочитать здесь.
Если этот ответ вас не удовлетворил, я буду рад увидеть, как вы построили этот компонент с использованием чистого функционального синтаксиса. Пожалуйста, дайте мне знать о ваших выводах и о том, есть ли граничные случаи, в которых синтаксис SFC действительно не может быть использован.