В этой статье я хотел бы познакомить вас с компонентной моделью Vue.js и показать преимущества повторного использования и инкапсуляции компонентов.
Эта статья изначально была опубликована в моем блоге.
Если вы еще не разбираетесь в Vue.js, я настоятельно рекомендую официальное Руководство по Vue.js. Но, пожалуйста, не бойтесь сразу погрузиться в эту статью вместе со мной. Я укажу вам на соответствующую официальную документацию, чтобы дать вам дополнительную информацию и возможность прочитать дополнительные справочные материалы.
Мы начнем с простого примера компонента и постепенно улучшим его функциональность.
HTML-код этого компонента карточки состоит из большой области изображения и тела с некоторым текстом:
<div id="demo">
<div class="image-card">
<img class="image-card__image" src="cat.jpg" />
<div class="image-card__body">
<h3 class="image-card__title">Striped Tiger Cat</h3>
<div class="image-card__author">Image by @lemepe</div>
</div>
</div>
</div>
Мы используем корневой HTML-элемент с demo
id в качестве нашего элемента для запуска Vue:
new Vue({ el: '#demo' })
Чего мы добились? Мы использовали Vue.js для рендеринга этой графической карты. Но мы не можем повторно использовать этот код как есть, и мы не хотим копировать и вставлять и тем самым дублировать код.
Решение нашей проблемы - превратить это в компонент.
Компоненты можно использовать повторно
Итак, давайте отделим карточку изображения от оставшегося приложения Vue.js.
Сначала мы вводим элемент шаблона со всем содержимым карточки изображения:
<template id="template-image-card">
<div class="image-card">
<img class="image-card__image" src="cat.jpg" />
<div class="image-card__body">
<h3>Striped Tiger Cat</h3>
<div class="image-card__author">Image by @lemepe</div>
</div>
</div>
</template>
И мы определяем компонент с помощью Vue.component
и ссылаемся на наш идентификатор шаблона template-image-card
:
Vue.component('image-card', {
template: "#template-image-card"
})
Это снова завернуто в корневой элемент HTML:
<div id="demo">
<image-card></image-card>
<image-card></image-card>
</div>
И вуаля! У нас есть две кошки :-)
Очевидно, что две кошки лучше, чем одна кошка, и мы показали, что у нас может быть несколько экземпляров нашего image-card
компонента на одной странице.
Теперь у нас есть средства для повторного использования этого компонента в нашем приложении. И если задуматься, то на самом деле весьма примечательно, что он включает в себя наш код HTML, CSS и Javascript, заключенный в один компонент.
Но все же этот компонент не очень полезен, не правда ли? Это просто недостаточно гибко! Было бы здорово, если бы мы могли изменить изображение и текст для каждого компонента.
Передача данных дочерним компонентам в качестве свойств
Чтобы настроить поведение компонента, мы будем использовать props.
Начнем с того, как мы хотим использовать наш компонент:
<div id="demo">
<image-card image-src="cat1.jpg" heading="Striped Tiger Cat" text="Image by @lemepe"></image-card>
<image-card image-src="cat2.jpg" heading="Alternative Text" text="alternative subtitle"></image-card>
</div>
Мы представляем три новых реквизита image-src
, heading
и text
. При использовании компонента они будут переданы как атрибуты HTML.
Далее следует prop
определение нашего компонента:
Vue.component('image-card', {
template: "#template-image-card",
props: {
heading: String,
text: String,
imageSrc: String
}
});
Обратите внимание, как опора imageSrc
написана в camelCase, тогда как в атрибутах HTML используется тире image-src
. Вы можете узнать больше о props
в официальном Руководстве по Vue.js.
И сопутствующий шаблон снова использует этот реквизит в формате camelCase:
<template id="template-image-card">
<div class="image-card">
<img class="image-card__image" :src="imageSrc" />
<div class="image-card__body">
<h3>{{heading}}</h3>
<div class="image-card__author">{{text}}</div>
</div>
</div>
</template>
Посмотрим на результат!
Это сработало! Мы использовали два экземпляра нашего image-card
компонента с разными реквизитами.
Разве не приятно, что мы можем визуализировать компонент по-другому, используя реквизиты в качестве входных данных?
Компоненты имеют состояние
На моей обычной повседневной работе менеджер по продукту, скорее всего, заметил бы, что image-card
сам по себе выглядит довольно хорошо с кошками и прочими вещами. Но пока это не очень интересно. Как насчет того, чтобы позволить пользователям лайкать нашу кошку и затем подсчитывать, какая кошка понравилась больше всего?
Компоненты могут иметь состояние с помощью атрибута data
:
Vue.component('image-card', {
template: "#template-image-card",
props: {
heading: String,
text: String,
imageSrc: String
},
data: function () {
return {
count: 0
}
}
});
Обратите внимание, что data
возвращает функцию, а не только объект Javascript data: { count: 0 }
. Это необходимо, чтобы каждый экземпляр компонента мог поддерживать независимую копию возвращаемых данных. Подробнее об этом читайте в Руководстве по Vue.js.
В нашем шаблоне используется этот счетчик:
<template id="template-image-card">
<div class="image-card">
<img class="image-card__image" :src="imageSrc" />
<div class="image-card__body">
<h3 class="image-card__heading">{{heading}}</h3>
<div class="image-card__author">{{text}}</div>
<button class="image-card__heart" @click="count++">
<svg viewBox="0 0 32 29.6">
<path d="M16,28.261c0,0-14-7.926-14-17.046c0-9.356,13.159-10.399,14-0.454c1.011-9.938,14-8.903,14,0.454 C30,20.335,16,28.261,16,28.261z"/>
</svg>
</button>
<div class="image-card__count" v-if="count > 0">{{count}}</div>
</div>
</div>
</template>
Мы используем элемент SVG для рендеринга сердечка, а в событии click
мы увеличиваем счетчик на 1. Рядом с сердечком отображается небольшое количество с текущим значением count
.
Если вас больше интересует работа с SVG, загляните в Поваренную книгу Vue.js для получения дополнительной информации.
Обратите внимание, что каждый экземпляр компонента имеет собственное локальное состояние count
, которое может быть изменено независимо от count
другого компонента.
В то время как в предыдущем примере мы только инкапсулировали HTML-код и сделали его более гибким с помощью props. Теперь мы также инкапсулируем некоторую бизнес-логику, чтобы вести счет.
В то время как props
являются входными параметрами нашего компонента, состояние является чем-то внутренним по отношению к компоненту и скрыто от пользователя кода нашего компонента. Мы могли бы изменить имя нашей переменной с count
на clickCount
, и пользователю нашего компонента даже не нужно было бы знать. Это замечательно, потому что мы можем продолжать улучшать наш компонент, не нарушая пользовательский код.
Отправка сообщений родителям с событиями
Теперь, когда мы знаем, как передавать данные потомкам и как инкапсулировать состояние. Не хватает одного: как мы можем получить данные от ребенка?
В Vue.js мы можем отправить настраиваемое событие от компонента к его родительскому элементу, который прослушивает это конкретное событие. Это событие может дополнительно передавать данные.
В нашем примере мы можем использовать $emit
для отправки события с именем change
с данными родителю:
Vue.component('image-card', {
template: "#template-image-card",
props: {
heading: String,
text: String,
imageSrc: String
},
data: function () {
return {
count: 0
}
},
methods: {
handleClick() {
this.count++;
this.$emit("change", this.count);
}
}
});
Мы определили метод handleClick
, который не только увеличивает наше count
состояние, но дополнительно использует $emit
для отправки сообщения нашему родителю. handleClick
вызывается в click
событии нашего сердца:
<template id="template-image-card">
<div class="image-card">
<img class="image-card__image" :src="imageSrc" />
<div class="image-card__body">
<h3 class="image-card__heading">{{heading}}</h3>
<div class="image-card__author">{{text}}</div>
<button class="image-card__heart" @click="handleClick">
<svg viewBox="0 0 32 29.6">
<path d="M16,28.261c0,0-14-7.926-14-17.046c0-9.356,13.159-10.399,14-0.454c1.011-9.938,14-8.903,14,0.454 C30,20.335,16,28.261,16,28.261z"/>
</svg>
</button>
<div class="image-card__count" v-if="count > 0">{{count}}</div>
</div>
</div>
</template>
Теперь родительский шаблон может использовать это для прослушивания события change
для увеличения totalCount
:
<div id="demo">
<image-card image-src="cat.jpg" heading="Striped Tiger Cat" text="Image by @lemepe" @change="handleChange"></image-card>
<image-card image-src="cat.jpg" heading="Alternative Text" text="alternative subtitle" @change="handleChange"></image-card>
<p>Total Count: {{totalCount}}</p>
</div>
Вместе с экземпляром Vue.js для отслеживания totalCount
:
new Vue({
el: '#demo',
data: {
totalCount: 0
},
methods: {
handleChange(count) {
console.log("count changed", count);
this.totalCount++;
}
}
});
Обратите внимание, что родитель ничего не знает о внутреннем устройстве компонента. Он просто знает, что доступно событие изменения и что сообщение отправляет count
компонента.
Событие, генерируемое через this.$emit("event")
, отправляется только родительскому компоненту. Он не будет всплывать в иерархии компонентов, как это происходит с собственными событиями DOM.
Резюме
В этой статье мы исследовали базовые концепции компонентной модели. Мы обсудили повторное использование и инкапсуляцию компонентов, как использовать реквизиты для передачи данных потомкам и как генерировать события для передачи сообщений родителю.
Если вам понравился этот пост, также ознакомьтесь с моим новым курсом Vue.js Component Patterns Course.