Честный 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-шаблонизатор, база данных, веб-сервер и немного логики, связывающей всё это, то сейчас «под капотом» чаще всего оказывается система из многочисленных компонентов, с хитрой системой общения и стеком различных технологий.

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

Даже удачный редизайн — это всегда, в какой-то степени, боль для пользователей, который привыкли к привычным паттернам поведения, даже если их можно сильно упростить и в них есть баги. Мы постепенно внедряли новые страницы и группами подключали пользователей к обновившемуся интерфейсу. У нас есть система обратной связи, и наши менеджеры разбирали пожелания и отзывы пользователей. Считаем, что довольно успешно справились с этой непростой задачей.
Метки:
CarPrice 23,39
Компания
Поделиться публикацией
Комментарии 30
  • +1
    А без react и redux был бы нечестный realtime?
    • +3
      На жукери тоже реалтайм бывает)
    • 0
      Маленькие скриншоты неинтересно смотреть..(
      Ссылку бы!
      • 0
        Ссылка работает только у зарегистрированных дилеров, прошедших верификацию.
        • 0
          Это очевидно.
          Просто странно рассказывать всем (а не тем самым верифицированным дилерам) о системе, которую нельзя прямо сейчас взять и посмотреть
          • 0
            Да ладно) Про palantir, например, все с удовольствием читают)
      • –1

        Я уж думал и тут будет рожа Жукова

      • +1
        второй, pod или компонентная архитектура (подход, набирающий популярность), когда всё лежит вместе и образует единый модуль.

        Интересно посмотреть на вашу реализацию этого подхода в примерах, в сравнении, увидеть момент когда компонент становится контейнером.
        • 0
          Посмотрите ссылку, которая есть в статье medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0, там очень много информации об этом, которую нельзя уложить в формат в интервью. Наш подход отличается лишь тем что, компоненты и контейнеры лежат в общем плоском списке, а не в отдельных папках т.к. чистый компонент обрастает логикой и становится контейнером довольно часто и не хочется постоянно перебрасывать их из папки в папку
          • 0

            А зачем вы их перебрасываете? Как только компонент "становится" контейнером, для него выводится отдельный контейнер без дополнительного маркапа, а сам он остается в папке с компонентами. У вас не так?

            • 0
              Не так, первый раз слышу чтобы для компонента что-то «выводилось» и зачем из него выводить «дополнительный» маркап, тоже не понял
              • 0

                Я имел в виду, что если у если у вас есть компонент <Foo/> в папке components, то для добавления в него логики не нужно его перебрасывать в папку containers. Вы создаете под него контейнер <FooContainer/> в папке containers, и этот контейнер connect-ит компонент к стору. Так?

                • 0

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

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

                      Нет, не размазывается. Код компонента в компонентах, контейнера — в контейнерах. Компоненты отвечают за UI, контейнеры — за данные для этого UI. Все на своих местах.

              • 0
                Посмотрите ссылку, которая есть в статье

                Дэн работает у вас?

                Интересна ваша реализация паттерна.
            • +2

              Очередная рекламная статья замаскированная под техническую на трендовую тему.

              • 0
                Простите, что рекламного здесь, тем более для аудитории хабра, которая вообще никак не автодилеры? Разработка получилась интересная. Сложная нестандартная задача решена эффективно. Жукова нет)
                • 0
                  Разработка получилась интересная.

                  Это да.

                  Сложная нестандартная задача решена эффективно.

                  Вы это как определили, без технических, в первую очередь, интересных деталей?
                • 0

                  все-таки технические детали есть, мне например интересно читать статьи по типу "как у них сделано?" тем более, что сейчас начинаю большой проект на реакте

                  • –1

                    Почему именно на Реакте? Вы уверены, что он подходит для больших проектов?

                    • 0

                      у нас уже есть несколько больших проектов на react, но я еще больших не делал

                • +1
                  Да, статья рекламная. Прискорбно наблюдать все это в разделе «Разработка».
                  • 0
                    Ребята молодцы! спасибо за подробную статью!
                    • +1
                      Хотелось бы узнать, как решали проблемы, когда нужно было получить информацию о сущностях по REST, зависящую друг от друга (нельзя получить параллельно). Совмещали все в одном запросе?
                      • 0
                        Запрашивали последовательно :) или делали новый хендлер на бекенде. В целом, индустрия, для решения таких проблем, движется в сторону решений типа GraphQL.
                        • 0
                          Последовательно – долго бывает, когда 3-5 запросов
                          Новый хендлер на бекенде – проблему решает, но ухудшает архитектуру бекенда
                          GraphQL – да, я тоже смотрел в сторону этого решения… но там тоже есть свои проблемы для бекенда, оптимизация запросов со связями, например

                          HTTP2 не пробовали? Мне кажется, оно бы помогло решить проблему скорости последовательных запросов
                          • 0
                            не пробовали, HTTP2 ускорит работу как раз работу с параллельными соединениями (хотя 3-5 запросов параллельно вам современный браузер и так сделает), а если запросы последовательные, нам их все равно по 1-му дергать, на любом протоколе.
                            • 0
                              Да, точно… тогда только через pub sub на веб-сокетах если, у вас они как раз используются)

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