Введение в Vue 3 и Typescript: Функциональные компоненты, атрибуты и слоты

Разобравшись с реактивностью и взаимодействием компонентов, давайте посмотрим, какие основные функции остались. Возьмем слоты и атрибуты. Мы используем их в основном в шаблонах. Но что, если вам нужно получить к ним доступ в скрипте? Скажем, чтобы написать функциональный компонент?

Функциональные компоненты

Официальная документация рекомендует использовать 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>

Вход в полноэкранный режим Выйти из полноэкранного режима

Здесь происходит много всего. Давайте рассмотрим все с самого начала.

  1. Мы импортируем необходимые помощники из ‘vue’.
  2. Мы объявляем интерфейс и тип. Они обеспечат форму реквизитам и утилитам нашего функционального компонента.
  3. Объявляем реквизиты компонента
  4. Мы создаем propClassMap. Она создает связь между свойствами компонента и классами CSS, к которым они должны применяться.
  5. Объявляем служебную функцию assembleContainerStyles. Она будет вызываться один раз при создании компонента и применять необходимые CSS-классы.
  6. Наконец, мы объявляем сами стили 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 действительно не может быть использован.

Оцените статью
Procodings.ru
Добавить комментарий