Разрабатываем библиотеку компонентов с помощью React и TypeScript

от автора

Привет, друзья!

В этой статье я покажу вам, как начать разработку библиотеки компонентов с помощью 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/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *