Честный realtime на React и Redux, как основа автоаукциона
В нашей первой статье о программной инфраструктуре сервиса CarPrice, — если не читали, то рекомендуем почитать, — упоминалось про сайт для дилеров. Что он собой представляет и как устроен, мы попросили рассказать одного из его разработчиков, Никиту Лебедева.
Никита, расскажи, пожалуйста, о чём пойдёт речь.
Для тех, кто не знает, — CarPrice создал аукционный сервис по продаже частными лицами своих подержанных автомобилей автодилерам. Владелец авто приезжает в один из наших многочисленных филиалов, наш сотрудник проводит инспекцию его машины, составляет карточку, которая выставляется на дилерский аукцион. За этот автомобиль в течение получаса торгуются дилеры, постепенно повышая цену. По завершении аукциона владелец решает, устраивает ли его предложенная сумма, и либо уезжает с деньгами, либо спокойно отказывается от продажи.
Недавно мы зарелизили новую версию сайта, на котором проходят аукционы для дилеров. Пользователь может просматривать предложения, одновременно торговаться за несколько автомобилей, делая ставки в различных лотах, словно перемещаясь между игровыми столами — поэтому внутри нашей компании за этим проектом прочно закрепилось неофициальное название Poker Stars.
Что изменилось в новом релизе?
Раньше всё было разбросано по отдельным вкладкам, пользователям приходилось всё время переходить между ними. Теперь это полноценное одностраничное веб-приложение, все блоки информации помещаются на одном экране.
При нажатии на любой из аукционов открывается карточка автомобиля:
Здесь в компактной и наглядной форме выведена вся информация о машине, занесённая нашим инспектором при осмотре. Показана схематическая развёртка кузова, на которой разными цветами показано состояние тех или иных элементов. Здесь отмечается, какие детали крашеные, поврежденные или сильно поврежденные. В галерее есть общие фотографии автомобиля, фотографии повреждений, видеоролики, на которых можно послушать звук стартёра и работающего мотора. Дальше идёт подробнейшая форма со всевозможными характеристиками машины.
Также мы используем несколько сторонних сервисов и баз данных, которые помогают узнать, использовалась ли машина в такси, каков её реальный пробег, и прочую информацию, которая поможет принять решение при покупке.
Расскажи поподробнее о технической стороне проекта.
Это веб-приложение, написанное на React/Redux и соответствующее парадигме этой технологии: оно состоит из отдельных функциональных компонентов. Даже пользовательский интерфейс собирается из отдельных карточек-компонентов. По сути, каждая карточка — это отдельное приложение, работающее само по себе. Его можно безболезненно извлечь и вставить в другое место общего приложения. То есть что-то вроде микросервисной архитектуры, только в клиентской части и на уровне пользовательского интерфейса.
Всё это работает абсолютно независимо от серверной части. То есть аукционное приложение состоит из статических файлов, которые собираются WebPack’ом и потом раздаются Nginx.
Какова схема взаимодействия фронтенда и бэкенда приложения? Какие модули с какими системами общаются, какие данные передают?
Мы используем комплексный подход, в приложении у нас есть и общение по HTTP REST API, и дуплексное real time-общение по WebSocket (pub/sub шаблон). Это обусловлено как историческими факторами (приложение работало так изначально), так и функциональными требованиями (у нас есть места, где real time и передача событий с сервера не нужна, и наоборот). Нам очень важно было реализовать именно честный real-time, так как это аукцион, и у нас бывают интенсивные торги, особенно на последних минутах. Мы отказались от схем, когда обновления передаются с сервера раз в какое-то время (например, в секунду, как это часто делают в мессенжерах), у нас клиент получает обновления с сервера так быстро, как это возможно. Очень важно было сохранить удобство интерфейса: когда за автомобиль начинается активная торговля, события летят очень быстро и пользователь уже не успевает вводить новую ставку вручную. Поля и кнопки (у нас есть возможность торгов из разных мест в приложении, и вид ставки бывает разный) сами обновляют информацию при получении событий с сервера, или, например, могут становится неактивными, если у дилера нет прав продолжать дальше торги. Это существенно упрощает работу с интерфейсом, но было не таким простым в реализации, потому что при таких сценариях связанность компонентов сильно возрастает.
Расскажи поподробнее об используемом при разработке инструментарии.
Мы сознательно отказались от «велосипедов» в пользу самых популярных решений для веб-приложений, которые сейчас есть на рынке. Как фреймворк, идеология и основа используется React/Redux. Проект собирается с помощью Webpack, мы используем Yarn как менеджер зависимостей, NodeJS и Express для dev/mock-сервера. В production статику раздает докеризованный Nginx.
Нельзя сказать, что этот стек абсолютно лишён недостатков, но без преимуществ, которые он даёт из коробки (реактивность, гибкий дебаггинг, хорошая инкапсуляция и переиспользование компонентов), трудно построить приложение, которое отвечает современным стандартам и требованиям (богатый интерактивный интерфейс с большим количеством информации на одном экране и возможностью real-time обновлений).
Использовались ли компоненты предыдущего приложения, или новое было создано с нуля?
Изначально у нас было одно большое монолитное приложение, мы постепенно переписываем из него критичные и сильно разрастающиеся модули. Сейчас в production работают и монолитное приложение, и новые сервисы и фронтенды в связке. Что касается интерфейса, то он сильно меняется в процессе редизайна, и взаимодействие старых и новых страниц — всегда сложная задача, каждый раз требующая уникального решения.
А были ли «отсеяны» какие-то технологии или инструменты, которые были признаны неподходящими для использования в приложении?
Наша архитектура позволяет довольно гибко работать с разными инструментами, не нарушая главных принципов приложения. Например, мы экспериментировали с CSS-модулями и БЭМ-именованием CSS-классов с использованием LESS. В результате выбрали первое как более удобный вариант. Тоже самое было с реализацией горячей перезагрузки на dev-сервере, технология ещё не до конца устоялась, и мы пробовали несколько вариантов реализации.
Что было самым трудным в реализации этого приложения?
Приложение имплементировало довольно много поведенческой логики, часто нетривиальной. Наша задача была грамотно разложить её по модулям, предусмотреть все взаимодействия с пользователем, продумать все сценарии использования. Когда все компоненты сильно связаны между собой и очень активно взаимодействуют друг с другом, важно предусмотреть, к чему проводит каждое действие пользователя (или других пользователей), какое влияние оказывает на приложение. В клиенсткой части в этом нам сильно помогла Redux-архитектура. Наш аукцион — это real time-экосистема, и важно было, чтобы те события, которые прилетают от других пользователей, не вступали в конфликт с теми, которые генерирует сам пользователь.
Насколько я понял, главное преимущество нового приложения — его модульный интерфейс. Ты мог бы рассказать подробнее о его разработке?
Сегодня существует два популярных подхода к разделению приложения на модули:
- первый, MVC-подход (довольно известен), когда все функциональные роли (модели, контроллеры, вьюхи) располагаются в соответствующих папках. И чтобы проследить весь жизненный цикл модуля, нужно из каждой папки взять, соответственно, его модель, контроллер и вьюху (всё может быть опционально).
- второй, pod или компонентная архитектура (подход, набирающий популярность), когда всё лежит вместе и образует единый модуль. Туда же складывается всё то, что относится к модулю (статические файлы, графические изображения, видео).
В нашем аукционном приложении мы используем второй подход. Наш типичный модуль представляет собой React-вьюшку, CSS-файл, Redux action, Redux reducer + статические файлы. Мы не разделяем контейнеры и компоненты, компонент сам становится контейнером, если у него появляется необходимость в action'e и reducer'e.
В чём отличие контейнера от компонента, почему вы проигнорировали этот паттерн?
Контейнер — это компонент, который работает со stor'ом напрямую, делает запросы на сервер, и так далее. А просто компонент умеет только принимать данные от родителя и рендериться в определенное место. Часто их разделяют по разным папкам на уровне приложения. Нам такой подход показался избыточным, я думаю, плоский список компонентов — это то, к чему шла вся фронтенд-разработка последние лет пять. Разработчик сам понимает, что компонент усложняется (становится контейнером), и это видно при открытии входной точки компонента, так как для этого нужно имплементировать несколько методов Redux'а.
Я слышал что Redux использует практики функционального программирования для построения приложения, это так? А как же ООП?
Да, Redux писался под влиянием функциональный языков, подходов и идей. Например, все reducer'ы — это чистые функции, а store — это неизменяемый объект. Все наборы action'ов и reducer'ов — просто функции, которые мы импортируем во входные точки компонента, никаких классов, инстансов практически нет (только в React'е и самописных модулях). ООП-подходы хорошо справляются с постоянно разбухающей логикой, но в данном случае удается сохранить приложение компактным, несмотря на то, что оно несет много функциональности.
Переиспользование компонентов и изоляция — одно из главных преимуществ такого подхода.
В этом очень сильно помогают новые версии JavaScript, поддержку которых активно добавляют в браузеры и NodeJS. Это всё ещё не полностью функциональный язык, и вероятно никогда им не будет. Но нововведения, сахар и общая мультипарадигменность языка позволяют сильно уменьшить боль при написании фронтендов и создавать такие платформы, как React/Redux.
Какой инструментарий используется в работе?
Redux DevTools и React Developer Tools добавляются в Chrome к стандартным инструментам разработчиков. Первый — очень мощный инструмент отладки, позволяет просматривать store, откатывать action'ы и, соответственно, то, что делалось в приложении по времени (он так и называется — машина времени). Второй позволяет работать с XML-подобным деревом React-компонентов, похожим на то, что мы пишем в JSX, а не с привычным DOM'ом. Это позволяет оперировать более крупными частями приложения, чем простые HTML-элементы. Также мы использует ESLint со стандартным, немного измененным AirBnb-конфигом, чтобы привести код разных разработчиков к примерно одинаковому виду.
Какой путь проходит приложение от разработки какой-то новой функциональности до её релиза пользователям?
Разработчик развёртывает приложение у себя локально, запускает Webpack Dev-сервер и сервер с моками на разных портах (или настраивает конфиги на production или staging-окружение) и начинает работу. После прохождения код-ревью, тестировщик в CI собирает себе Docker-контейнер с нужной веткой, и проверяет. Далее запускается Drone (о нём мы скоро расскажем в отдельном посте), CI собирает контейнер для production и запускает его на боевом сервере. В дальнейшем мы планируем собирать один контейнер и для тестирования, и для развёртывания.
Как за последние 2-3 года изменился подход к разработке фронтенда и бэкенда? Какие идеи/концепции, ранее считавшиеся нормальной практикой, ты сегодня оцениваешь как устаревшие, и что пришло на их место? Какие методики вы взяли на вооружение, и, быть может, использовали при создании этого приложения?
Я бы охватил больший период. За последние 4-6 лет требования к интерфейсам в вебе серьёзно возросли, в большинстве новых сложных продуктов основную версию делают для браузера, часть старых тоже мигрировала. Приложения стали значительно сложнее, и монолитная архитектура, которая раньше превалировала, уступает место микросервисам на бекенде и компонентам на фронтенде. Бекенд и фронтенд в типичном большом проекте сначала отделились друг от друга, уменьшая сложность и энтропию, а потом начали делить зоны ответственности внутри себя. Если раньше, зайдя даже на популярный посещаемый сайт, можно было понять, что внутри работает простой строчный HTML-шаблонизатор, база данных, веб-сервер и немного логики, связывающей всё это, то сейчас «под капотом» чаще всего оказывается система из многочисленных компонентов, с хитрой системой общения и стеком различных технологий.
И последний вопрос. Покупка автомобиля — это довольно ответственный шаг даже для людей, которые занимаются этим профессионально. Как дилеры отреагировали на новый интерфейс?
Даже удачный редизайн — это всегда, в какой-то степени, боль для пользователей, который привыкли к привычным паттернам поведения, даже если их можно сильно упростить и в них есть баги. Мы постепенно внедряли новые страницы и группами подключали пользователей к обновившемуся интерфейсу. У нас есть система обратной связи, и наши менеджеры разбирали пожелания и отзывы пользователей. Считаем, что довольно успешно справились с этой непростой задачей.