Привет! Меня зовут Влад, я старший разработчик партнерских продуктов Яндекс Такси. Во фронтенд-разработке я с 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>
);
}
Порядок действий:
- Получаем стейт для нашего DateField.
- Передаем пропсы, стейт и ссылку в поведенческий хук 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. У нас есть две причины, почему мы выбрали именно этот подход:
- В Superweb UI сейчас два разработчика, которые обеспечивают двадцать продуктовых разработчиков. Если бы мы писали компоненты сами с нуля, то такой небольшой командой не смогли бы создать библиотеку.
- У Такси сквозной UI-kit, который применяется для Яндекс Go, Яндекс Про и для наших партнерских продуктов.
Итоги
Резюмируем наши четыре опции создания библиотеки компонентов.
- Написать с нуля. Отличный вариант, если есть силы, если нужен полный контроль над библиотекой, если вы готовы вкладываться в развитие. Возможно, выйдет отличная библиотека, которая вытеснит bootstrap или MUI.
- Взять библиотеку с рынка, использовать ее как есть или исправить дизайн-токенами. Дешево, но здесь мы принимаем UI-kit библиотеки и ее поведение.
- Стилизовать готовую библиотеку. Здесь нужен ресурс разработки, чтобы написать стили. Мы получим свой UI-kit, но нам всё еще нужно принять поведение библиотеки, хоть изредка мы и можем его исправить.
- Библиотека React-хуков. Здесь нужно написать не только стили, но и разметку. Зато взамен мы получаем, во-первых, поведение из коробки, а во-вторых — возможность менять это поведение, если нам это нужно.