Привет, друзья!
В этой статье я покажу вам, как начать разработку библиотеки компонентов с помощью 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!
ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/691338/

Добавить комментарий