[Перевод] Vue.js для начинающих, урок 11: вкладки, глобальная шина событий
Сегодня, в 11 уроке, который завершает этот учебный курс по основам Vue, мы поговорим о том, как организовать содержимое страницы приложения с помощью вкладок. Здесь же мы обсудим глобальную шину событий — простой механизм по передаче данных внутри приложения.
→ Vue.js для начинающих, урок 1: экземпляр Vue
→ Vue.js для начинающих, урок 2: привязка атрибутов
→ Vue.js для начинающих, урок 3: условный рендеринг
→ Vue.js для начинающих, урок 4: рендеринг списков
→ Vue.js для начинающих, урок 5: обработка событий
→ Vue.js для начинающих, урок 6: привязка классов и стилей
→ Vue.js для начинающих, урок 7: вычисляемые свойства
→ Vue.js для начинающих, урок 8: компоненты
→ Vue.js для начинающих, урок 9: пользовательские события
→ Vue.js для начинающих, урок 10: формы
Цель урока
Мы хотим, чтобы на странице приложения присутствовали бы вкладки, одна из которых позволяет посетителям писать отзывы о товарах, а другая — просматривать существующие отзывы.
Начальный вариант кода
Вот как на данном этапе работы выглядит содержимое файла index.html
:
Cart({{ cart.length }})
В main.js
имеется следующий код:
Vue.component('product', {
props: {
premium: {
type: Boolean,
required: true
}
},
template: `
{{ title }}
In stock
Out of Stock
Shipping: {{ shipping }}
- {{ detail }}
Reviews
There are no reviews yet.
-
{{ review.name }}
Rating: {{ review.rating }}
{{ review.review }}
`,
data() {
return {
product: 'Socks',
brand: 'Vue Mastery',
selectedVariant: 0,
details: ['80% cotton', '20% polyester', 'Gender-neutral'],
variants: [
{
variantId: 2234,
variantColor: 'green',
variantImage: './assets/vmSocks-green.jpg',
variantQuantity: 10
},
{
variantId: 2235,
variantColor: 'blue',
variantImage: './assets/vmSocks-blue.jpg',
variantQuantity: 0
}
],
reviews: []
}
},
methods: {
addToCart() {
this.$emit('add-to-cart', this.variants[this.selectedVariant].variantId);
},
updateProduct(index) {
this.selectedVariant = index;
},
addReview(productReview) {
this.reviews.push(productReview)
}
},
computed: {
title() {
return this.brand + ' ' + this.product;
},
image() {
return this.variants[this.selectedVariant].variantImage;
},
inStock() {
return this.variants[this.selectedVariant].variantQuantity;
},
shipping() {
if (this.premium) {
return "Free";
} else {
return 2.99
}
}
}
})
Vue.component('product-review', {
template: `
`,
data() {
return {
name: null,
review: null,
rating: null,
errors: []
}
},
methods: {
onSubmit() {
if(this.name && this.review && this.rating) {
let productReview = {
name: this.name,
review: this.review,
rating: this.rating
}
this.$emit('review-submitted', productReview)
this.name = null
this.review = null
this.rating = null
} else {
if(!this.name) this.errors.push("Name required.")
if(!this.review) this.errors.push("Review required.")
if(!this.rating) this.errors.push("Rating required.")
}
}
}
})
var app = new Vue({
el: '#app',
data: {
premium: true,
cart: []
},
methods: {
updateCart(id) {
this.cart.push(id);
}
}
})
Вот как сейчас выглядит приложение.
Страница приложения
Задача
Сейчас отзывы и форма, которая используется для отправки отзывов, выводятся на странице рядом друг с другом. Это — вполне рабочая структура. Но ожидается, что со временем на странице будет появляться всё больше и больше отзывов. Это значит, что пользователям будет удобнее взаимодействовать со страницей, на которой, по их выбору, выводится либо форма, либо список отзывов.
Решение задачи
Для того чтобы решить нашу задачу, мы можем добавить на страницу систему вкладок. Одна из них, с заголовком Reviews
, будет выводить отзывы. Вторая, с заголовком Make a Review
, будет выводить форму для отправки отзывов.
Создание компонента, реализующего систему вкладок
Начнём работу с создания компонента product-tabs
. Он будет выводиться в нижней части визуального представления компонента product
. Со временем он заменит собой тот код, который сейчас используется для вывода на странице списка отзывов и формы.
Vue.component('product-tabs', {
template: `
{{ tab }}
`,
data() {
return {
tabs: ['Reviews', 'Make a Review']
}
}
})
Сейчас это — лишь заготовка компонента, которую мы скоро доработаем. Пока же в двух словах обсудим то, что представлено в этом коде.
В данных компонента имеется массив tabs
, содержащий строки, которые мы используем в качестве заголовков вкладок. В шаблоне компонента применяется конструкция v-for
, с помощью которой для каждого элемента массива tabs
создаётся элемент , содержащий соответствующую строку. То, что формирует этот компонент на данном этапе работы над ним, будет выглядеть так, как показано ниже.
Компонент product-tabs на начальном этапе работы над ним
Нам, для достижения наших целей, нужно знать о том, какая из вкладок является активной. Поэтому добавим в данные компонента свойство selectedTab
. Будем динамически задавать значение этого свойства, пользуясь обработчиком событий, реагирующим на щелчки по заголовкам вкладок:
@click="selectedTab = tab"
В свойство будут записываться строки, соответствующие заголовкам вкладок.
То есть, если пользователь щёлкнет по вкладке Reviews
, то в selectedTab
будет записана строка Reviews
. Если будет сделан щелчок по вкладке Make a Review
, то в selectedTab
попадёт строка Make a Review
.
Вот как теперь будет выглядеть полный код компонента.
Vue.component('product-tabs', {
template: `
{{ tab }}
`,
data() {
return {
tabs: ['Reviews', 'Make a Review'],
selectedTab: 'Reviews' // устанавливается с помощью @click
}
}
})
Привязка класса к активной вкладке
Пользователь, работая с интерфейсом, в котором используются вкладки, должен знать о том, какая вкладка является активной. Реализовать подобный механизм можно, воспользовавшись привязкой классов к элементам , использующимся для вывода названий вкладок:
:class="{ activeTab: selectedTab === tab }"
Вот CSS-файл, в котором определён стиль использованного здесь класса activeTab
. Вот как выглядит этот стиль:
.activeTab {
color: #16C0B0;
text-decoration: underline;
}
А вот — стиль класса tab
:
.tab {
margin-left: 20px;
cursor: pointer;
}
Если объяснить вышеприведённую конструкцию простым языком, то оказывается, что к вкладке применяется стиль, заданный для класса activeTab
, в том случае, когда selectedTab
равняется tab
. Так как в selectedTab
записывается название вкладки, по которой только что щёлкнул пользователь, стиль .activeTab
будет применяться именно к активной вкладке.
Другими словами, когда пользователь щёлкнет по первой вкладке, в tab
будет находиться Reviews
, то же самое будет записано и в selectedTab
. В результате к первой вкладке будет применён стиль .activeTab
.
Теперь заголовки вкладок на странице будут выглядеть так, как показано ниже.
Выделенный заголовок активной вкладки
Судя по всему, на данном этапе всё работает так, как ожидается, поэтому мы можем идти дальше.
Работа над шаблоном компонента
Теперь, когда мы можем сообщить пользователю о том, какая именно вкладка является активной, можно продолжить работу над компонентом. А именно, речь идёт о доработке его шаблона, об описании того, что именно будет выводиться на странице при активации каждой из вкладок.
Подумаем о том, что надо показать пользователю в том случае, если он щёлкнет по вкладке Reviews
. Это, понятно, отзывы о товаре. Поэтому переместим код вывода отзывов из шаблона компонента product
в шаблон компонента product-tabs
, разместив этот код ниже конструкции, используемой для вывода заголовков вкладок. Вот как теперь будет выглядеть шаблон компонента product-tabs
:
template: `
{{ tab }}
There are no reviews yet.
-
{{ review.name }}
Rating: {{ review.rating }}
{{ review.review }}
`
Обратите внимание на то, что мы избавились от тега , так как нам больше не нужно выводить заголовок
Reviews
над списком отзывов. Вместо этого заголовка будет выводиться заголовок соответствующей вкладки.
Но одного только переноса кода шаблона недостаточно для того чтобы обеспечить вывод отзывов. Массив reviews
, данные которого используются для вывода отзывов, хранится в составе данных компонента product
. Нам нужно передать этот массив в компонент product-tabs
, используя механизм входных параметров компонента. Добавим в объект с опциями, используемый при создании product-tabs
, следующее:
props: {
reviews: {
type: Array,
required: false
}
}
Передадим массив reviews
из компонента product
в компонент product-tabs
, воспользовавшись, в шаблоне product
, следующей конструкцией:
При этом щелчки по заголовкам, хотя и приводят к их выделению, никак не влияют на другие элементы страницы. Далее, если попытаться воспользоваться формой, окажется, что она перестала нормально работать. Всё это — вполне ожидаемые следствия изменений, внесённых нами в приложение. Продолжим работу и приведём наш проект в работоспособное состояние. В данных компонента уже есть свойство Так, к тегу Аналогично, к тегу Вот как теперь будет выглядеть шаблон компонента There are no reviews yet. {{ review.name }} Rating: {{ review.rating }} {{ review.review }} Но отправка отзывов с помощью формы всё так же не работает. Исследуем проблему и исправим её. Очевидно, система не может обнаружить метод Для ответа на этот вопрос вспомним о том, что Наш код сейчас рассчитан на взаимодействие компонента Глобальная шина событий — это канал связи, который можно использовать для передачи информации между компонентами. И это, на самом деле, просто экземпляр Vue, который создают, не передавая ему объект с опциями. Создадим шину событий: Возможно, вам будет легче освоить эту концепцию, если вы представите шину событий в виде автобуса. Его пассажирами являются данные, которые одни компоненты отправляют другим. В нашем случае речь идёт о передаче одним компонентам сведений о событиях, сгенерированных другими компонентами. То есть, наш «автобус» будет ездить от компонента Сейчас в компоненте Этот код можно переписать и без использования стрелочных функций, но тогда понадобится организовать привязку Если теперь попробовать оставить отзыв о товаре, воспользовавшись формой, окажется, что этот отзыв выводится там, где он должен быть. По мере того, как приложение растёт, в нём может очень пригодиться система управления состоянием, основанная на Vuex. Это — паттерн и библиотека управления состоянием приложений. Если вы только что завершили этот курс — просим поделиться впечатлениями.
А теперь поразмыслим о том, что нужно вывести на странице в том случае, если пользователь щёлкнет по заголовку вкладки Make a Review
. Это, конечно, форма для отправки отзывов. Для того чтобы подготовить проект к дальнейшей работе над ним — перенесём код подключения компонента product-review
из шаблона компонента product
в шаблон product-tabs
. Разместим следующий код ниже элемента
Если взглянуть сейчас на страницу приложения, то окажется, что список отзывов и форма выводятся на ней ниже заголовков вкладок.
Промежуточный этап работы над страницейВывод элементов страницы по условию
Теперь, когда мы подготовили основные элементы шаблона компонента product-tabs
, пришло время создать систему, которая позволит выводить разные элементы страницы основываясь на том, по заголовку какой именно вкладки щёлкнул пользователь.selectedTab
. Мы можем воспользоваться им в директиве v-show
для организации условного рендеринга того, что относится к каждой из вкладок.v-show="selectedTab === 'Reviews'"
Благодаря ей список отзывов будет выводиться тогда, когда будет активной вкладка Reviews
.product-review
, мы добавим следующее:
v-show="selectedTab === 'Make a Review'"
Это приведёт к тому, что форма будет выводиться только тогда, когда активна вкладка Make a Review
.product-tabs
: template: `
{{ tab }}
Если взглянуть на страницу и пощёлкать по вкладкам — можно убедиться в том, что созданный нами механизм работает правильно.
Щелчки по вкладкам приводят к скрытию одних элементов и к отображению другихРешение проблемы с отправкой отзывов
Если сейчас взглянуть в консоль инструментов разработчика браузера, там можно обнаружить предупреждение.
Предупреждение в консолиaddReview
. Что с ним случилось? addReview
— это метод, который объявлен в компоненте product
. Он должен вызываться в том случае, если компонент product-review
(а это — дочерний компонент компонента product
) генерирует событие review-submitted
:
Именно так всё и работало до переноса вышеприведённого фрагмента кода в компонент product-tabs
. А теперь дочерним компонентом product
является компонент product-tabs
, а product-review
— это теперь не «ребёнок», компонента product
, а его «внук».product-review
с родительским компонентом. Но теперь это — уже не компонент product
. В результате оказывается, что нам, чтобы форма заработала бы правильно, нужно подвергнуть код проекта рефакторингу.Рефакторинг кода проекта
Для того чтобы обеспечить связь внучатых компонентов с их «бабушками» и «дедушками», или для того, чтобы наладить связь между компонентами одного уровня, нередко используют механизм, называемый глобальной шиной событий (global event bus).var eventBus = new Vue()
Этот код попадёт на верхний уровень файла main.js
.product-review
к компоненту product
, перевозя сведения о том, что форма был отправлена, и доставляя данные формы из product-review
в product
.product-review
, в методе onSubmit
, есть такая строчка: this.$emit('review-submitted', productReview)
Заменим её на следующую, воспользовавшись eventBus
вместо this
: eventBus.$emit('review-submitted', productReview)
После этого больше не нужно прослушивать событие review-submitted
компонента product-review
. Поэтому изменим код этого компонента в шаблоне компонента product-tabs
на такой:
Из компонента product
теперь можно удалить метод addReview
. Вместо него мы воспользуемся следующей конструкцией: eventBus.$on('review-submitted', productReview => {
this.reviews.push(productReview)
})
О том, как именно применить её в компоненте, мы поговорим ниже, а пока в двух словах опишем то, что в ней происходит. Эта конструкция указывает на то, что когда eventBus
генерирует событие review-submitted
, нужно взять данные, передаваемые в этом событии (то есть — productReview
) и поместить их в массив reviews
компонента product
. Собственно говоря, это очень похоже на то, что до сих пор делалось в методе addReview
, который нам больше не нужен. Обратите внимание на то, что в вышеприведённом фрагменте кода используется стрелочная функция. Этот момент достоин более подробного освещения.Причины использования стрелочной функции
Здесь мы используем синтаксис стрелочных функций, который появился в ES6. Дело в том, что контекст стрелочной функции привязан к родительскому контексту. То есть — когда мы, внутри этой функции, пользуемся ключевым словом this
, оно равнозначно тому ключевому слову this
, которое соответствует сущности, содержащей стрелочную функцию.this
: eventBus.$on('review-submitted', function (productReview) {
this.reviews.push(productReview)
}.bind(this))
Завершение работы над проектом
Мы почти достигли цели. Всё, что осталось сделать — найти место для фрагмента кода, обеспечивающего реакцию на событие review-submitted
. Таким местом в компоненте product
может стать функция mounted
: mounted() {
eventBus.$on('review-submitted', productReview => {
this.reviews.push(productReview)
})
}
Что это за функция? Это — хук жизненного цикла, который вызывается один раз после того, как компонент будет смонтирован в DOM. Теперь, после того, как компонент product
будет смонтирован, он будет ожидать появления событий review-submitted
. После того, как такое событие будет сгенерировано, в данные компонента будет добавлено то, что передано в этом событии, то есть — productReview
.
Форма работает так, как нужноШина событий — это не лучшее решение для обеспечения связи компонентов
Хотя шина событий используется часто, и хотя вы можете встретить её в различных проектах, учитывайте то, что это — далеко не самое лучшее решение задачи обеспечения связи компонентов приложений.Практикум
Добавьте в проект вкладки Shipping
и Details
, на которых, соответственно, выводится стоимость доставки покупок и сведения о товарах.Итоги
Вот что вы узнали, изучив этот урок:
Надеемся, что вы, изучив данный курс по Vue, узнали то, что хотели, и готовы к тому, чтобы узнать ещё много нового и интересного об этом фреймворке.
→ Vue.js для начинающих, урок 1: экземпляр Vue
→ Vue.js для начинающих, урок 2: привязка атрибутов
→ Vue.js для начинающих, урок 3: условный рендеринг
→ Vue.js для начинающих, урок 4: рендеринг списков
→ Vue.js для начинающих, урок 5: обработка событий
→ Vue.js для начинающих, урок 6: привязка классов и стилей
→ Vue.js для начинающих, урок 7: вычисляемые свойства
→ Vue.js для начинающих, урок 8: компоненты
→ Vue.js для начинающих, урок 9: пользовательские события
→ Vue.js для начинающих, урок 10: формы