Мы разрабатываем ATI.SU — это площадка, где грузоотправитель находит грузоперевозчика. Между собой они общаются заявками. Заявка — это карточка со множеством полей. Так мы её и зовём — «Карточка груза». Поиск таких заявок по сложным фильтрам — то, зачем к нам приходят сотни тысяч пользователей. Эта статья о том, как мы с нуля переписали карточку груза, и этого почти никто не заметил. Но это хорошо, так и было задумано. Сначала я поведаю о проекте. Опишу боль, которую он вызывал. Потом расскажу, что и как мы переписали. И напоследок немного размышлений о том, что можно было сделать лучше. Поиском грузов пользуется очень большая часть наших пользователей. Если сломать поиск грузов — испортишь им рабочий день. Но какие-то изменения вносить, конечно, нужно. Появляются новые фичи, в словари добавляются населённые пункты, типы кузовов машин, меняется отображение контактных данных и т.д. С одной стороны, у нас вектор на постоянные изменения. С другой: карточка — часть старого большого фронта на Angular 1, в который, кроме неё, входят фильтр для поиска, пейджинг и масса сопутствующего кода; этот фронт — часть огромного старого приложения на ASP.NET 4.5, большая часть которого уже разъехалась по отдельным сервисам; это приложение работает только в IIS, под Windows, билд в TeamCity выполняется по 20 минут, а деплой на прод занимает до полутора часов; чтобы его развернуть и хотя бы попробовать что-то поменять, нужен опытный человек и довольно много времени. Для работы оно будет ходить в кучу баз данных и отдельных сервисов, что тоже не упрощает дело. Даже самое мелкое изменение при таком раскладе занимает дни, если не недели. (Данные по времени — для момента старта проекта, сейчас там всё стало заметно лучше, но это отдельная история.) Очень хотелось вылечить эту боль и появился проект pretty-load, он же известен как «Груз-красавчик». Проект становится красавчиком когда:  в нём элементарный онбординг: достаточно склонировать и npm install, и npm run dev;  разработка не требует конфигов, внешних сервисов и прочих сложностей; элементарный и быстрый деплой: пару минут по кнопке в GitLab CI; лёгкое тестирование: исчерпывающие тест-кейсы и автоматизация где возможно; лёгкие и дешёвые изменения: маленькие и очень-очень тупые компоненты, которые легко найти, поменять и тут же убедиться в результате; версию с прода можно откатить быстро и одной кнопкой; хватает людей, которые разбираются в проекте. Увидели где находимся, куда хотим придти — представили. Осталось придумать способ добраться. Абстрактно карточку груза можно представить как функцию. На вход подаём комбинацию разных состояний, а на выходе получаем «картинку». Внутри функция выглядит как огромное дерево решений «если… то».  Несколько примеров. Если груз едет из одной страны в другую страну, то блок направления выделяем жирным шрифтом. Если пользователь не авторизован, то не показываем ставку за перевозку.  Если в грузе есть торги, то показываем блок аукциона вместо блока ставки. И так далее.  Если из процесса разработки и тестирования удастся убрать побочные эффекты в виде походов к API, то процесс упростится и ускорится в разы. Входных параметров довольно много: описывающая груз большая структура от API поиска, свойства пользователя (платный, авторизован, из какого часового пояса и т.д.), часть параметров фильтра для поиска, предыдущие взаимодействия пользователя с грузом (заметки, скрытие) и т.д. Всё это можно не получать от сервера, а сделать дамп и положить в виде JSON на диск. Остаётся только: подобрать тест-кейсы под все варианты отображения грузов; создать эталонные грузы; собрать их стейт и положить в репозиторий.  На это ушло несколько месяцев работы наших коллег из QA, которые зафиксировали, назвали и организовали в иерархическую структуру больше тысячи разных состояний карточки. Для каждого тест-кейса есть один или несколько скриншотов. Их мы используем как эталон при разработке. Осталось создать какую-то обвязку, которая позволит видеть список тест-кейсов, и для каждого будет показывать наш компонент. Желательно, чтобы работал hot reload и прочие вещи, упрощающие разработку. Выбор пал на замечательный проект Storybook. Универсальный инструмент, верстак для разработки компонентов и демо-стенд. Благодаря ему быстро сделали первый прототип и убедились в успешности задумки. Но были и минусы, их опишу в конце. Итак, мы написали костыль к сторибуку. Его задача была — превращать дерево тест-кейсов в stories. Для каждого блока карточки выделена своя папка с index.js, в который включены все *.json файлы с названием и списком эталонных изображений. Кажется сложной задачей разрабатывать «сразу всё». Поэтому карточку побили на смысловые блоки и начали делать их по очереди. Так получились первые компоненты высокого уровня. Поначалу во всех блоках были только гифки с котиками :)   Для проверки гипотезы и начала разработки сделали самый простой блок направлений. Для него подобрали всего 24 тест-кейса. Разработка иногда вставала на паузу, иногда велась в фоне — были более приоритетные фичи. За счёт выбранного процесса массу очень важной работы сделали наши тестировщики, без них это не было бы так легко и быстро. На сегодня проект успешно запущен, сменил в процессе разработке три команды и в итоге передан на поддержку команде, которая разрабатывает поиск грузов.  Самое время немного порефлексировать. Работать в режиме «приведи одну картинку к другой» оказалось очень удобно. На одном мониторе код, на другом рядом компонент в разработке и эталонная картинка. Правишь маленькие простые компоненты, сравниваешь, переходишь от кейса к кейсу, пока все не совпадут. Огромную массу работы при таком раскладе делают QA-специалисты: готовят кейсы, собирают эталоны.  В старом Angular-приложении написали кусочек кода, который, в зависимости от фича-флага для конкретного пользователя, определял, что ему показывать — новую или старую карточку груза. В дальнейшем появилась возможность определять по наличию куки с определённым названием.  Первая версия которую увидели пользователи — карточка для неавторизованных пользователей. На ней меньше всего данных и нет самых сложных блоков взятия грузов и торгов. После этого был релиз карточки для авторизованных, а потом финальный релиз карточки с аукционами. В процессе разработке настоящие пользователи нашего эксперимента увидеть не могли. Уйму времени сэкономила возможность вливаться в мастер-ветку. В параллель работало обычно не больше двух программистов, почти не было конфликтов и других проблем. Фикс замечаний в процессе тестирования занимал считаные минуты. Нам понравился подобный комфортный процесс. Мы начали искать ещё идеи как можно упростить жизнь себе и тестированию. Карточка в сторибуке со временем обросла инструментами: помощь при разработке тултипов, переключение типа пользователя прямо на лету, настройки, влияющие на отображение, кнопка, копирующая JSON, по которому построена выдача. В какой-то момент мы добавили страницу, на которой можно было вставить JSON любой поисковой выдачи и получить рендер карточек. Торги для нас очень важный механизм. Проще всего их будет объяснить как аукцион, в котором перевозчики предлагают ставку, по которой они готовы перевезти груз. Торги разрабатывались в последнюю очередь. Аукцион — штука интерактивная и слушает вебсокеты. От остальной карточки отличается тем, что может радикально менять свой вид с течением времени или по событиям. (Аукцион завершился — вид поменялся. Ставку перебили — поменялся.) Всё ещё избегая работы с сервером, вебсокеты по привычке заменили кнопкой в сторибуке, нажал кнопку — отправил очередной JSON как событие в карточку.  В целом аукцион довольно неплохо описывается как конечный автомат. Почти каждый тест-кейс для аукционов представлен в виде двух состояний карточки: до события, после события. Подвох торгов — в работе теперь участвуют и другие сервисы, для их проведения нужно использовать от двух до трёх тестовых пользователей одновременно, JSON-ов надо собирать заметно больше и картинок на каждый кейс получается по две-три минимум. Слишком большое количество ручной работы. API аукционов меняется чаще, чем довольно стабильный поиск грузов, и может понадобиться пересобирать дампы. Тут на помощь пришла команда автотестеров. Под все тест-кейсы для аукционов были написаны автотесты с дополнительной нагрузкой в виде сохранения скриншотов и дампов ответов сервера. Теперь всегда можно проверить корректность работы, нажав одну кнопку в GitLab и обновив все сопутствующие  .json файлы одним мерж-реквестом. (А заодно, увидеть в диффе, что именно поменялось.) Для ясности картинки, каждый тест делает следующее:  генерирует груз; создаёт нескольких пользователей и площадку, на которой тот будет размещён; создаёт аукцион для груза; находит груз разными пользователями, делает ставку или совершает другие действия; снимает скриншоты и дампы всех релевантных JSON-ответов. Кроме этого, коллеги выручили нас, прикрутив к сторибуку прогон визуальных тестов. Если где-то съехали стили, а ты этого не планировал, то узнаешь об этом сразу после пуша в репозиторий, а не от пользователей. При старте проекта я потратил немного времени, чтобы зафиксировать приоритеты для проекта в порядке убывания важности. Получился вот такой список, копирую из readme проекта: скорость; размер; лёгкость поддержки; реюзабельность; developer experience. То есть скорость и размер приложения важнее лёгкости поддержки — это сознательный выбор для приложения, которое грузит сотни тысяч пользователей.  Эти пункты не один раз вызывали споры. Неоднократно кто-то порывался добавить очередную «облегчающую жизнь» библиотеку. Результат стоил всех споров, до сих пор карточка груза занимает меньше 100 килобайт (gzipped), и в итоговую сборку не попадает ни одной внешней зависимости (кроме небольшого количества полифиллов и пары внутренних небольших библиотек, одна из которых morpheus). Словарей у нас много. Города, типы кузовов (Знаешь ли ты, что такое клюшковоз?), типы грузов и т.д. Обычный подход к подобным вещам — выдавать расшифровку значений сразу с бэкенда, если необходим полный словарь — запрашивать его у API.  Ни одного лишнего запроса к серверу делать не хотелось. Если посмотреть на динамику изменения словарей, окажется, что большинство меняется редко. Те, которые нужны карточке — очень редко. Поэтому при билде проекта для всех нужных словарей мы делаем следующее:  забираем данные из БД или другого источника словарей; выбрасываем ненужные поля; преобразуем их в тот вид, в котором их будет удобно использовать; сохраняем в виде *.json в проект; в нужных местах делаем import dic from “data/dics/dictionary.json”; Да, такой способ требует ребилда проекта при редком добавлении нового типа кузова. Но лучше сделать ребилд, чем сотни тысяч ненужных запросов. Проект по переключению фича-флагов на клиенте. Он был создан как раз для разработки pretty-load. Это один из трюков, который позволил нам освоить некоторые практики trunk based development и пилить новые фичи прям в мастер.  Мы прячем часть проекта так, чтобы её смог увидеть только тестировщик и разработчик. Нет долго висящих мерж-реквестов и нет конфликтов. Проверено на себе: когда постоянно вкатываешь в мастер небольшими порциями — психологически  легче работать.  И до сих пор в карточке груза, спрятанные morpheus, живут некоторые инструменты для тестировщиков. В момент старта проекта выбрали webpack. Если бы начинали проект сейчас — это гарантированно был бы esbuild, swc или другие экспериментальные проекты. Возможность срезать лишнюю минуту из билда, если помножить её на годы работы — бесценна. Моя ошибка — использование в данном проекте CSS Modules. Сама технология отличная и поначалу мне понравилась история с хэшированными классами. Плюс понятен — гарантированно ни с чем не пересекутся.  Минусы — при некоторых настройках кэширования статики могут быть проблемы со стилями у пользователей после новой выкладки. Теряется отличная возможность быстро найти соответствующий дом-элементу компонент в проекте. Довольно муторное переиспользование классов, который приводит иногда к дупликации (см. приоритеты проекта). Единственный плюс легко можно заменить чёткой договорённостью о нейминге классов.  Если бы мы делали проект ещё раз, написали бы свою обвязку вместо storybook. Всё в нём хорошо, но тащить многие сотни зависимостей не лучшее из моих решений. Из всего спектра возможностей storybook мы используем только дерево stories и демонстрацию компонента. Это можно было написать за день-два самостоятельно. В какой-то момент, пытаясь ускорить сборку, я попробовал утрамбовать storybook в отдельный Docker контейнер и научить его работать с произвольными папками на диске. Времени на это ушло нерационально много, а выхлоп не так уж и порадовал.  Люблю смотреть результаты работы gource на больших проектах. CTO