От автора: рендеринг списков – одна из наиболее используемых практик в front end разработке. Динамический рендеринг списков часто используется для представления серии схожей сгруппированной информации в сжатом и дружелюбном для пользователя формате. Почти в каждом используемом нам веб-приложении можно найти списки контента во множестве областей. В этой статье я расскажу про директиву Vue v for при создании динамических списков, покажу пару примеров о том, почему для этого нужно использовать атрибут key. Мы все будем подробно объяснять при написании кода. Статья предполагает, что у вас нет знаний по Vue, или вы знаете совсем немного.
Пример: Twitter
В качестве примера для статьи возьмем Twitter. После авторизации на главном роуте index в Twitter нам открывается следующий вид:
На домашней странице мы привыкли видеть список трендов, список твитов, список возможных фоловеров и т.д. Контент в этих списках зависит от множества факторов – наши история в Twitter, наши подписки, наши лайки и т.д. В общем, можно сказать, все эти данные динамические.
Данные получаются динамически, но их отображение остается одинаковым. Это отчасти связано с использованием многоразовых веб-компонентов.
Например, список твитов – это список элементов tweet-component. tweet-component можно представить, как оболочку, которая собирает данные (username, id пользователя, твит и аватар) из других частей и просто отображает в единообразной разметке.
Скажем, нам нужно отрендерить список компонентов (например, список элементов tweet-component) на основе большого объема данных, полученных с сервера. Первое, что должно прийти на ум в Vue, это директива v-for.
Директива v-for
Директива v-for используется для рендера списка элементов на основе исходных данных. Директиву можно использовать на шаблонном элементе. Для этого нужен специальный синтаксис:
Разберем пример. Предполагаем, что уже получили коллекцию твитов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
const tweets = [ { id: 1, name: 'James', handle: '@jokerjames', img: '//semantic-ui.com/images/avatar2/large/matthew.png', tweet: "If you don't succeed, dust yourself off and try again.", likes: 10, }, { id: 2, name: 'Fatima', handle: '@fantasticfatima', img: '//semantic-ui.com/images/avatar2/large/molly.png', tweet: 'Better late than never but never late is better.', likes: 12, }, { id: 3, name: 'Xin', handle: '@xeroxin', img: '//semantic-ui.com/images/avatar2/large/elyse.png', tweet: 'Beauty in the struggle, ugliness in the success.', likes: 18, } ] |
Tweets – это коллекция объектов tweet. В каждом tweet содержится детальная информация одного твита – уникальный id, имя, id профиля, сообщение и т.д. Теперь давайте попробуем с помощью директивы v-for отрендерить список компонентов tweet на основе этих данных.
Прежде всего создаем объект Vue – сердце приложения Vue. Мы подключим/прицепим наш объект к DOM элементу с id app и присвоим коллекцию tweets, как часть объекта данных объекта Vue.
1 2 3 4 5 6 |
new Vue({ el: '#app', data: { tweets } }); |
Теперь создадим tweet-component, который будет использовать директива v-for для рендера списка. Используем глобальный конструктор Vue.component для создания компонента tweet-component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
Vue.component('tweet-component', { template: ` <div class="tweet"> <div class="box"> <article class="media"> <div class="media-left"> <figure class="image is-64x64"> <img :src="tweet.img" alt="Image"> </figure> </div> <div class="media-content"> <div class="content"> <p> <strong>{{tweet.name}}</strong> <small>{{tweet.handle}}</small> <br> {{tweet.tweet}} </p> </div> <div class="level-left"> <a class="level-item"> <span class="icon is-small"><i class="fas fa-heart"></i></span> <span class="likes">{{tweet.likes}}</span> </a> </div> </div> </article> </div> </div> `, props: { tweet: Object } }); |
Что здесь стоит отметить:
tweet-component ожидает prop объект tweet, как видно из требования валидации prop (props: {tweet: Object}). Если компонент рендерится с tweet prop, который не является объектом, Vue пропустит предупреждения.
Мы привязываем свойства prop объекта tweet к шаблону компонента с помощью синтаксиса {{ }}.
Разметка компонента адаптирует элемент Bulma Box, так как он сильно похож на твит.
В HTML шаблоне нужно создать разметку, куда будет подключаться наше Vue приложение (например, элемент с id app). Внутри этой разметку мы будем использовать v-for для рендера списка твитов. Tweets – это коллекция данных, по которой мы будем бегать в цикле. Поэтому подходящим алиасом для использования в директиве будет tweet. В каждый отрендереный tweet-component будем передавать объект tweet из цикла в виде props, чтобы он был доступен в компоненте.
1 2 3 4 5 |
<div id="app" class="columns"> <div class="column"> <tweet-component v-for="tweet in tweets" :tweet="tweet"/> </div> </div> |
Независимо от количества объектов tweet в коллекции и того, как они могут меняться со временем, наш код всегда будет рендерить все твиты из коллекции в той же разметке. Добавим немного своего CSS, и наше приложение выглядит примерно так:
Все работает, как надо, но в консоли можно обнаружить совет от Vue:
1 |
[Vue tip]: <tweet-component v-for="tweet in tweets">: component lists rendered with v-for should have explicit keys... |
При запуске кода на CodePen вы можете не увидеть предупреждения в консоли браузера.
Почему Vue говорит нам указать явные ключи списка, когда все работает?
Key
Для элементов в цикле принято добавлять атрибут key внутри списка v-for. Vue использует атрибут key для создания уникальной привязки для каждой идентичности узла.
Давайте разъясним – если в наш список будут внесены UI изменения (например, перемешается порядок элементов), Vue будет менять данные в каждом элементе, а не двигать DOM элементы. В большинстве случаев это не будет проблемой. Тем не менее, в определенных сценариях, где список v-for зависит от состояния DOM и/или состояния дочернего компонента, это может привести к нежелательному поведению.
Разберем пример. Что, если бы наш простой компонент твита содержал поле ввода, позволяющее пользователю автоматически отвечать на твит? Не будем вдаваться в то, как этот ответ будет отправлен, просто обратимся к новому полю ввода:
Добавим новое поле ввода в шаблон tweet-component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Vue.component('tweet-component', { template: ` <div class="tweet"> <div class="box"> // ... </div> <div class="control has-icons-left has-icons-right"> <input class="input is-small" placeholder="Tweet your reply..." /> <span class="icon is-small is-left"> <i class="fas fa-envelope"></i> </span> </div> </div> `, props: { tweet: Object } }); |
Предположим, что хотели добавить новую функцию в приложение. Эта функция позволяет перемешивать случайным образом список твитов. Для этого сначала нужно подключить кнопку Shuffle! в HTML шаблоне:
1 2 3 4 5 6 |
<div id="app" class="columns"> <div class="column"> <button class="is-primary button" @click="shuffle">Shuffle!</button> <tweet-component v-for="tweet in tweets" :tweet="tweet"/> </div> </div> |
Мы подключили обработчик события click на кнопку. По клику вызывается метод shuffle. В объекте Vue создадим метод shuffle, который будет случайно перемешивать коллекцию tweets в объекте. Для этого используем метод lodash _shuffle:
1 2 3 4 5 6 7 8 9 10 11 |
new Vue({ el: '#app', data: { tweets }, methods: { shuffle() { this.tweets = _.shuffle(this.tweets) } } }); |
Давайте проверим! Если кликнуть несколько раз на shuffle, мы заметим, что каждый клик случайным образом перемешивает твиты.
Но если ввести текст в поля компонентов и нажать shuffle, происходит нечто необычное:
Мы не стали использовать атрибут key, поэтому Vue не создал уникальные привязки к узлам твитов. Как результат, когда мы хотим отсортировать твиты, Vue идет самым быстрым способом и просто меняет данные во всех элементах. Временное состояние DOM (например, введенный текст) не затрагивается, поэтому нам это не подходит.
На рисунке показано, как во всех элементах меняются данные, а состояние DOM нет:
Чтобы решить эту проблему, нам нужно присвоить уникальный key всем tweet-component, рендерящимся в списке. В качестве уникального идентификатора возьмем id tweet, так как мы точно знаем, что они все разные. Мы используем динамические значения, поэтому используем директиву v-bind для привязки нашего key к tweet.id:
1 2 3 4 5 6 |
<div id="app" class="columns"> <div class="column"> <button class="is-primary button" @click="shuffle">Shuffle!</button> <tweet-component v-for="tweet in tweets" :tweet="tweet" :key="tweet.id" /> </div> </div> |
Теперь Vue видит каждую идентичность узла твита и будет правильно сортировать компоненты при перемешивании списка.
Теперь твиты перемещаются правильно. Можно показать, как именно сортируются элементы с помощью Vue transition-group.
Для этого добавим элемент transition-group как обертку к списку v-for. Укажем имя перехода tweets и объявим, что группа перехода должна рендериться как тег div.
1 2 3 4 5 6 7 8 |
<div id="app" class="columns"> <div class="column"> <button class="is-primary button" @click="shuffle">Shuffle!</button> <transition-group name="tweets" tag="div"> <tweet-component v-for="tweet in tweets" :tweet="tweet" :key="tweet.id" /> </transition-group> </div> </div> |
По имени перехода Vue автоматически распознает любые установленные CSS переходы/анимации. Наша цель – переместить элементы в списке с помощью перехода. Поэтому Vue будет искать CSS переход в строках tweets-move (где tweets – имя, указанное в transition group). Как результат, мы вручную вводим класс .tweets-move, в котором задан определенный тип и время перехода:
1 2 3 |
#app .tweets-move { transition: transform 1s; } |
Это очень краткий пример применения переходов к спискам. Посмотреть все типы переходов можно в документации Vue.
Теперь наши элементы tweet-component будут плавно менять положение при перемешивании. Попробуйте! Введите что-нибудь в поля ввода и кликните Shuffle! несколько раз.
Круто, правда? Без атрибута key мы бы не смогли использовать элемент transition-group для создания перехода в списке, так как вместо элементов менялись бы данные в них.
Стоит ли всегда использовать атрибут key? Рекомендуется. Спецификация Vue утверждает, что атрибут key не стоит использовать только:
Нам специально нужна стандартная замена элементов для повышения производительности
Контента DOM просто достаточно
Заключение
Вот и все! Надеюсь, эта короткая статья показала полезность директивы v-for и объяснила, почему атрибут key так часто используется. Если у вас возникли вопросы/мысли, дайте мне знать.
Автор: Hassan Djirdeh
Источник: //css-tricks.com/
Редакция: Команда webformyself.