Разработка Сложных Компонентов: Быстро Доставляем, Легко Поддерживаем

Всем привет, меня зовут Роман Пятаков! Я технический руководитель фронтенд-команды Lamoda. И сегодня я хочу поговорить с вами о разработке сложных компонентов.

Lamoda — технически сложный продукт, которым ежемесячно пользуются 10 миллионов пользователей и имеющий более 100 внутренних подсистем.

Верхушкой этого айсберга является интерфейс интернет-магазина, или фронтенд. Наша команда разрабатывает и поддерживает UI для десктопных и мобильных сайтов, те части нативных приложений для iOS и Android, которые сделаны на WebView, а также различные маркетинговые «надстройки» (баннеры и лендинги).

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

Нам встречаются самые разные компоненты, но есть и такие, которые по праву можно назвать сложными.

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

Около года назад на мои плечи легла задача разработки этого, казалось бы, простого компонента, что в итоге вылилось в три календарных месяца работы.

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

Сразу оговорюсь, что в нашей компании мы используем стек на базе Vue.js. Но если у вас React или Angular, эта статья все равно будет полезна, поскольку выделенные мной принципы универсальны.



Разработка сложных компонентов: быстро доставляем, легко поддерживаем

Принцип №1: «Компонент — это песочница» На этом слайде показано модальное мобильное окно до и после того, как продукт получил задание провести A/B-тестирование и улучшить UX. В новой версии модальное окно перемещается снизу и не занимает весь экран.

Пользователь может в любой момент смахнуть его и вернуться к тому месту, с которого начал.



Разработка сложных компонентов: быстро доставляем, легко поддерживаем

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

К сожалению, на GitHub не оказалось необходимой библиотеки или готового решения.

Библиотеки типа Hammer.js нам не подходят, поскольку они умеют распознавать только базовые жесты.

Но я не хотел изобретать велосипед и создавать JavaScript с нуля.

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

Swiper соответствовал требованиям задачи, так как может проводить пальцем и следовать за пальцем.

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

Я решил проявить креативность и предложил перевернуть галерею, сделав ее импровизированной из двух слайдов, занимающих весь экран.

Один слайд пустой, другой содержит модальный фрейм.

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



Разработка сложных компонентов: быстро доставляем, легко поддерживаем

Swiper — небезопасная среда для детей Казалось бы, все работает и все хорошо.

Но потом начали возникать проблемы.

У нас есть модал с окном авторизации, где пользователь авторизуется в сервисе.

Вот капча Google, которая, в свою очередь, использует очень избитый шаблон — положение: фиксированное.

Это необходимо для того, чтобы расположиться в правом нижнем углу.



Разработка сложных компонентов: быстро доставляем, легко поддерживаем

Проблема в том, что Swiper использует преобразование CSS для перемещения слайдов.

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

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

Внутри такого модального окна находится галерея, которая также использует Swiper.

Разработка сложных компонентов: быстро доставляем, легко поддерживаем

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

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

И тут я пришёл к выводу, что использовать Swiper здесь не лучшее решение.

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

Это привело к созданию различных побочных эффектов, косвенно влияющих на расположение элементов и возможность использования компонентов.



Разработка сложных компонентов: быстро доставляем, легко поддерживаем

Альтернативное решение Есть такое нативное решение, как CSS Scroll Snap. Сейчас эта технология находится в нашем проекте на стадии тестирования, но мы ожидаем, что все будет работать абсолютно прозрачно и никак не повлияет на наши дочерние компоненты.

  
  
   

.

modal { scroll-snap-type: y mandatory; } .

modalFrame, .

emptyFrame { scroll-snap-align: start; }



function onScroll(event) { if (event.target.scrollTop === 0) { // modal is closed } }

Всего несколько строк CSS и все готово.

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

Заключение: Из этого опыта я понял, что стоит убедиться, что компоненты не создают никаких помех или побочных эффектов своим дочерним компонентам.

Это иначе называется принципом ортогональности или низкой связи в программировании.

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

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

Но есть и другая сторона медали.

Такое «чистое» решение, как CSS Scroll Snap, не всегда лежит на поверхности.

Чтобы найти его, потребуется время.

Поскольку это нативное решение, оно может работать не везде.

В этом случае придется идти на компромиссы.

Например, пожертвовав такой важной функцией модального окна, как свайп.

Принцип №2: «Причудливая функция может подождать» У нас есть модальное окно фильтров, которое прикрыто другим окном с выбором цвета.

Получается, что верх полностью закрывает низ.

И только тонкая серая полоска - индикатор - показывает пользователю, что под одним модалом есть еще один.



Разработка сложных компонентов: быстро доставляем, легко поддерживаем

Да, мне удалось реализовать эту возможность, но код компонента стал намного сложнее, потому что это было необходимо:

  • Поддерживайте порядок модальных окон.

  • Узнайте высоту содержимого.

  • Создайте хранилище данных.

  • Рассчитать видимость.

А в некоторых случаях индикатор может вообще не работать.

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

Дело в том, что индикатор реализован как псевдоэлемент ::before. Фотография в галерее использует свойство overflow:hidden для обрезки верхнего края.

В результате псевдоэлемент также усекается.

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

Да, функция полезная и классная, но не критичная.

Без этого вполне можно было обойтись.

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

И в дальнейшем я решил не наступать на эти грабли.

У нас был такой случай, когда я разрабатывал полноэкранное окно — поле ввода.

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



Разработка сложных компонентов: быстро доставляем, легко поддерживаем

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

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

Да, получилось не очень хорошо, но эффективно.

Заключение: Я понял, что важно вовремя обнаружить, что функция в компоненте — это необычная функция.

Да, это круто, но его реализация ставит большие задачи.

В этом случае лучше найти компромиссное решение: упростить фичу или перенести ее на следующую итерацию.

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

Если это действительно «кровь из носа» и какую-то фичу нужно сделать здесь и сейчас, то стоит прибегнуть к подходу Git Successful Flow. В этом случае мы можем порыться в ветке разработки с кодом компонента без этой навороченной функции.

Это, по крайней мере, позволит избежать задержек в работе команды.

Принцип №3: «Не объединяйтесь и не побеждайте» Однажды прибежал наш тимлид и сказал: «Ребята, Рома сделал такое, что модал теперь может всё!» И я действительно неплохо его накачал вот так:

  • Реализована поддержка старого и нового дизайна A/B-тестов.

  • Добавлены интересные функции: индикатор перекрытия, свайп, различные анимации.

  • Появилась возможность кастомизации — поддержка разных тем для разных случаев использования.

  • Стала возможна поддержка прелоадера и виртуальных списков.

В итоге наш прокачанный модал занимает 700 строк кода, у него 15 реквизитов, 5 слотов.

На мой взгляд, получилось слишком.

И ситуация может стать еще хуже.

Дело в том, что помимо мобильного модального окна у нас есть еще и десктопное.

В наши планы входило создание адаптивного модального окна XModal, которое было бы либо мобильным, либо настольным, в зависимости от того, какой сейчас экран.

Если мы решим эту проблему, то получим компонент-монстр.

А именно:

  • К объему кода добавится еще +50%.

  • Давайте подробно разберемся с «если», так как будет добавлено множество разных условий.

  • Мы перебьем мобильные стили десктопными, что приведет к полному хаосу в CSS.
  • Будут конфликты в кодовой базе и, как следствие, могут возникнуть конфликты между командами мобильных и настольных компьютеров.

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

А также привести их API к одному типу и автоматически переключаться на нужную реализацию незаметно для родительского компонента.



Разработка сложных компонентов: быстро доставляем, легко поддерживаем

И вот как это выглядит в коде:

import MModal from ‘components/m-modal/m-modal.vue’; import DModal from ‘components/d-modal/d-modal.vue’; import deviceProps from ‘utils/device-props’; const XModal = { functional: true, render(createElement, context) { return context.parent.$createElement( deviceProps.screenSize === ‘phone’ ? MModal : DModal, context.data, context.children, ); }, };

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

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

И этот подход может пригодиться нам для A/B-тестирования или в случае нескольких сред (сайт и мобильное приложение).

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



Разработка сложных компонентов: быстро доставляем, легко поддерживаем

Таким образом, у нас есть прокси-компонент и selectorFunction, инкапсулирующий критерий, по которому мы выбираем одну из реализаций компонента.

Если кто-то еще не догадался, то это Прокси-шаблон структурного проектирования .

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

Шаблон Proxy действует как прозрачный клей, который скрепляет различные реализации компонента, оставляя клиента в неведении относительно того, как все работает. Логика компонентов существенно упрощается, не будет путаницы в стилях, а команды смогут работать самостоятельно.

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

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

Также могут возникнуть проблемы с приборами.

Например, ваша любимая IDE не сможет распознать API из-за экрана прокси и не поможет вам с автозаполнением.

выводы Поэтому не следует недооценивать разработку компонентов.

Даже примитивное модальное окно может создать серьезные проблемы с тем, как это сделать и как реализовать.

Принципы, описанные в статье, позволят вам:

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

  • Распознайте необычные функции, которые не являются критичными, но довольно сложно реализовать.

  • Если существует несколько реализаций компонента, не смешивайте все в одну кучу, а используйте прокси-шаблон, чтобы склеить части в один общий компонент.
Приверженность этим принципам помогла мне достичь главной цели – доставлять быстро, поддерживать легко.

Теги: #программирование #разработка веб-сайтов #фронтенд-разработка #vue.js #компоненты #шаблоны проектирования

Вместе с данным постом часто просматривают: