Привет! Я Леша Кузьмин, главный по фронтенду в AGIMA. Мы с коллегами решили суммировать наш опыт по подготовке UI-китов и сделать большую и внятную инструкцию для новичков. Во-первых, это удобно — будем давать эту статью нашим стажерам и падаванам. Во-вторых, нам не жалко — читайте, делитесь опытом, задавайте вопросы в комментариях.
Ниже разбираем всё с самого начала: от «зачем это вообще нужно» до «как использовать на реальном проекте». А в самом конце найдете репозиторий с фрагментами кода, которые можно использовать в своей работе. Статья для начинающих и не только начинающих Frontend-разработчиков. За помощь в ее подготовке благодарю мою коллегу Ангелину Николаеву.
![](https://habrastorage.org/getpro/habr/upload_files/c77/abb/77a/c77abb77a8f364a18aabc1e67a2482a8.png)
Что такое UI Kit и зачем он нужен
UI Kit (он же UI-кит) — набор компонентов для пользовательского интерфейса, из которых, как из кирпичиков, разработчики в дальнейшем могут построить интерфейс приложений. Понятие это используется во всех фреймворках.
Основное преимущество UI-кита — что с его помощью можно быстро менять интерфейсы сразу в нескольких приложениях, при этом используя единый источник истины. Это позволяет в долгосрочной перспективе ускорять разработку. И хотя при подготовке такой библиотеки может потребоваться дополнительное время, оно точно окупится в будущем.
Вот другие преимущества UI-китов:
Синхронизация с дизайном, например с использованием Storybook.
Более простое и быстрое тестирование компонентов, независимо от всей системы.
Более высокая безопасность приложений при обновлениях — так как всё тестируется за их пределами.
Более высокая скорость разработки продуктов: если компонент используется в нескольких системах, его нужно будет написать один раз, а далее использовать уже везде.
Далее расскажу, на какие категории мы можем разделить библиотеки и какие стратегии в зависимости от этого стоит выбирать. А затем поговорим о практическом использовании UI-китов и рассмотрим пример стартера для библиотеки.
Основа UI-кита и его категории
Создание любой библиотеки компонентов требует ясного понимания целого ряда факторов:
какие компоненты создавать;
как они будут использоваться разработчиком;
насколько поддерживаема будет вся система компонентов;
как будет устанавливаться связь между библиотекой и дизайн-системой;
оптимально ли будут подключаться компоненты;
как поддерживать библиотеку, если будет требоваться поддержка нескольких дизайн-систем одновременно и т. д.
И это только часть вопросов, с которыми непременно столкнется разработчик. Если разбивать компоненты на составляющие, можно выделить две основные части: логика и стили.
Логика. Отвечает за функциональность компонента: сюда, например, входит обработка событий (клики, фокус, управление клавиатурой и т. д.), управление состоянием компонента (открытие/закрытие, активность/неактивность и т. д.), работа с входными данными (фильтрация, сортировка и т. д.).
Стили. Это оболочка компонента. Стили визуально отражают дизайн-систему и, соответственно, могут часто изменяться и иметь большую вариативность.
Если опираться на эти две составляющие, можно разбить все UI-киты на две категории:
1. Библиотека умных «безголовых» компонентов (Headless UI). В таком UI-ките нет стилизации, все компоненты будут включать исключительно функционал.
Когда такие использовать:
когда создаете библиотеку для использования в разных проектах, в которых разные требования к стилям;
в больших проектах с несколькими командами, так как одна команда может отвечать за работу библиотеки, а другая за дизайн;
когда нужно адаптировать сайт под разные платформы — можно разделить стили.
Преимущества | Недостатки |
Можно легко интегрировать в уже существующий проект. Компоненты могут использоваться в разных дизайн-системах. Производительны за счет разделения логики и стилей. | Сложны в поддержке: требуют тщательной документации на всех этапах. Вероятность возникновения «каши» компонентов с разной стилизацией. Больше времени — больше кода. |
2. Библиотека с умными и красивыми компонентами. В такой все компоненты имеют как логику, так и стили.
Когда такие использовать:
когда библиотека будет использоваться на нескольких проектах, где важна единая дизайн-система;
когда нужно быстрое развертывание проектов клиента;
когда не нужна большая гибкость в стилизации.
Преимущества | Недостатки |
Высокая стабильность библиотеки, так как логика и стили будут протестированы сразу. Скорость и простота сборки любого проекта. Консистентность внешнего вида компонентов. | Если сборка проекта при использовании такой библиотеки быстрая, то создание и изменение компонентов будет занимать больше времени. Ограничения в возможностях кастомизации. Компоненты более тяжелые. Возможность возникновения конфликтов в стилях. |
Стратегии написания компонентов UI-кита
Можно выделить два основных подхода к разработке UI-китов:
All-in подход.
Подключение компонента вместе со стилями или без них. Здесь любой компонент — это самостоятельная готовая единица, которая уже содержит всё нужное. Внутри этого подхода можно выделить еще два подвида:
Инлайн-стили через Styled Components (возможно, добавить просто подключение стилей внутри компонента). Этот метод позволяет писать стили непосредственно в компоненте. При этом стили изолированы, что уменьшает возможность конфликтов между стилями разных компонентов.
import styled from 'styled-components';
const StyledComponent = styled.div`
/* стили компонента */
`;
const Component = () => (
<StyledComponent>
{/* ... */}
</StyledComponent>
);
export default Component;
Без добавления стилей (Headless). В этом случае компоненты предоставляют только логику без UI, что позволяет самостоятельно управлять стилями. Для создания подобной библиотеки нужно также ознакомиться с паттерном Compound component, о котором речь пойдет ниже.
const Component = () => {
return (
<>
{/* ... */}
<>
);
};
export default Component;
Dependency CSS & Bundle CSS подход.
Второй большой подход — когда стили и компонент подключаются по отдельности. В этом случае стили и логика компонента отделены друг от друга.
Dependency CSS: этот способ подключения улучшает модульность и позволяет загружать стили только тогда, когда они действительно необходимы.
Bundle CSS предполагает подключение всех стилей сразу и отдельно компонента. По сути, в этом случае все стили объединены в общий бандл и импортируются в корне проекта.
Но при написании они схожи и стили к компоненту подключаются как модули.
import styles from './component.module.css'
const Component = () => {
return (
<div className={styles.div}>
<h1 className={styles.title}>Title</h1>
{/* ... */}
</div>
);
};
export default Component;
Способы подключения библиотек
Теперь давайте разберем, как эти подходы будут выглядеть с точки зрения кода. Примеры будем приводить на базе React/Next приложения.
Начнем с того, что рассмотрим варианты, как их вообще подключают к проекту. Существует несколько способов:
git + https — достаточно простой вариант подключения библиотеки как зависимости. В этом случае ориентир по умолчанию будет на последний коммит в ветке в вашем репозитории. Из минусов: необходимо будет каждый раз устанавливать библиотеку, так как коммит фиксируется в Lock-файле и вы всегда будете получать одну и ту же версию библиотеки. Ну и про полный Push сборки тоже нельзя забывать. В таком случае мы не можем игнорировать папку Dist со сборкой.
package registry — с нашей точки зрения, более верный и привычный подход. Данную манипуляцию можно сделать в рамках вашего Git-репозитория через настройку CI/CD, информацию о котором можно легко найти в документации. Вот как, например, это можно сделать в GitLab. Но тут встает вопрос, хотите ли вы разобраться сами или у вас есть свободный девопс, который с этим поможет.
npm/yarn link — совсем костыльный метод, но он работает и позволяет локально прилинковать ваш пакет как зависимость к проекту, что позволит пользоваться им без публикации.
При использовании Headless UI, Styled Components или при импорте модульных стилей в файл компонента всё просто. Мы импортируем компонент и сразу можем его использовать. Во всех случаях предусматриваем возможность передачи класса для кастомизации стилей извне.
import { Button, Typography } from "@frontend/ui-kit";
Одним из ограничений при использовании стилей в качестве зависимости компонента может стать ваш фреймворк. Например, NextJS запрещает такой подход. Больше об этом можно прочитать в документации.
При использовании полноценной библиотеки компонентов у нас появляется два разных пути:
Импорт всех стилей в приложение одним файлом. И далее — использование компонентов в необходимых местах. Тут всё зависит от вашего фреймворка. В NextJS App Router нужно импортировать стили в корневом файле приложения. В нашем случае это _app.tsx, который лежит по пути ./src/pages, одной строкой:
import "@frontend/ui-kit/dist/style.css";
А далее, как и в предыдущем примере, можем в необходимых местах использовать схожий импорт:
import { Button, Typography } from "@frontend/ui-kit";
Если у вас SPA, это наиболее простой и, скорее всего, правильный подход для использования компонентов.
Использование SSR/SSG, когда нам не нужен весь бандл стилей. Всё тоже просто, но уже не так удобно. Первое, что необходимо сделать, это написать/найти и подключить плагин, который позволяет отделить стили от компонента без их внедрения как зависимости. Далее посмотрим, как это будет использоваться в коде. Скажу сразу, что для импорта компонентов ничего не меняется. Всё остается одной строкой:
import { Button, Typography } from "@frontend/ui-kit";
Теперь посмотрим, как подключаются стили:
import "@frontend/ui-kit/dist/Button/Button.css";
import "@frontend/ui-kit/dist/Typography/Typography.css";
Тут всё терпимо, но не так удобно, как с общим бандлом. Особенно, если учесть, что это необходимо сделать для каждого компонента, а их количество может быть очень большим. Зато экономим на размере бандла, который получит конечный пользователь приложения.
Сравнительная таблица стратегий
All-in | Dependency CSS | Bundle CSS | ||
Headless UI | Styled Components | |||
Простота разработки компонентов | + | - | + | + |
Простота использования компонентов | + | + | - | + |
Повышение сложности проекта | + | + | + | + |
Простота во внедрении нескольких дизайн-систем (?) | N/A | - | + | +/- |
Подходит для небольших проектов | + | + | + | - |
Подходит для больших проектов | + | - | + | + |
SSR friendly (?) | + | + | +/- | + |
Минимальный размер бандла | + | + | + | - |
Добавление гибкости в UI-кит
Наша практика доказывает, что для написания более гибких и функциональных библиотек нужно обратить внимание на использование следующих стилистических и не только подходов.
Первый и, возможно, самый очевидный — это использование переменных в стилях, как глобальных, так и локальных. В этом подходе ваши компоненты будут опираться на внешнюю среду, что позволяет достаточно гибко управлять их внешним видом с минимальными изменениями. Например, если вы делаете White Label Library, то стоит обратить внимание на данный вариант. Из минусов стоит отметить необходимость поддержания документации об используемых переменных в актуальном состоянии.
Второй, не менее интересный, подход — это использование именованных слоев для стилизации. В таком случае изменить стиль компонентов тоже достаточно просто, так как можно через новый слой добавить новые стили, и приоритет будет отдан именно им. А если разработчик не укажет имя нового слоя, то все стили будут размещены в слое по умолчанию, который также имеет более высокий приоритет для применения. Если вы по какой-то причине не знакомы с таким подходом, рекомендую посмотреть документацию @layer.
Третий подход — использование паттернов вашего фреймворка. Например, Compound Components — для отображения табов, аккордеонов, таблиц и т. п. Не лучший, конечно же, пример из React Bootstrap, но для понимания гибкости подойдет:
import Carousel from 'react-bootstrap/Carousel';
function UncontrolledExample() {
return (
<Carousel>
<Carousel.Item>
<Carousel.Caption>
<h3>Second slide label</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
</Carousel.Caption>
</Carousel.Item>
<Carousel.Item>
<Carousel.Caption>
<h3>Third slide label</h3>
<p>
Praesent commodo cursus magna, vel scelerisque nisl consectetur.
</p>
</Carousel.Caption>
</Carousel.Item>
</Carousel>
);
}
export default UncontrolledExample;
Разбор выбранной стратегии с реализацией (пример конфига vite-rollup)
Теперь разберем, как сделать свой UI-кит, на конкретном примере. Рассмотрим один из наших проектов. Это SPA-приложение, в котором есть личный кабинет пользователя, то есть проблем с долгой загрузкой быть не должно. Из всех перечисленных выше вариантов здесь подходит вариант со сборкой стилей в единый бандл. По стратегии, проще получить один раз все стили, чтобы не подгружать чанки в дальнейшем.
Старт разработки начинаем достаточно просто. Идем на сайт Vite и берем команду для инициализации пустого проекта на React + TypeScript. Если необходимы другие стартеры, то можете посмотреть в документации, как это сделать.
yarn create vite ui-kit --template react-ts
После выполнения команды мы получим практически пустой проект-стартер, с которым и будем работать.
Первым делом я начинаю с конфигурации package.json для ограничения версий используемых технологий, а также с настройки пакета как библиотеки.
"name": "@projectName/ui-kit",
"engines": {
"node": ">=18.18.2",
"yarn": ">=1.22.21"
},
В качестве имени "name" проекта мы используем пространство имен "name" space, в нашем случае — "@projectName". Это полезно, если собираетесь поднимать свой собственный реестр пакетов Package registry, из которого затем и устанавливать его.
Блок "engines" я тоже рекомендую указывать, чтобы закрепить версии пакетного менеджера и ноды, и гарантировать правильную работу. Далее, если CI/CD будете делать не вы сами, то это в том числе поможет избежать путаницы. Это не обязательно, но лучше один раз указать.
Из базовых настроек всё. Дальше специфические для библиотеки.
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"files": [
"dist"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./dist/style.css": "./dist/style.css",
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"sideEffects": [
"**/*.css"
]
"peerDependencies" — позволяет указать необходимые версии библиотек, которые будет поддерживать наша библиотека компонентов.
"files" — та самая папка, где будет лежать наш билд.
"main" — файл, который будет содержать набор наших компонентов для импорта.
"types" — типы для компонентов, куда без них.
"exports" — блок, в котором указываем необходимые файлы для экспорта — в дальнейшем они будут доступны для импорта в нашем приложении.
"sideEffects" — параметр, необходимый для возможности сброса лишних зависимостей или, как принято говорить, "tree shaking" при использовании в основном проекте в качестве сборщика WebPack.
Мы разобрались с первым пунктом. Дальше нам ничего не мешает написать свои компоненты и собрать их в бандл. Этого уже будет достаточно для работы с Headless UI Kit. Но если нам нужны стили для компонентов, придется добавить еще несколько строк в конфигурацию Vite. Для начала установим необходимые зависимости:
yarn add rollup vite-plugin-dts glob @vitejs/plugin-react
После этого заменим содержимое "vite.config.ts" на следующее, после чего разберем его по пунктам:
import { defineConfig } from 'vite'
import dts from "vite-plugin-dts";
import { extname, relative, resolve } from 'path'
import { fileURLToPath } from 'node:url'
import { glob } from 'glob'
import react from "@vitejs/plugin-react";
const entries = Object.fromEntries(
glob.sync('src/components/**/*.{ts,tsx}').map(file => [
relative(
'src/components',
file.slice(0, file.length - extname(file).length)
),
fileURLToPath(new URL(file, import.meta.url))
])
)
const outputBase = {
globals: {
"react": "React",
"react-dom": "ReactDOM",
"react/jsx-runtime": "jsxRuntime",
"classnames/bind": "cn",
"classnames": "classnames"
}
}
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
dts({
insertTypesEntry: true,
}),
],
define: {
'process.env': {}
},
build: {
emptyOutDir: true,
outDir: "./dist",
lib: {
name: "uikit",
entry: resolve(__dirname, "src/components/index.ts"),
},
ssr: true,
copyPublicDir: false,
// https://vitejs.dev/config/build-options.html#build-rollupoptions
rollupOptions: {
external: ["react", "react-dom", "styled-components", "classnames"],
input: entries,
output:
[
{
...outputBase,
exports: "named",
format: "cjs",
esModule: true
},
{
...outputBase,
exports: "named",
format: "esm",
interop: "esModule",
},
],
plugins: [
],
}
},
})
entries — это, по сути, список путей к нашим компонентам, который мы будем использовать далее для сборки. Он позволяет нам повторить структуру папки с компонентами в папке Dist нашей библиотеки.
outputBase — для сборки разного формата мы будем использовать настройки, и часть из них повторяется, так что выносим для более удобной управляемости.
Теперь посмотрим на сам конфиг.
plugins — плагины, которые мы подключили для сборки. У нас в базе их два: сам реакт — первый, второй — для генерации типов.
define — позволяет определять переменные, с которыми мы можем работать при сборке, так как, возможно, возникнет необходимость в использовании переменных окружения. Объявим их в этом блоке.
build — один из самых интересных разделов конфига, про который стоит поговорить отдельно, но и в нем есть и очевидные вещи.
emptyOutDir — отвечает за очистку директории для сборки.
outDir: — отвечает за имя папки для сборки.
lib — позволяет настроить режим сборки в качестве библиотеки.
name — имя библиотеки.
entry — имя входного файла для сборки.
ssr — отвечает за возможность сборки, которая будет ориентирована для SSR.
copyPublicDir — отвечает за необходимость копирования папки Public в сборку, например если нам нужны какие-то картинки.
И наконец-то добрались до чего-то полезного:
rollupOptions — отвечает за настройку сборщика rollup.
external — позволяет определить внешний зависимости для библиотеки.
input — позволяет указать файл или список файлов для сборки библиотеки. Если указываем список файлов, то в папке Dist будет повторяющаяся структура папки с компонентами. Мы именно для этого ранее сформировали массив с именами файлов и определили его как entries.
output — массив настроек форматов для конечного бандла. В нашем случае мы используем два формата: это CJS и ESM. Более детально про форматы и их настройки можно посмотреть в документации.
plugins — позволяет подключать плагины для сборщика, в нашем случае мы ничего не используем.
Итак, на данный момент мы готовы написать наши компоненты и собрать из них библиотеку. На самом деле есть куча подходов к написанию компонентов, поэтому выбирайте тот, который вам нравится. Мы выбрали module.css + clsx + sass для более гибкого написания и применения стилей.
По структуре папок мы сделали всё достаточно просто, добавили components, где дальше уже в плоской структуре лежат наши компоненты.
Важно, что в корне папки с компонентами нам необходимо добавить файл index.ts, в который мы будем добавлять наши компоненты, а он в свою очередь будет также являться точкой входа для сборщика.
Небольшой пример содержимого данного файла:
import { Checkbox } from "./checkbox/checkbox";
import { Form } from "./form/form";
import { Radio } from "./radio/radio";
import Input from "./input/input";
import SearchInput from "./search-input/search-input";
import PasswordInput from "./password-input/password-input";
export {
Checkbox,
Form,
Radio,
Input,
SearchInput,
PasswordInput,
};
Давайте посмотрим на пример блока "scripts" в нашем проекте:
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write ./src",
"prepare": "husky",
"bump": "npm version patch -m \"UI Kit version updated to v%s\"",
"bump:minor": "npm version minor -m \"UI Kit version updated to v%s\"",
"bump:major": "npm version major -m \"UI Kit version updated to v%s\""
},
Чаще всего мы будем использовать две команды. Первая — yarn bump, которая через NPM изменяет версию нашей библиотеки. Вторая — build, которая собирает бандл библиотеки.
Если имеем дело с реестром пакетов, настраиваем в CI/CD, но не забываем и про husky, который тоже должен сделать билд, чтобы мы не запушили нерабочую версию.
dev — ничего нового, это режим для разработки.
Также было бы неплохо использовать линтеры и форматеры. Кроме того, мы можем накручивать не только фикс-версию, но и минорную и мажорную версию "bump:minor" и "bump:major" соответственно.
Пример стартера UI-кита для React/Next JS можно найти в репозитории.
Как используем всё это в реальном проекте
В нашей практике было несколько проектов, когда мы строили собственные CMS-системы. Как правило, в этом случае мы применяем подход UI Kit + Headless CMS или пишем отдельные админки для API, которые позволяют быстро собирать пользовательский интерфейс из предсказуемого списка компонентов. Это как минимум снижает шансы на получение неконсистентного интерфейса. Как мы связываем UI Kit и Headless CMS, рассказали в отдельной статье.
Давайте рассмотрим еще один пример из личного опыта. Компания-заказчик была представлена в нескольких странах, не менее 20 регионов. В какой-то момент у бизнеса появилось задача запускать эксперименты по регистрации пользователей на платформе каждые две недели, причем с новым интерфейсом.
Это сложно сделать, когда у тебя одно монолитное приложение. Поэтому мы пришли к варианту разделения приложения на несколько частей, одна из которых отвечает за компоненты — по сути, UI-кит, другая — за бизнес-логику и взаимодействие с бэкендом, а последняя — это шаблон-стартер для быстрой возможности собрать бандл из компонентов и утилит предыдущих двух.Соответственно, при необходимости изменений в 99% случаев нам требовалось только добавить новые версии компонентов и собрать новое приложение. Это делается быстро, за один спринт длиною в две недели мы успевали всё сделать, протестировать и задеплоить.
Скорее всего, «смягчающим обстоятельством» было соблюдение дизайна и по возможности сохранение узнаваемости, но никто не мешает вам написать компоненты болванки, на которые можно так же быстро набросать стили и получить готовое решение.
Если пойти дальше по данной стратегии, то можно выделить разработчика в команду дизайна, который сможет готовить компоненты для внешних команд разработки. Это не только поможет ускорить процесс, но и усилит независимость команды от внешних факторов и контроль за качеством разработки презентационных компонентов и соответствием единой дизайн-системе.
В заключение отмечу, что не стоит бояться писать собственный UI-кит, так как в большинстве случаев у всех компаний уникальный дизайн, а умение создавать собственную библиотеку будет большим плюсом. Правда, это потребует некоторый усилий с вашей стороны.
Также немаловажный фактор — это время. Придется его потратить для детальной проработки компонентов, чтобы учесть все необходимые варианты поведения и возможностей взаимодействия. Насколько я помню, для разделения приложения на несколько частей мы потратили около двух недель разработки. Но стоит только начать и закрепить правила, перестать писать компоненты в рамках проекта и сразу выносить их в UI-кит, как дело пойдет намного быстрее.
Главное — помнить, что нужно постепенно наполнять библиотеку компонентами, а не требовать быстрого переноса всего и сразу. Берем и постепенно рефакторим, никуда не торопимся, чтобы получить готовый набор.
Если у вас остались вопросы, задавайте их в комментариях. Постараемся на все ответить. Спасибо за внимание!