Стелс-миграция с первого Angular на React 17, или история о том, как дёшево побороть страшное легаси

Мы разрабатываем 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

Выбор пал на замечательный проект Storybook. Универсальный инструмент, верстак для разработки компонентов и демо-стенд. Благодаря ему быстро сделали первый прототип и убедились в успешности задумки. Но были и минусы, их опишу в конце.

Итак, мы написали костыль к сторибуку. Его задача была — превращать дерево тест-кейсов в stories. Для каждого блока карточки выделена своя папка с index.js, в который включены все *.json файлы с названием и списком эталонных изображений.

Пример дерева тест-кейсов. В JSON файлах просто ответ от API
Пример дерева тест-кейсов. В JSON файлах просто ответ от API

Кажется сложной задачей разрабатывать «сразу всё». Поэтому карточку побили на смысловые блоки и начали делать их по очереди. Так получились первые компоненты высокого уровня. Поначалу во всех блоках были только гифки с котиками 🙂

Разбили карточку на смысловые блоки
Разбили карточку на смысловые блоки

Для проверки гипотезы и начала разработки сделали самый простой блок направлений. Для него подобрали всего 24 тест-кейса.

Мистер Блок Направлений
Мистер Блок Направлений

Разработка иногда вставала на паузу, иногда велась в фоне — были более приоритетные фичи. За счёт выбранного процесса массу очень важной работы сделали наши тестировщики, без них это не было бы так легко и быстро. На сегодня проект успешно запущен, сменил в процессе разработке три команды и в итоге передан на поддержку команде, которая разрабатывает поиск грузов.

Самое время немного порефлексировать.

Что получилось хорошо

Поток разработки под пальмой с коктейлем

Работать в режиме «приведи одну картинку к другой» оказалось очень удобно. На одном мониторе код, на другом рядом компонент в разрботке и эталонная картинка. Правишь маленькие простые компоненты, сравниваешь, переходишь от кейса к кейсу, пока все не совпадут. Огромную массу работы при таком раскладе делают QA-специалисты: готовят кейсы, собирают эталоны.

В старом Angular-приложении написали кусочек кода, который, в зависимости от фича-флага для конкретного пользователя, определял, что ему показывать — новую или старую карточку груза. В дальнейшем появилась возможность определять по наличию куки с определённым названием.

Первая версия которую увидели пользователи — карточка для неавторизованных пользователей. На ней меньше всего данных и нет самых сложных блоков взятия грузов и торгов. После этого был релиз карточки для авторизованных, а потом финальный релиз карточки с аукционами.

В процессе разработке настоящие пользователи нашего эксперимента увидеть не могли. Уйму времени сэкономила возможность вливаться в мастер-ветку. В параллель работало обычно не больше двух программистов, почти не было конфликтов и других проблем. Фикс замечаний в процессе тестирования занимал считаные минуты.

Нам понравился подобный комфортный процесс. Мы начали искать ещё идеи как можно упростить жизнь себе и тестированию. Карточка в сторибуке со временем обросла инструментами: помощь при разработке тултипов, переключение типа пользователя прямо на лету, настройки, влияющие на отображение, кнопка, копирующая JSON, по которому построена выдача.

В какой-то момент мы добавили страницу, на которой можно было вставить JSON любой поисковой выдачи и получить рендер карточек.

Вот, что получилось в результате. Скриншот современный, в разработке это выглядело попроще, да и эталоны уже немного устарели
Вот, что получилось в результате. Скриншот современный, в разработке это выглядело попроще, да и эталоны уже немного устарели

Система торгов: автотесты спасают всех

Торги для нас очень важный механизм. Проще всего их будет объяснить как аукцион, в котором перевозчики предлагают ставку, по которой они готовы перевезти груз.

Торги разрабатывались в последнюю очередь. Аукцион — штука интерактивная и слушает вебсокеты. От остальной карточки отличается тем, что может радикально менять свой вид с течением времени или по событиям. (Аукцион завершился — вид поменялся. Ставку перебили — поменялся.) Всё ещё избегая работы с сервером, вебсокеты по привычке заменили кнопкой в сторибуке, нажал кнопку — отправил очередной JSON как событие в карточку.

В целом аукцион довольно неплохо описывается как конечный автомат. Почти каждый тест-кейс для аукционов представлен в виде двух состояний карточки: до события, после события.

Подвох торгов — в работе теперь участвуют и другие сервисы, для их проведения нужно использовать от двух до трёх тестовых пользователей одновременно, JSON-ов надо собирать заметно больше и картинок на каждый кейс получается по две-три минимум. Слишком большое количество ручной работы. API аукционов меняется чаще, чем довольно стабильный поиск грузов, и может понадобиться пересобирать дампы.

Тут на помощь пришла команда автотестеров. Под все тест-кейсы для аукционов были написаны автотесты с дополнительной нагрузкой в виде сохранения скриншотов и дампов ответов сервера. Теперь всегда можно проверить корректность работы, нажав одну кнопку в GitLab и обновив все сопутствующие .json файлы одним мерж-реквестом. (А заодно, увидеть в диффе, что именно поменялось.)

Для ясности картинки, каждый тест делает следующее: 

  • генерирует груз;

  • создаёт нескольких пользователей и площадку, на которой тот будет размещён;

  • создаёт аукцион для груза;

  • находит груз разными пользователями, делает ставку или совершает другие действия;

  • снимает скриншоты и дампы всех релевантных JSON-ответов.

Кроме этого, коллеги выручили нас, прикрутив к сторибуку прогон визуальных тестов. Если где-то съехали стили, а ты этого не планировал, то узнаешь об этом сразу после пуша в репозиторий, а не от пользователей.

Приоритеты при принятии решений

При старте проекта я потратил немного времени, чтобы зафиксировать приоритеты для проекта в порядке убывания важности.

Получился вот такой список, копирую из readme проекта:

То есть скорость и размер приложения важнее лёгкости поддержки — это сознательный выбор для приложения, которое грузит сотни тысяч пользователей.

Эти пункты не один раз вызывали споры. Неоднократно кто-то порывался добавить очередную «облегчающую жизнь» библиотеку. Результат стоил всех споров, до сих пор карточка груза занимает меньше 100 килобайт (gzipped), и в итоговую сборку не попадает ни одной внешней зависимости (кроме небольшого количества полифиллов и пары внутренних небольших библиотек, одна из которых morpheus).

Словари

Словарей у нас много. Города, типы кузовов (Знаешь ли ты, что такое клюшковоз?), типы грузов и т.д. Обычный подход к подобным вещам — выдавать расшифровку значений сразу с бэкенда, если необходим полный словарь — запрашивать его у API.

Ни одного лишнего запроса к серверу делать не хотелось.

Если посмотреть на динамику изменения словарей, окажется, что большинство меняется редко. Те, которые нужны карточке — очень редко. Поэтому при билде проекта для всех нужных словарей мы делаем следующее: 

  • забираем данные из БД или другого источника словарей;

  • выбрасываем ненужные поля;

  • преобразуем их в тот вид, в котором их будет удобно использовать;

  • сохраняем в виде *.json в проект;

  • в нужных местах делаем import dic from “data/dics/dictionary.json”;

Да, такой способ требует ребилда проекта при редком добавлении нового типа кузова. Но лучше сделать ребилд, чем сотни тысяч ненужных запросов.

Morpheus

Проект по переключению фича-флагов на клиенте. Он был создан как раз для разработки pretty-load. Это один из трюков, который позволил нам освоить некоторые практики trunk based development и пилить новые фичи прям в мастер.

Мы прячем часть проекта так, чтобы её смог увидеть только тестировщик и разработчик. Нет долго висящих мерж-реквестов и нет конфликтов. Проверено на себе: когда постоянно вкатываешь в мастер небольшими порциями — психологически легче работать.

И до сих пор в карточке груза, спрятанные morpheus, живут некоторые инструменты для тестировщиков.

Что можно было бы сделать лучше

Бандлеры

В момент старта проекта выбрали webpack. Если бы начинали проект сейчас — это гарантированно был бы esbuild, swc или другие экспериментальные проекты. Возможность срезать лишнюю минуту из билда, если помножить её на годы работы — бесценна.

CSS Modules

Моя ошибка — использование в данном проекте CSS Modules. Сама технология отличная и поначалу мне понравилась история с хэшированными классами.

Плюс понятен — гарантированно ни с чем не пересекутся.

Минусы — при некоторых настройках кэширования статики могут быть проблемы со стилями у пользователей после новой выкладки. Теряется отличная возможность быстро найти соответствующий дом-элементу компонент в проекте. Довольно муторное переиспользование классов, который приводит иногда к дупликации (см. приоритеты проекта).

Единственный плюс легко можно заменить чёткой договорённостью о нейминге классов.

Storybook?

Если бы мы делали проект ещё раз, написали бы свою обвязку вместо storybook. Всё в нём хорошо, но тащить многие сотни зависимостей не лучшее из моих решений. Из всего спектра возможностей storybook мы используем только дерево stories и демонстрацию компонента. Это можно было написать за день-два самостоятельно.

В какой-то момент, пытаясь ускорить сборку, я попробовал утрамбовать storybook в отдельный Docker контейнер и научить его работать с произвольными папками на диске. Времени на это ушло нерационально много, а выхлоп не так уж и порадовал.

И напоследок

Люблю смотреть результаты работы gource на больших проектах.

gource -s .06 -1280x720 --auto-skip-seconds .5 --multi-sampling --stop-at-end --key --highlight-users --hide mouse,progress,filenames --file-idle-time 0 --max-files 0 --background-colour 000000 --font-size 22 --title "Pretty Load" --output-ppm-stream - --output-framerate 30 | avconv -y -r 30 -f image2pipe -vcodec ppm -i - -b 65536K movie.mp4 && ffmpeg -i movie.mp4 -b:v 3048780 -vcodec libx264 -crf 24 output.mp4

Читайте так же: