Использование слотов в Vue.js
Опубликовано: 2022-03-10В недавнем выпуске Vue 2.6 синтаксис использования слотов стал более лаконичным. Это изменение в слотах заставило меня снова заинтересоваться открытием потенциальных возможностей слотов для обеспечения повторного использования, новых функций и более четкой читаемости наших проектов на основе Vue. На что действительно способны слоты?
Если вы новичок в Vue или не видели изменений по сравнению с версией 2.6, читайте дальше. Вероятно, лучшим ресурсом для изучения слотов является собственная документация Vue, но я попытаюсь дать краткое изложение здесь.
Что такое слоты?
Слоты — это механизм для компонентов Vue, который позволяет вам составлять ваши компоненты способом, отличным от строгих отношений родитель-потомок. Слоты дают вам возможность размещать контент в новых местах или делать компоненты более общими. Лучший способ понять их — увидеть их в действии. Начнем с простого примера:
// frame.vue <template> <div class="frame"> <slot></slot> </div> </template>
Этот компонент имеет оболочку div
. Давайте представим, что div
создан для того, чтобы создать стилистическую рамку вокруг своего содержимого. Этот компонент можно использовать в общем, чтобы обернуть фрейм вокруг любого контента, который вы хотите. Давайте посмотрим, как это выглядит при использовании. Компонент frame
здесь относится к компоненту, который мы только что сделали выше.
// app.vue <template> <frame><img src="an-image.jpg"></frame> </template>
Содержимое, находящееся между открывающим и закрывающим тегами frame
, будет вставлено в компонент frame
, где находится slot
, заменив теги slot
. Это самый простой способ сделать это. Вы также можете указать содержимое по умолчанию для размещения в слоте, просто заполнив его:
// frame.vue <template> <div class="frame"> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
Итак, теперь, если мы используем его вот так:
// app.vue <template> <frame /> </template>
Появится текст по умолчанию «Это содержимое по умолчанию, если ничего не указано для перехода сюда», но если мы будем использовать его, как и раньше, текст по умолчанию будет переопределен тегом img
.
Несколько/именных слотов
Вы можете добавить несколько слотов к компоненту, но если вы это сделаете, все они, кроме одного, должны иметь имя. Если есть один без имени, это слот по умолчанию. Вот как вы создаете несколько слотов:
// titled-frame.vue <template> <div class="frame"> <header><h2><slot name="header">Title</slot></h2></header> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
Мы сохранили тот же слот по умолчанию, но на этот раз мы добавили слот с именем header
где вы можете ввести заголовок. Вы используете это так:
// app.vue <template> <titled-frame> <template v-slot:header> <!-- The code below goes into the header slot --> My Image's Title </template> <!-- The code below goes into the default slot --> <img src="an-image.jpg"> </titled-frame> </template>
Как и раньше, если мы хотим добавить контент в слот по умолчанию, просто поместите его непосредственно внутрь компонента titled-frame
. Однако, чтобы добавить контент в именованный слот, нам нужно было поместить код в тег template
с директивой v-slot
. Вы добавляете двоеточие ( :
) после v-slot
а затем пишете имя слота, в который хотите передать контент. Обратите внимание, что v-slot
является новым для Vue 2.6, поэтому, если вы используете более старую версию, вам необходимо прочитать документы об устаревшем синтаксисе слотов.
Слоты с ограниченной областью действия
Еще одна вещь, которую вам нужно знать, это то, что слоты могут передавать данные/функции своим дочерним элементам. Чтобы продемонстрировать это, нам понадобится совершенно другой пример компонента со слотами, еще более надуманный, чем предыдущий: давайте как бы скопируем пример из документации, создав компонент, который передает данные о текущем пользователе в свои слоты:
// current-user.vue <template> <span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span> </template> <script> export default { data () { return { user: ... } } } </script>
Этот компонент имеет свойство user
с подробной информацией о пользователе. По умолчанию компонент показывает фамилию пользователя, но обратите внимание, что он использует v-bind
для привязки пользовательских данных к слоту. При этом мы можем использовать этот компонент для предоставления пользовательских данных его потомку:
// app.vue <template> <current-user> <template v-slot:default="slotProps">{{ slotProps.user.firstName }}</template> </current-user> </template>
Чтобы получить доступ к данным, передаваемым в слот, мы указываем имя переменной области видимости со значением директивы v-slot
.
Здесь нужно сделать несколько замечаний:
- Мы указали имя
default
, хотя для слота по умолчанию это не требуется. Вместо этого мы могли бы просто использоватьv-slot="slotProps"
. - Вам не нужно использовать
slotProps
в качестве имени. Вы можете называть это как хотите. - Если вы используете только слот по умолчанию, вы можете пропустить этот внутренний тег
template
и поместить директивуv-slot
непосредственно в тегcurrent-user
. - Вы можете использовать деструктурирование объекта для создания прямых ссылок на данные слота с заданной областью вместо использования одного имени переменной. Другими словами, вы можете использовать
v-slot="{user}"
вместоv-slot="slotProps"
и тогда вы можете использоватьuser
напрямую вместоslotProps.user
.
Принимая во внимание эти примечания, приведенный выше пример можно переписать следующим образом:
// app.vue <template> <current-user v-slot="{user}"> {{ user.firstName }} </current-user> </template>
Еще пара вещей, которые нужно иметь в виду:
- Вы можете связать более одного значения с директивами
v-bind
. Так что в этом примере я мог бы сделать больше, чем простоuser
. - Вы также можете передавать функции в слоты с ограниченной областью действия. Многие библиотеки используют это для предоставления повторно используемых функциональных компонентов, как вы увидите позже.
-
v-slot
имеет псевдоним#
. Поэтому вместоv-slot:header="data"
вы можете написать#header="data"
. Вы также можете просто указать#header
вместоv-slot:header
если вы не используете слоты с ограниченной областью действия. Что касается слотов по умолчанию, вам нужно указать имя поdefault
при использовании псевдонима. Другими словами, вам нужно будет написать#default="data"
вместо#="data"
.
Есть еще несколько мелких моментов, о которых вы можете узнать из документации, но этого должно быть достаточно, чтобы помочь вам понять, о чем мы говорим в оставшейся части этой статьи.
Что вы можете делать со слотами?
Слоты не были созданы для одной цели, или, по крайней мере, если бы они были созданы, они развились далеко за пределы первоначального намерения стать мощным инструментом для выполнения множества разных задач.
Многоразовые шаблоны
Компоненты всегда проектировались с возможностью повторного использования, но некоторые шаблоны нецелесообразно применять с помощью одного «нормального» компонента, потому что количество props
, которые вам понадобятся для его настройки, может быть чрезмерным или вам придется передавать большие разделы контента и, возможно, другие компоненты через props
. Слоты можно использовать, чтобы охватить «внешнюю» часть шаблона и позволить другим HTML и/или компонентам размещаться внутри них для настройки «внутренней» части, позволяя компоненту со слотами определять шаблон и компоненты, внедряемые в шаблон. слоты должны быть уникальными.
Для нашего первого примера давайте начнем с чего-то простого: кнопки. Представьте, что вы и ваша команда используете Bootstrap*. В Bootstrap ваши кнопки часто связаны с базовым классом `btn` и классом, определяющим цвет, например, `btn-primary`. Вы также можете добавить класс размера, например `btn-lg`.
* Я не поощряю и не отговариваю вас от этого, мне просто нужно было кое-что для моего примера, и это довольно хорошо известно.
Теперь предположим, для простоты, что ваше приложение/сайт всегда использует btn-primary
и btn-lg
. Вы не хотите всегда писать все три класса на своих кнопках, или, может быть, вы не доверяете новичку, чтобы он не забыл сделать все три. В этом случае вы можете создать компонент, который автоматически имеет все три этих класса, но как разрешить настройку содержимого? prop
нецелесообразно, потому что тег button
может содержать все виды HTML, поэтому мы должны использовать слот.
<!-- my-button.vue --> <template> <button class="btn btn-primary btn-lg"> <slot>Click Me!</slot> </button> </template>
Теперь мы можем использовать его везде с любым содержимым:
<!-- somewhere else, using my-button.vue --> <template> <my-button> <img src="/img/awesome-icon.jpg"> SMASH THIS BUTTON TO BECOME AWESOME FOR ONLY $500!!! </my-button> </template>
Конечно, вы можете пойти с чем-то намного большим, чем кнопка. Придерживаясь Bootstrap, давайте посмотрим на модальную часть или, по крайней мере, на HTML-часть; Я не буду вдаваться в функциональность… пока.
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <slot name="footer"></slot> </div> </div> </div> </div> </template>
Теперь воспользуемся этим:
<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <template #footer> <em>Now back to your regularly scheduled app usage</em> </template> </my-modal> </template>
Вышеупомянутый вариант использования слотов, безусловно, очень полезен, но он может сделать еще больше.
Повторное использование функциональности
Компоненты Vue — это не только HTML и CSS. Они созданы с помощью JavaScript, поэтому они также касаются функциональности. Слоты могут быть полезны для создания функциональности один раз и использования ее в нескольких местах. Вернемся к нашему модальному примеру и добавим функцию, закрывающую модальное окно:
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <!-- using `v-bind` shorthand to pass the `closeModal` method to the component that will be in this slot --> <slot name="footer" :closeModal="closeModal"></slot> </div> </div> </div> </div> </template> <script> export default { //... methods: { closeModal () { // Do what needs to be done to close the modal... and maybe remove it from the DOM } } } </script>
Теперь, когда вы используете этот компонент, вы можете добавить кнопку в нижний колонтитул, которая может закрыть модальное окно. Обычно в случае модального окна Bootstrap вы можете просто добавить data-dismiss="modal"
к кнопке, но мы хотим скрыть определенные вещи Bootstrap от компонентов, которые будут вставляться в этот модальный компонент. Итак, мы передаем им функцию, которую они могут вызвать, и они ничего не знают об участии Bootstrap:
<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <!-- pull in `closeModal` and use it in a button's click handler --> <template #footer="{closeModal}"> <button @click="closeModal"> Take me back to the app so I can be awesome </button> </template> </my-modal> </template>
Компоненты без рендеринга
И, наконец, вы можете использовать то, что знаете об использовании слотов, для повторного использования функций, удалить практически весь HTML и просто использовать слоты. По сути, это то, чем является компонент без рендеринга: компонент, который предоставляет только функциональность без какого-либо HTML.
Сделать компоненты действительно без рендеринга может быть немного сложно, потому что вам нужно будет написать функции render
, а не использовать шаблон, чтобы устранить необходимость в корневом элементе, но это не всегда может быть необходимо. Однако давайте рассмотрим простой пример, который позволяет нам сначала использовать шаблон:
<template> <transition name="fade" v-bind="$attrs" v-on="$listeners"> <slot></slot> </transition> </template> <style> .fade-enter-active, .fade-leave-active { transition: opacity 0.3s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
Это странный пример компонента без рендеринга, потому что в нем даже нет JavaScript. В основном это потому, что мы просто создаем предварительно настроенную повторно используемую версию встроенной функции без рендеринга: transition
.
Да, Vue имеет встроенные компоненты без рендеринга. Этот конкретный пример взят из статьи Cristi Jora о повторно используемых переходах и показывает простой способ создания компонента без рендеринга, который может стандартизировать переходы, используемые в вашем приложении. Статья Кристи более глубока и показывает некоторые более продвинутые варианты многоразовых переходов, поэтому я рекомендую ознакомиться с ней.
Для другого нашего примера мы создадим компонент, который обрабатывает переключение того, что отображается в разных состояниях промиса: ожидание, успешное разрешение и сбой. Это распространенный шаблон, и хотя он не требует большого количества кода, он может запутать многие ваши компоненты, если логика не будет извлечена для повторного использования.
<!-- promised.vue --> <template> <span> <slot name="rejected" v-if="error" :error="error"></slot> <slot name="resolved" v-else-if="resolved" :data="data"></slot> <slot name="pending" v-else></slot> </span> </template> <script> export default { props: { promise: Promise }, data: () => ({ resolved: false, data: null, error: null }), watch: { promise: { handler (promise) { this.resolved = false this.error = null if (!promise) { this.data = null return } promise.then(data => { this.data = data this.resolved = true }) .catch(err => { this.error = err this.resolved = true }) }, immediate: true } } } </script>
и так, что здесь происходит? Во-первых, обратите внимание, что мы получаем промис под названием promise
, который является Promise
. В секции watch
мы наблюдаем за изменениями обещания, и когда оно изменяется (или сразу при создании компонента благодаря свойству immediate
), мы очищаем состояние, then
вызываем и catch
обещание, обновляя состояние, когда оно либо завершается успешно, либо терпит неудачу.
Затем в шаблоне мы показываем другой слот в зависимости от состояния. Обратите внимание, что нам не удалось сделать его действительно без рендеринга, потому что нам нужен был корневой элемент, чтобы использовать шаблон. Мы также передаем data
и error
в соответствующие области слотов.
И вот пример его использования:
<template> <div> <promised :promise="somePromise"> <template #resolved="{ data }"> Resolved: {{ data }} </template> <template #rejected="{ error }"> Rejected: {{ error }} </template> <template #pending> Working on it... </template> </promised> </div> </template> ...
Мы somePromise
компоненту без рендеринга. Пока мы ждем его завершения, мы показываем «Работаем над этим…» благодаря pending
слоту. Если это удается, мы отображаем «Resolved:» и значение разрешения. Если это не удается, мы отображаем «Отклонено:» и ошибку, вызвавшую отклонение. Теперь нам больше не нужно отслеживать состояние промиса в этом компоненте, потому что эта часть вынесена в свой собственный повторно используемый компонент.
Итак, что мы можем сделать с этим span
, охватывающим слоты в promised.vue
? Чтобы удалить его, нам нужно удалить часть template
и добавить функцию render
в наш компонент:
render () { if (this.error) { return this.$scopedSlots['rejected']({error: this.error}) } if (this.resolved) { return this.$scopedSlots['resolved']({data: this.data}) } return this.$scopedSlots['pending']() }
Здесь нет ничего слишком сложного. Мы просто используем некоторые блоки if
для нахождения состояния, а затем возвращаем правильный слот с областью действия (через this.$scopedSlots['SLOTNAME'](...)
) и передаем соответствующие данные в область действия слота. Если вы не используете шаблон, вы можете не использовать расширение файла .vue
, вытащив код JavaScript из тега script
и просто вставив его в файл .js
. Это должно дать вам очень небольшой прирост производительности при компиляции этих файлов Vue.
Этот пример представляет собой урезанную и слегка измененную версию vue-promised, которую я бы рекомендовал использовать вместо приведенного выше примера, потому что он покрывает некоторые потенциальные ловушки. Есть много других замечательных примеров компонентов без рендеринга. Baleada — это целая библиотека, полная компонентов без рендеринга, которые предоставляют полезные функции, подобные этой. Также есть vue-virtual-scroller для управления рендерингом элемента списка на основе того, что видно на экране, или PortalVue для «телепортации» контента в совершенно разные части DOM.
Я выхожу
Слоты Vue выводят разработку на основе компонентов на совершенно новый уровень, и, хотя я продемонстрировал множество отличных способов использования слотов, существует бесчисленное множество других. Какую великую идею вы можете придумать? Как вы думаете, как слоты могут получить обновление? Если они у вас есть, обязательно поделитесь своими идеями с командой Vue. Да благословит Бог и счастливого кодирования.