Привет, друзья!
В этой статье я покажу вам, как начать разработку библиотеки компонентов с помощью Vite, React, TypeScript и Storybook.
Мы разработаем библиотеку, состоящую из одного простого компонента — кнопки, подготовим библиотеку к публикации в реестре npm, а также сгенерируем и визуализируем документацию для кнопки.
Репозиторий с кодом проекта.
Если вам это интересно, прошу под кат.
Подготовка и настройка проекта
Создаем шаблон проекта с помощью Vite
:
# npm 7+
# react-ts-lib - название проекта
# react-ts - используемый шаблон
npm create vite react-ts-lib -- --template react-ts
Переходим в созданную директорию, устанавливаем зависимости и запускаем сервер для разработки:
cd react-ts-lib
npm i
npm run dev
Приводим директорию к следующей структуре:
- src
- lib
- Button
- Button.tsx
- index.ts
- App.tsx
- index.css
- vite.config.ts
- ...
Устанавливаем библиотеку styled-components (мы будем использовать эту библиотеку для стилизации кнопки) и типы для нее:
npm i styled-componets
npm i -D @types/styled-components
Устанавливаем плагин Vite
для автоматической генерации файла с определениями типов:
npm i -D vite-plugin-dts
Настраиваем сборку, редактируя файл vite.config.ts
:
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
import path from "path";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [
// поддержка синтаксиса React (JSX и прочее)
react(),
// генерация файла `index.d.ts`
dts({
insertTypesEntry: true,
}),
],
build: {
lib: {
// путь к основному файлу библиотеки
entry: path.resolve(__dirname, "src/lib/index.ts"),
// название библиотеки
name: "ReactTSLib",
// форматы генерируемых файлов
formats: ["es", "umd"],
// названия генерируемых файлов
fileName: (format) => `react-ts-lib.${format}.js`,
},
// https://vitejs.dev/config/build-options.html#build-rollupoptions
rollupOptions: {
external: ["react", "react-dom", "styled-components"],
output: {
globals: {
react: "React",
"react-dom": "ReactDOM",
"styled-components": "styled",
},
},
},
},
});
Разработка компонента
Определяем минимальные стили и несколько переменных в файле index.css
:
/* импортируем шрифт */
@import url("https://fonts.googleapis.com/css2?family=Montserrat&display=swap");
/* определяем переменные */
/* палитра `Bootstrap` */
:root {
--primary: #0275d8;
--success: #5cb85c;
--warning: #f0ad4e;
--danger: #d9534f;
--light: #f7f7f7;
--dark: #292b2c;
--gray: rgb(155, 155, 155);
}
/* "легкий" сброс стилей */
*,
*::before,
*::after {
box-sizing: border-box;
font-family: "Montserrat", sans-serif;
margin: 0;
padding: 0;
}
/* выравнивание по центру */
#root {
align-items: center;
display: flex;
gap: 0.6rem;
height: 100vh;
justify-content: center;
}
Приступаем к разработке кнопки.
Работаем с файлом src/lib/Button/Button.tsx
.
Импортируем зависимости:
import {
ButtonHTMLAttributes,
FC,
MouseEventHandler,
PropsWithChildren,
} from "react";
import styled from "styled-components";
Определяем перечисление с вариантами кнопки:
export enum BUTTON_VARIANTS {
PRIMARY = "primary",
SUCCESS = "success",
WARNING = "warning",
DANGER = "danger",
}
Определяем типы пропов:
type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: BUTTON_VARIANTS;
onClick?: MouseEventHandler<HTMLButtonElement>;
};
Кроме стандартных атрибутов, кнопка принимает 2 пропа:
variant
— вариант кнопки (primary
и др.);onClick
— обработчик нажатия кнопки.
Определяем компонент кнопки:
const Button: FC<PropsWithChildren<Props>> = ({
children,
disabled,
onClick,
variant = BUTTON_VARIANTS.PRIMARY,
...restProps
}) => {
// если кнопка заблокирована, переданный обработчик не вызывается
const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => {
if (disabled) return;
onClick && onClick(e);
};
return (
<button disabled={disabled} onClick={handleClick} {...restProps}>
{children}
</button>
);
};
Определяем стилизованную кнопку с помощью styled
:
const StyledButton = styled(Button)`
background-color: var(
--${(props) => (props.disabled ? "gray" : props.variant ?? "primary")}
);
border-radius: 6px;
border: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
color: var(
${(props) =>
props.variant &&
(props.variant === BUTTON_VARIANTS.SUCCESS ??
props.variant === BUTTON_VARIANTS.WARNING)
? "--dark"
: "--light"}
);
cursor: ${(props) => (props.disabled ? "default" : "pointer")};
font-weight: 600;
letter-spacing: 1px;
opacity: ${(props) => (props.disabled ? "0.6" : "1")};
outline: none;
padding: 0.8rem;
text-transform: uppercase;
transition: 0.4s;
&:not([disabled]):hover {
opacity: 0.8;
}
&:active {
box-shadow: none;
}
`;
Здесь хочется отметить 2 момента:
background-color: var(--${(props) => (props.disabled ? "gray" : props.variant ?? "primary")});
означает, что фоновый цвет зависит от варианта кнопки и определяется с помощью переменных, объявленных вindex.css
. Фон заблокированной кнопки —--gray
илиrgb(155, 155, 155)
, дефолтный фон —--primary
или#0275d8
;- это:
color: var(
${(props) =>
props.variant &&
(props.variant === BUTTON_VARIANTS.SUCCESS ??
props.variant === BUTTON_VARIANTS.WARNING)
? "--dark"
: "--light"}
);
означает, что цвет текста также зависит от варианта кнопки и определяется с помощью переменных CSS
. Цвет текста кнопки успеха или предупреждения — --dark
или #292b2c
, цвет остальных кнопок — --light
или #f7f7f7
.
Полагаю, остальные стили вопросов не вызывают.
Повторно экспортируем кнопку и перечисление в файле src/lib/index.ts
:
export { default as Button, BUTTON_VARIANTS } from "./Button/Button";
Посмотрим, как выглядит и работает наша кнопка.
Редактируем файл App.tsx
:
import { Button, BUTTON_VARIANTS } from "./lib";
function App() {
// обработчик нажатия кнопки
// принимает вариант кнопки
const onClick = (variant: string) => {
// выводим сообщение в консоль инструментов разработчика в браузере
console.log(`${variant} button clicked`);
};
return (
<>
{/* дефолтная кнопка */}
<Button onClick={() => onClick("primary")}>primary</Button>
{/* заблокированная кнопка */}
<Button onClick={() => onClick("disabled")} disabled>
disabled
</Button>
{/* успех */}
<Button
variant={BUTTON_VARIANTS.SUCCESS}
onClick={() => onClick(BUTTON_VARIANTS.SUCCESS)}
>
{BUTTON_VARIANTS.SUCCESS}
</Button>
{/* предупреждение */}
<Button
variant={BUTTON_VARIANTS.WARNING}
onClick={() => onClick(BUTTON_VARIANTS.WARNING)}
>
{BUTTON_VARIANTS.WARNING}
</Button>
{/* опасность */}
<Button
variant={BUTTON_VARIANTS.DANGER}
onClick={() => onClick(BUTTON_VARIANTS.DANGER)}
>
{BUTTON_VARIANTS.DANGER}
</Button>
</>
);
}
export default App;
Запускаем сервер для разработки с помощью команды npm run dev
:
Сборка и публикация пакета
Редактируем файл package.json
, определяя в нем название пакета (наш пакет будет иметь scope с оригинальным названием @my-scope
(в данном случае префикс @
является обязательным)), его версию, лицензию, директорию с файлами, файл с типами, а также настраивая экспорты (разделы scripts
, dependencies
и devDependencies
опущены):
{
"name": "@my-scope/react-ts-lib",
"version": "0.0.0",
"license": "MIT",
"files": [
"dist"
],
"main": "./dist/react-ts-lib.umd.js",
"module": "./dist/react-ts-lib.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/react-ts-lib.es.js",
"require": "./dist/react-ts-lib.umd.js"
}
}
}
Пример package.json
(с дополнительными полями) реальной библиотеки можно найти здесь.
Обратите внимание: перед сборкой имеет смысл "чистить" package.json
.
Устанавливаем пакет json в качестве зависимости для разработки:
npm i -D json
И определяем в разделе scripts
следующую команду:
"prepack": "json -f package.json -I -e \"delete this.devDependencies; delete this.dependencies\"",
Выполняем сборку с помощью команды npm run build
:
Это приводит к генерации директории dist
с файлами библиотеки.
Для локального тестирования библиотеки необходимо сделать следующее:
- находясь в корневой директории проекта, выполняем команду
npm link
для создания символической ссылки. Эта команда приводит к добавлению пакета в глобальную директориюnode_modules
. Список глобально установленных пакетов можно получить с помощью командыnpm -g list --depth 0
:
- находясь в корневой директории (или любой другой), выполняем команду
npm link @my-scope/react-ts-lib
для привязки пакета к проекту.
Редактируем импорт в файле App.tsx
:
import { Button, BUTTON_VARIANTS } from "@my-scope/react-ts-lib";
И запускаем сервер для разработки с помощью команды npm run dev
:
Обратите внимание: после локального тестирования пакета необходимо выполнить 2 команды:
npm unlink @my-scope/react-ts-lib
для того, чтобы отвязать пакет от проекта;npm -g rm @my-scope/react-ts-lib
для удаления пакета изnode_modules
на глобальном уровне.
Для публикации пакета в реестре npm
необходимо сделать следующее:
- создаем аккаунт npm;
- авторизуемся с помощью команды
npm login
; - публикуем пакет с помощью команды
npm publish
.
Список опубликованных пакетов можно увидеть на странице своего профиля (в моем случае — это https://www.npmjs.com/~igor_agapov):
Генерация и визуализация документации
Устанавливаем пакет @storybook/builder-vite
в качестве зависимости для разработки:
npm i -D @storybook/builder-vite
И инициализируем Storybook
с помощью следующей команды:
npx sb init --builder @storybook/builder-vite
Это приводит к генерации директории .storybook
. Убедитесь, что файл main.js
в этой директории имеет следующий вид:
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions"
],
"framework": "@storybook/react",
"core": {
"builder": "@storybook/builder-vite"
},
"features": {
"storyStoreV7": true
}
}
Создаем в корневой директории файл .npmrc
следующего содержания:
legacy-peer-deps=true
Создаем файл src/lib/Button/Button.stories.tsx
следующего содержания:
import { ComponentMeta, ComponentStoryObj } from "@storybook/react";
import Button, { BUTTON_VARIANTS } from "./Button";
// импортируем стили
import "../../index.css";
// описание компонента и ссылка на него
const meta: ComponentMeta<typeof Button> = {
title: "Design System/Button",
component: Button,
};
export default meta;
// истории
// дефолтная кнопка
export const Default: ComponentStoryObj<typeof Button> = {
args: {
children: "primary",
},
};
// заблокированная кнопка
export const Disabled: ComponentStoryObj<typeof Button> = {
args: {
children: "disabled",
disabled: true,
},
};
// успех
export const SuccessVariant: ComponentStoryObj<typeof Button> = {
args: {
children: "success",
variant: BUTTON_VARIANTS.SUCCESS,
},
};
// кнопка с обработчиком нажатия
export const WithClickHandler: ComponentStoryObj<typeof Button> = {
args: {
children: "click me",
onClick: () => alert("button clicked"),
},
};
Выполняем команду npm run storybook
:
Пожалуй, это все, о чем я хотел рассказать в этой статье.
Надеюсь, вы узнали что-то новое и не зря потратили время.
Благодарю за внимание и happy coding!