Админка магазина на vue.js. Урок 9. Перерабатываем фильтры
Мы уже поработали и с vuex, и с компонентами vue. А сегодня затронем обе темы и займемся небольшим рефакторингом. Возможно, вы спросите зачем?
Вообще я редко рассуждаю о правильных паттернах и практиках, но сегодня на меня напал легкий перфекционизм. Я уже немного бомбил на тему, что компоненты товаров и фильтры сделаны не так, как хотелось бы. Частично потому, что в первых уроках без vuex мои задумки неудобно было исправлять, частично потому что находились и другие интересные темы. В итоге товары и фильтры получились очень громоздким компонентом.
Кратко вспомним суть претензий: компонент товаров огромный, нужно его разносить, фильтры отдельно, фильтрам не нужно ничего знать о товарах, логику фильтрации нужно выносить во vuex. В этом уроке мы и займемся причесыванием кода, сделаем приложение чуть более структурированным, а заодно узнаем несколько моментов работы с vue. По функционалу ничего нового не будет, поэтому если вам не интересны манипуляции над кодом ради какой-то мифической правильности, просто скачайте исходники и дождитесь следующего урока. Там точно будет новый функционал и будет на что посмотреть.
Итак, поехали.
Геттер getProductsByFilter
Для начала создадим в модуле products.js геттер для получения отфильтрованных и отсортированных товаров - getProductsByFilter. Код для геттера мы практически скопируем из компонента Products.vue, вычисляемого свойства filteredProducts. Но есть и отличия: до этого данные для фильтров и сортировок мы брали из свойств компонента Products.vue. Напомню, это selectCategory, selectBrand, minPrice и maxPrice, inputSearch и selectSort. Вроде ничего не забыл. Этих данных у нас в геттере нет, мы их передадим параметром. Это легально, ведь геттеры можно использовать не только как свойства, но и как функции. Все эти параметры мы завернем в объект filter и передадим его в геттер. А там уже разберем их по кусочкам и отфильтруем-отсортируем товары.
Вот пример объекта filter:
{ selectCategory: 2, selectBrand: 5, minPrice: 2500, maxPrice: 1000, inputSearch: 'Asus', selectSort: 'price:desc' }
Откуда мы сформируем этот объект, пока не задумываемся, главное, что в геттер он приходит именно в таком формате. Теперь пишем код самого геттера. Я сначала выложу код, он уже очень знакомый, а потом напишу, что поменялось.
getProductsByFilter: state => filter => { // Фильтруем товары let filtered = state.all // По категории .filter(product => { return filter.selectCategory == 0 || product.categoryId == filter.selectCategory; }) // По брендам .filter(product => { return filter.selectBrand == 0 || product.brandId == filter.selectBrand; }) // По ценам .filter(product => { return Number(product.price) >= filter.minPrice && Number(product.price) <= filter.maxPrice; }) // По полю поиска .filter(product => { return filter.inputSearch == '' || product.title.toLowerCase().indexOf(filter.inputSearch.toLowerCase()) !== -1; }); // Сортируем let sortKey = filter.selectSort.split(':')[0]; let sortDir = filter.selectSort.split(':')[1]; let sorted = _.sortBy(filtered, product => { return Number(product[sortKey]); }); // При необходимости сортируем в обратном направлении if (sortDir === 'desc') { sorted = sorted.reverse(); } return sorted; }
Первое, что видим, немножко необычная первая строка
getProductsByFilter: state => filter => { ... }
В геттер всегда первым параметром передается state, а нам еще нужен второй параметр filter, поэтому получилась такая хитрая цепочка.
Второе, в самом начале в переменную filtered записываем не this.products, как раньше в Products.vue, а state.all. Ну логично, это же хранилище, там нет свойств, а есть state.
Третье, в условиях и вычислениях вместо this.название_поля пишем filter.название_поля. Помним, что эти данные берем не из свойств, а уже из входного параметра filter.
И четвертое, раньше sortKey и sortDir мы брали из вычисляемых полей, но сейчас это ни к чему, можно просто завести переменные в коде геттера.
let sortKey = filter.selectSort.split(':')[0]; let sortDir = filter.selectSort.split(':')[1];
А так все, подробно алгоритм фильтрации и сортировки мы разбирали еще во втором уроке и принципиально там ничего не поменялось. Из компонентах геттер вызывается вот таким образом
this.$store.getters['products/getProductsByFilter'](filter);
Но об этом чуть ниже.
Сокращаем код Products.vue в 3 раза
Да, в целых 3 раза: у меня было 154 строки, а осталось 54. Как это сделать?
Сначала выкинем весь html-код фильтров, вот такую здоровенную байду
Мы уже говорили, что для фильтров создадим отдельный компонент, соответственно, весь этот код уйдет туда. А вместо этого набора мы просто подключим новый, еще не написанный компонент ProductsFilters, вот так
Забегая чуть вперед: что за @filter="filter"? Это значит, что компонент ProductsFilters.vue будет генерировать событие filter (@filter), а при его срабатывании мы будем запускать метод filter компонента Products.vue. Метод тоже напишем ниже.
То есть вырисовывается такая схема: компонент фильтров при изменении любых данных (типа категории или сортировки) собирает все эти данные из 6 полей в один объект и отправляет его родительскому компоненту Products.vue. А тот уже вызывает геттер getProductsByFilter и отрисовывает таблицу.
В итоге получается так, что фильтры ничего не знают о товарах, они просто принимают от пользователя данные и отправляют их компоненту товаров. А товары в свою очередь не в курсе, как реализованы фильтры. Товары просто принимают набор фильтров, вызывают геттер и отрисовывают список товаров.
Теперь, разобравшись с общей схемой, можно дописать компонент Products.vue. Переходим к тегу script
Сначала убираем импорты mapGetters и lodash и импортируем компонент ProductsFilters. То есть в импортах будет так
import ProductsFilters from './ProductsFilters' import ProductsItem from './ProductsItem'
Идем дальше. Регистрируем новый компонент ProductsFilters в поле components, вот так
components: { ProductsFilters, ProductsItem }
Дальше изменения кардинальные. Удаляем из поля data все, что есть, все эти sortRules, selectBrand и minPrice. Нам это уже не интересно, пусть ими занимается новый компонент фильтров. А в товарах останется одно единственное поле - отфильтрованные товары filteredProducts
data () { return { filteredProducts: [] } }
Затем удаляем целиком поле computed. Да-да, minPriceAll, categories, products и filteredProducts, sortKey и sortDir здесь не нужны. Они или переходят в фильтры, или уже реализованы в геттере getProductsByFilter. Раздел watch тоже сносим, отслеживать изменения будем в фильтрах.
И еще изменим методы - поле methods. Сейчас у нас там очистка полей фильтров clear - это уходит тоже в фильтры. А вместо clear будет новый метод filter - тот самый, о котором я упоминал в начале, помните?
Это единственный метод, который действительно нужен в компоненте товаров и реализуется он очень просто
methods: { filter (filter) { this.filteredProducts = this.$store.getters['products/getProductsByFilter'](filter); } }
В поле filteredProducts записывается результат вызова геттера. А набор параметров фильтра приходит из компонента ProductsFilters через прослушивание кастомного события @filter.
На этом с товарами все, компонент получился такой маленький, что я даже не постесняюсь привести полный код Products.vue
Список товаров
Id Название Бренд Цена Рейтинг
Как видите, он стал гораздо проще и понятнее. Фильтры и таблица. И один метод, принимающий набор фильтров и вызывающий геттер. Можно было еще и выделить таблицу в отдельный компонент, но и так получилось довольно коротко и аккуратно.
А теперь переходим к новому компоненту ProductsFilters
Компонент ProductsFilters
К сожалению, код, который мы весело выкинули из товаров, нельзя просто взять и удалить. Он должен всплыть, как, хм, поплавок, где-то в другом месте. В одном месте, в vuex-модуле products.js, всплыл геттер getProductsByFilter, а остальное мы оформим в компоненте ProductsFilters.
Для начала самое простое - шаблон. Здесь мы просто копируем html-разметку, которую удалили из товаров
С одним небольшим отличием: раньше в товарах поля типа selectCategory брались из соответствующих полей в data, а у нас они будут браться из объекта filter, например, filter.selectCategory
Теперь к javascript-коду. Сначала импортируем mapGetters
import { mapGetters } from 'vuex'
Потом оформим data
data () { return { sortRules: [ { key :'id:asc', title: 'По порядку' }, { key :'rating:desc', title: 'По рейтингу' }, { key :'price:asc', title: 'По цене, сначала дешевые' }, { key :'price:desc', title: 'По цене, сначала дорогие' } ], filter: { inputSearch: '', selectCategory: 0, selectBrand: 0, minPrice: 0, maxPrice: 0, selectSort: 'id:asc' } } }
sortRules мы уже видели и целиком скопировали, а в объект filter закинули все поля, участвующие в фильтрах и сортировках. Это тот самый объект, который передается в геттер getProductsByFilter. Ниже мы увидим, как он уходит наверх, в компонент товаров.
Теперь нам нужно создать несколько вычисляемых полей
computed: { ...mapGetters('products', { minPriceAll: 'minPrice', maxPriceAll: 'maxPrice' }), categories () { return this.$store.state.categories.all; }, brands () { return this.$store.state.brands.all; }, products () { return this.$store.state.products.all; } }
minPriceAll и maxPriceAll мы получаем, как и в прошлом уроке, через mapGetters, а категории, бренды и товары через state.
Здесь вы можете задать коварный вопрос: как же так, ты давеча рассказывал, что фильтры ничего не знают о товарах? К сожалению, нам приходится здесь использовать товары для одной-единственной цели: отслеживать их изменения, чтобы устанавливать minPriceAll и maxPriceAll. Напомню, это минимальная и максимальная стоимости товаров и они задаются как начальные значения полей filter.minPrice и filter.maxPrice.
Возможно, есть вариант сделать это как-то элегантнее, через модуль products.js или еще как-то, но я пока не сообразил. Если у вас есть варианты, накидывайте в комментариях, буду благодарен любым идеям. Впрочем, и текущий код меня более менее устраивает, ведь это все равно намного лучше, чем мешанина фильтров и логики работы с товарами в одном компоненте.
Итак, насчет прослушивания изменений товаров, вот как это выглядит
watch: { products () { this.filter.minPrice = this.minPriceAll; this.filter.maxPrice = this.maxPriceAll; } }
Изменились товары, пересчитали максимальную и минимальную цены. В старом Products.vue мы это уже делали. А вот что новое, так это главная функция фильтров: передать данные наверх при изменении любого поля фильтров или сортировки. Реализуется это через прослушивание объекта filter
filter: { handler (newFilter) { this.$emit('filter', newFilter); }, deep: true }
Здесь по-другому: это не функция, как при прослушивании products в коде выше, а объект из двух полей. handler - это собственно функция, в которой событие уходит наверх вместе с данными через $emit. И deep: true - важный флажок, означающий, что нас интересует прослушивание изменений любого поля объекта filter. Без этого флага прослушивание работает для строк, чисел, массивов, но не объектов. Говорят, что в третьей версии vue Эван Ю это пофиксит, но пока так. Конечно, невелика сложность проставить deep: true, но не зная этой хитрости, можно убить порядочно времени, как это получилось у меня :-)
Мы заканчиваем писать компонент ProductsFilters, осталось реализовать метод clear - сброс фильтров в дефолтные значения
methods: { clear () { this.filter = { inputSearch: '', selectCategory: 0, selectBrand: 0, minPrice: this.minPriceAll, maxPrice: this.maxPriceAll, selectSort: 'id:asc' } } }
Этот метод мы тоже уже видели, здесь только разница, что мы сбрасываем не 6 свойств, а 6 полей одного свойства filter.
На этом почти все. Мы разделили большой компонент товаров на простой компонент и фильтры. В итоге кода получилось не меньше, но он лучше организован. А организация кода - это важная штука даже для таких небольших проектов, как наша админка, что уж говорить о больших приложениях на десятки тысяч строк.
Кстати, я не сторонник идеи, что нужно разбивать приложение на как можно меньшие компоненты, как советуют любители реакта. Если в компоненте 3 сотни строк, но они все относятся к одной сущности и легко считываются, то нет смысла плодить файлы и держать в уме их иерархию. Все-таки стараюсь придерживаться середины и не впадать в крайности.
Переносим компоненты в отдельную папку
Последнее, что мы сделаем сегодня - это чуток поправим организацию файлов в проекте. Сейчас в папке src лежит главный файл main.js, хранилище vuex - папка store и куча компонентов. Почему-то мы для хранилища выделили отдельную папку, а для компонентов нет. Давайте исправим, создадим папку components и перенесем в нее все компоненты. Только не забудьте поправить импорт App.vue в файле main.js
import App from './components/App.vue'
Вот теперь в src все по полочкам: в components - компоненты, в store - хранилище vuex, в main.js - главный файл, запускающий приложение. Красота!
На сегодня все, удачи и до встречи в следующих уроках!
Все уроки админки на vue.js
- Урок 1. Список товаров
- Урок 2. Фильтры и сортировки
- Урок 3. Новое REST API на чистом PHP
- Урок 4. Правим клиентский код под новое REST API и находим багу
- Урок 5. Разбиваем приложение на компоненты
- Урок 6. Инструмент vue-cli и vue-компоненты
- Урок 7. Flux и Vuex - общие вопросы
- Урок 8. Vuex на практике
- Урок 9. Перерабатываем фильтры
- Урок 10. Добавляем и удаляем бренды
- Урок 11. Обрабатываем ошибки на клиенте и сервере
- Урок 12. Редактируем бренды
- Урок 13. Роутинг
- Урок 14. Карточка товара
Истории из жизни айти и обсуждение кода.