Яндекс Такси

С какой стороны подойти к библиотеке компонентов

  • Владислав Клюев
    Владислав Клюев

    Владислав Клюев

    Старший разработчик интерфейсов в Яндекс Про

Привет! Меня зовут Влад, я старший разработчик партнерских продуктов Яндекс Такси. Во фронтенд-разработке я с 2015 года. До Яндекса работал в паре криптостартапов и в компании Netcracker.

Так получилось, что в Netcracker я пользовался библиотекой компонентов, которая была разработана в компании. А в стартапах — наоборот, использовал сторонние библиотеки. Наконец, в Яндексе я тоже работаю с компонентами, поэтому стал размышлять: есть ли та самая серебряная библиотека, которая подойдет всем? А если нет — что предпочесть в конкретном случае? Делюсь размышлениями.

Подход первый: своя библиотека

Чтобы разработать свою библиотеку, составим сперва чек-лист разработки, чтобы не потерять весь скоуп, который требуется написать. Мой чек-лист получился такой:

  • UI-kit
  • Компоненты
  • Тесты
  • Интернационализация
  • Доступность
  • Кроссплатформенность
  • Перфоманс
  • Документация
  • Демостенд/StoryBook

Этот список понадобится нам во всех подходах, поэтому разберем пункты по порядку.

UI-kit

Любая библиотека компонентов начинается с UI-kit. Это работа дизайнеров, но и разработчики, и продакты тоже должны в ней участвовать и сразу фиксировать, что может меняться, а что нет, что постоянно, а что переменно.

Разберем на примере кнопок:

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

Выше — скриншоты сервиса, который, по его собственному мнению, победил пиратство на ПК. Я насчитал здесь 11 серч-баров, 9 таб-меню, 19 дроп-даунов, а кнопки даже считать не стал. Если не хотите, чтобы ваш сервис был похож на этот скрин, пожалуйста, перед началом разработки уделите внимание UI-kit.

Компоненты

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

Все мы знаем, что нельзя просто так взять и написать календарь с первого раза. Легче, наверное, сходить в Мордор =)

Тесты

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

Интернационализация

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

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

Доступность

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

function Button({onClick, children}) {
  return (
    <div onClick={onClick} style={/*какие-то стили*/}>
      {children}
    </div>
  );
}

Код похож на код кнопки? Не совсем. Здесь использовали div и навесили onClick. Это даже может корректно работать, если заходить с компьютера и пользоваться мышкой, но мы потеряли семантику, доступность и фокус-менеджмент. Tab-ом на такую кнопку не перейдешь. Это легко исправить, заменив div на button, добавив соответствующую роль.

function Button({onClick, children}) {
  return (
    <button onClick={onClick} style={/*какие-то стили*/}>
      {children}
    </button>
  );
}

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

Кроссплатформенность

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

Минутка историй из жизни. В январе я летал отдыхать с женой и с детьми и не взял с собой ноутбук. Отдохнули хорошо, но всё-таки пришла пора регистрироваться на обратный рейс. Ввел данные — всё прекрасно. Иду по процессу, дохожу до выбора места в салоне — а выбор места со смартфона не работает. На рейс в ту сторону я регистрировался с десктопа — и всё получилось. А со смартфона — нет, тап не поддерживается. Для меня это было критично, потому что дочь очень хотела лететь у окна, и если бы она сидела в другом месте, случилась бы катастрофа. Моя личная катастрофа.

Перфоманс

После того как мы поддержали UI, компоненты, тесты, международность, доступность и разные платформы, остается риск наткнуться на проблему перфоманса. Надо следить за оптимальностью написания компонентов и за размером бандла. Еще желательно, чтобы анимации были оптимизированы, вынесены в отдельный слой и обрабатывались GPU.

Документация/Демостенд/StoryBook

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

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

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

Подход второй: сторонняя библиотека

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

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

Посмотрите на него и вспомните, что тот же Ant имеет проблемы при работе на мобильном устройстве.
  • UI-kit
  • Компоненты
  • Тесты
  • Интернационализация
  • Доступность
  • Кроссплатформенность
  • Перфоманс
  • Документация
  • Демостенд/StoryBook

Из плюсов такого подхода — быстрый старт. Мы получаем всё из коробки, если не ошиблись с выбором.

Из минусов — нам нужно принять чужой UI-kit. Чужой UI-kit — это не только цвета, которые можно поправить токенами, это отступы, тени и вообще общий визуал компонентов. А кроме того — нет удобного механизма менять поведение компонентов.

В каких случаях подойдет сторонняя библиотека?

  • Прототипы. Если вам нужно быстро написать MVP.
  • Инструменты для разработки. Если есть какие-то конфиги или настройки, которые надо редактировать QA или продуктовым менеджерам, то приятно иметь для этого простой интерфейс.
  • Простые административные панели. Отлично подойдет для внутренних интерфейсов, где нет требований к дизайну.

Подход третий: стилизация сторонней библиотеки

Чужой UI-kit — проблема для внешних интерфейсов. Мы хотим, чтобы наши кнопки не были похожи на перекрашенный Material UI или Ant Design. Мы хотим свои кнопки!

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

При этом мы готовы вложиться:

  • в разработку UI-kit;
  • в разработку адаптивных стилей;
  • в демостенд и сторибук, чтобы рассказать обо всём коллегам.

Выглядит отлично! Почему бы так всем и не делать? Есть маленькая проблема.

Слева на картинке — календарик из Material UI. Справа — наш календарь, куда дизайнеры добавили противный крестик. Как покрыть требование крестика, если у нас нет доступа к коду библиотеки? Написать компонент с нуля или какую-то обертку?

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

Плюсы стилизации — очевидно, свой UI-kit и то, что поведение мы получаем из коробки. Минусы — надо писать стили, нет удобного механизма смены поведения.

Подход четвертый: библиотека UI-примитивов

Еще один способ разработать свою библиотеку компонентов: использовать библиотеку UI-примитивов. Для начала вспомним, что такое headless-архитектура.

Headless-подход — это когда всё поведение вынесено в «тело», на сервер, а «голову» с каналами мы меняем: приложение может быть для веба, для мобилок и даже для телевизоров. Можно попробовать этот подход для компонентов: оставить поведение, которое мы хотим получить от компонента готовым, а верстку полностью написать самим.

function Button(props) {
  const { buttonProps } = useButton(props);

  return (
    <button {...buttonProps} style={/*какие-то стили*/}>
      {props.children}
    </button>
  );
}

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

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

При этом придется:

  • вложиться в UI-kit;
  • написать вместе со стилями верстку и разметку;
  • обеспечить адаптивность;
  • написать демостенд или сторибук.

Приведу пример реализации такой библиотеки от React Aria, которую можно поставить с NPM или Yarn (yarn add react-aria).

import {useButton} from 'react-aria';

function Button(props) {
  const ref = React.useRef(null);
  const { buttonProps } = useButton(props, ref);

  return (
    <button {...buttonProps} ref={ref} style={/*какие-то стили*/}>
      {props.children}
    </button>
  );
}

В этом примере мы:

  • импортируем хук useButton из пакета react-aria;
  • создаем ссылку на нашу кнопку;
  • передаем в импортированный хук наши входные пропсы и ссылку;
  • получаем пропсы, спредим эти пропсы в компонент;
  • пишем для него стили.

В чём же магия? Почему мы передаем в хук пропсы и пропсы же получаем? В полученных пропсах будут лежать все aria-атрибуты и функции, которые позволяют нашему компоненту быть доступным и кроссплатформенным. Не нужно думать о том, будет ли у тебя работать onClick.

Это простой пример, но не все компоненты такие же простые, как кнопка. Есть компоненты с состояниями — например, чекбокс.

import {useCheckbox} from 'react-aria';
import {useToggleState} from 'react-stately';

function Checkbox(props) {
  const ref = React.useRef(null);
  const state = useToggleState(props);
  const { inputProps } = useCheckbox(props, state, ref);

  return (
    <input {...inputProps} ref={ref}/>
  );
}

Для хуков с состояниями в React Aria есть отдельный пакет, react-stately. У нас всё так же есть ссылка, теперь на наш инпут. И есть хук, который возвращает наше состояние.

Мы передаем в хук пропсы, получаем стейт, далее уже в поведенческий хук передаем пропсы, стейт и ссылку, получаем inputProps и спредим его в инпут.

Как раз в этих inputProps — все aria-атрибуты, все функции, которые необходимы, чтобы чекбокс был кроссплатформенным. Просто и здорово.

Давайте возьмем реальный сложный компонент.

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

Как мы его напишем? Наш код будет состоять из двух компонентов и двух поведенческих хуков. Первый компонент — DateField. У нас есть ссылка на наше поле ввода.

function DateField(props) {
  const ref = React.useRef(null);
  const state = useDateFieldState(props);
  const { labelProps, fieldProps } = useDateField(props, state, ref);

  return (
    <div>
      <label {...labelProps}> {props.label} </label>

      <div {...fieldProps} ref={ref}>
        {state.segments.map((segment, i) => (
          <DateSegment key={i} segment={segment} state={state} />
        ))}
      </div>
    </div>
  );
}

Порядок действий:

  1. Получаем стейт для нашего DateField.
  2. Передаем пропсы, стейт и ссылку в поведенческий хук useDateField.

Обратите внимание, что мы получаем пропсы и для лейбла, и для филда. Они уже слинкованы внутри при помощи id, и нам не нужно самим писать, что этот лейбл относится к данному филду.

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

Обратите внимание, что в сегмент мы передаем и сам сегмент, и созданный стейт.

function DateSegment({ segment, state }) {
  const ref = React.useRef(null);
  const { segmentProps } = useDateSegment(segment, state, ref);

  return (
  <div {...segmentProps} ref={ref}>
    {segment.text}
  </div>
  );
}

В компоненте DateSegment мы приняли наш сегмент и стейт, создали ссылку на div, передали сегмент, стейт и реф в поведенческий хук useDateSegment и заспредили их.

Этот сегмент представляет собой простой div, но в нём находятся пропсы, которые говорят обо всём на свете: как отображать клавиатуру на мобильных устройствах, какие ограничения для читалок, какие максимальные и минимальные значения. Они позволяют обрабатывать нажатия стрелочек «вверх-вниз», чтобы управлять датой. Сегмент-разделитель будет локализован и сможет работать с RTL. Это всё из коробки.

Плюс библиотеки на готовых хуках в том, что у нас всё еще собственный UI-kit, при этом мы получаем готовое поведение, но с возможностью кастомизации. У нас есть доступ как к стейту компонента, так и к его пропсам. Если захотим, мы можем их элегантно кастомизировать. Минусы — нам всё еще нужно писать стили и разметку. Но, вроде как, мы за это зарплату получаем?

Для своей библиотеки компонентов мы выбрали именно этот подход. Все партнерские продукты Такси базируются на одной платформе Superweb, а библиотека компонентов Superweb UI — ее часть, которая основана на хуках React Aria. У нас есть две причины, почему мы выбрали именно этот подход:

  1. В Superweb UI сейчас два разработчика, которые обеспечивают двадцать продуктовых разработчиков. Если бы мы писали компоненты сами с нуля, то такой небольшой командой не смогли бы создать библиотеку.
  2. У Такси сквозной UI-kit, который применяется для Яндекс Go, Яндекс Про и для наших партнерских продуктов.

Итоги

Резюмируем наши четыре опции создания библиотеки компонентов.

  1. Написать с нуля. Отличный вариант, если есть силы, если нужен полный контроль над библиотекой, если вы готовы вкладываться в развитие. Возможно, выйдет отличная библиотека, которая вытеснит bootstrap или MUI.
  2. Взять библиотеку с рынка, использовать ее как есть или исправить дизайн-токенами. Дешево, но здесь мы принимаем UI-kit библиотеки и ее поведение.
  3. Стилизовать готовую библиотеку. Здесь нужен ресурс разработки, чтобы написать стили. Мы получим свой UI-kit, но нам всё еще нужно принять поведение библиотеки, хоть изредка мы и можем его исправить.
  4. Библиотека React-хуков. Здесь нужно написать не только стили, но и разметку. Зато взамен мы получаем, во-первых, поведение из коробки, а во-вторых — возможность менять это поведение, если нам это нужно.