Как стать автором
Обновить
140.88
hh.ru
HR Digital

Рефакторинг прайс-листа без духоты

Время на прочтение8 мин
Количество просмотров1.9K

Любой крупный проект старше пары лет имеет легаси. hh.ru здесь — не исключение. Однажды перед нашей командой встала задача перевести страницу прайс-листа работодателя на React. Сперва это занятие показалось нам абсолютно рутинным, но если бы это на самом деле было так, вы бы сейчас не читали эту статью.

Всем привет! Меня зовут Саша, я — фронтенд-разработчик команды «Монетизация» hh.ru. В своем материале расскажу, как мы рефакторили наболевшее, обнаруживали главные проблемы и находили элегантные решения.  

Погружение

Перед началом моей увлекательной истории придется немного погрузиться в предметную область. hh.ru предоставляет работодателям услуги, которые мы называем «продуктами». Примером такого продукта может служить публикация вакансий. Публикация позволяет пользователю разместить вакансию на сайте и собирать отклики. При этом продукты одной и той же категории могут отличаться в деталях: так, например, публикация вакансий типа «Стандарт» позволяет просто разместить публикацию на сайте, а типа «Стандарт Плюс» — еще и поднимает вакансию в поиске на определенное время.

Итак, история. Однажды к нам пришел владелец продукта и сказал, что нам нужно научиться легко добавлять новые продукты и безболезненно модифицировать старые. В частности, в ближайшее время придется заняться дифференциацией публикаций вакансий. Если простыми словами, то раньше, приобретая разные типы публикации вакансии, работодатель получал разные поведения вакансий в поиске, но публикацию мог совершить по любому региону. Теперь же, после добавления регионального критерия, для публикации вакансии в Москве работодателю нужен продукт с региональным критерием «Москва или Московская область» или с более широким — «вся Россия». Если работодатель захочет разместить вакансию, купленную для Москвы, скажем, в городе Орел — сделать этого не удастся.

Для разработчиков такая постановка задачи означала следующее: нужно сделать всё, чтобы появилась возможность добавлять новые атрибуты в продукт с наименьшими затратами.

Так у нас появилась цель. 

Грядут изменения

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

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

Раньше считалось вполне нормальным передать на бэкенд корзины не целый объект продукта, а только его первичный ключ.

const product = {
    code: 'PRODUCT_A',
    count: 5,
};

С добавлением новых атрибутов, вроде региональности, во всех местах на фронтенде приходилось добавлять новые поля, поскольку первичный ключ продукта менялся.

const product = {
    code: 'PRODUCT_A',
    count: 5,
    regionId: '533',
};

В новой же парадигме мы запрещаем модифицировать продукты на фронте, поэтому на бэк корзины может быть отправлен только целый объект продукта, который мы получили с сервера ранее: либо при инициализации страницы, либо при ajax походе.

const product = {
    code: 'PRODUCT_A',
    count: 5,
    currency: 'RUR',
    price: 100,
    regionId: '533',
    ...otherProductFields,
};

Глобально наша задача свелась к тому, что мы создаем полностью новые страницы, но со старым дизайном.

Как там с дизайном обстоит вопрос

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

Избавляемся от старых ссылок

Первая неприятность заключалась в том, как именно работает наш компонент вкладок.

Дело в том, что для него приходилось загружать данные всего прайс-листа в DOM-дерево. Контент каждой вкладки помещался в отдельный div с уникальным id. Затем мы использовали url-ы с якорями. То есть url вида /price#dbaccess показывал содержимое div с id dbaccess — вкладку доступов, а /price#publications — вкладку публикаций.

Разные вкладки пользуются разной популярностью у пользователей. Ситуация, когда клиент без перезагрузки страницы прокликивает последовательно все табы, может и встречается, но крайне редко. Скорее всего, такой клиент — это наш тестировщик. Поэтому, отбросив тень сомнения, мы разделили страницу прайс-листа на восемь. Каждая вкладка стала отдельной страницей. Кроме того, нам потребовалось создать еще одну страницу для обратной совместимости по адресу /price. Попадая на нее, у пользователя случается редирект на первую доступную вкладку прайс-листа. 

Небольшой совет

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

Поступательный рефакторинг

Окей, понятно как будем рефакторить, но как делать это постепенно? Одновременно переделывать бэкенд и фронтенд для корзины и контента вкладок — слишком сложно и опасно. К счастью, мы нашли способ. Дело в том, что hh.ru имеет сайты на разных доменах. Наполнение прайс-листа на разных доменах отличается: где-то вкладок восемь, а где-то всего парочка, где-то корзины нет вообще, а функциональность обвязки сильно упрощена. Поэтому мы решили взять за основу самый простой домен, перевести его на React, а затем добавлять к нему новые вкладки и функциональность обвязки. Так постепенно мы планировали дойти до самого сложного прайса – российского. 

Такой способ рефакторинга отлично сочетается с нашими динамическими настройками — так мы называем наши feature-флаги. Настройка позволяет включить или выключить некоторую функциональность без дополнительного релиза — просто через админку. В коде же это выглядит как обычный if-else. 

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

Последний вопрос перед началом рефакторинга: как отрисовывать список всех вкладок? Раньше все данные мы грузили при инициализации страницы, а теперь мы этого не делаем. Здесь нам помог бэкенд. Логику вычисления доступных пользователю продуктов, а значит и доступных вкладок, мы вынесли на сервер. У нас получилась прямая зависимость между списком вкладок и доменом, на который зашел пользователь. Эту логику мы так и назвали – доменная модель. 

Ура, все вопросы решены! Мы перевели прайс-лист на первом домене на React. У нас есть план, и мы его придерживаемся.

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

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

К счастью, ошибку получилось быстро поправить и теперь список продуктов, а значит и список вкладок, определялся по стране, к которой прикреплен работодатель, а для анонима определяющим фактором остался домен. 

Планы меняются

Бизнес никогда не стоит на месте, поэтому мы совершенно не удивились, когда к нам снова пришел владелец продукта и сказал, что мы собираемся переосмыслить вкладку публикации вакансий для России.

Старая вкладка публикаций
Старая вкладка публикаций
Новая вкладка публикаций
Новая вкладка публикаций

Проблема заключалась в том, что весь прайс-лист России до сих пор еще работал на xslt. Писать новый код на старом стеке технологий, а затем переводить его на новый, звучало как двойная работа. Поэтому мы решили написать контент новой вкладки публикаций сразу на React. К счастью, бизнес хотел изменение только там, а корзину и обвязку мы решили оставить как есть. Работает — не трогай.

Реализация такого решения на практике оказалась даже легче, чем мы думали. Было достаточно в старую страницу добавить кастомный React root, который позволит React проинициализироваться, дальше написание кода пошло своим обычным путем. Но с одной проблемой мы все-таки столкнулись.

Дело в том, что кнопка «В корзину» рисуется в React. При клике на эту кнопку объект продукта должен попасть на предобработку в старый js-компонент xslt-корзины. Короче говоря, у нас возникла проблема связи React-кода с кодом на старом стеке. К счастью, нам помог нативный механизм custom event. На стороне React достаточно задиспатчить событие, а на стороне корзины поймать его. 

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

Корзина

В былые времена наша корзина жила в localStorage браузера – дешево и сердито. Но если вспомнить о требовании про изменения продукта, картинка перестает быть такой радужной:

  • Одно малейшее изменение в структуре продукта, который лежит в localStorage и продукта, который пришел к нам c бэкенда — и ваша корзина пуста;

  • Некоторые пользователи используют один компьютер для нескольких учетных записей;

    Как мы это выяснили

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

  • Корзины не синхронизируются между разными устройствами одного и того же пользователя;

  • Корзины в localStorage тяжело поддаются исследованиям нашими аналитиками.

Всё это подтолкнуло нас к использованию таблицы базы данных вместо localStorage. Мораль этой части истории проста: если ваши данные подвержены обратной несовместимости с течением времени, то ваш бро — это базы данных.

Обвязки и подвески 

Вот мы и добрались до последней части прайс-листа — обвязки. В этом фрагменте истории на ум приходит только один интересный случай — блок «Купленные услуги».

Дело в том, что данная функциональность жила у нас годами, и мы тратили кучу времени на её поддержку. Когда мы затеяли рефакторинг прайс-листа, мы обратились к аналитикам с просьбой проверить, насколько она вообще полезна пользователям. Оказалось, что в этот блок заглядывает не более 1% клиентов. Это позволило нам не нести «Купленные услуги» в новый код на React.

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

Итоги 

Вот что мы получили в результате рефакторинга:

  • Прайс-лист теперь работает на React, команда радуется новому коду, а бизнес — приятным срокам при работе на этих страницах;

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

  • Мы удалили малоиспользуемую часть функционала, а значит, мы снизили когнитивную нагрузку на клиента;

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

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

А еще можно поглядеть на видеоверсию этой статьи по ссылке.

Где вам хочется работать 

Мы запускаем ежегодное исследование узнаваемости брендов IT-компаний на российском рынке. Каждый год изучаем, какие айтишки у нас знают лучше, и в каких хочется работать большинству специалистов. 

Пройдите этот опрос и расскажите о своих впечатлениях от сегодняшнего IT в РФ.

Ваши ответы помогут нам составить наиболее объективную картину, которой мы по традиции поделимся со всеми.

Теги:
Хабы:
Всего голосов 5: ↑4 и ↓1+3
Комментарии0

Публикации

Информация

Сайт
hh.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия