Как отделить UIkit от основного продукта?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как отделить UIkit от основного продукта
Отделение UIkit (компонентной библиотеки) от основного приложения — это важная архитектурная задача для масштабируемости. Давайте разберём стратегии.
Что такое UIkit
UIkit — это набор переиспользуемых компонентов (Button, Input, Card, Modal и т.д.), которые используются на всех страницах приложения. Отделить их означает:
- Независимое развитие и версионирование
- Переиспользование в других проектах
- Снижение связанности (coupling)
- Четкий контракт между UIkit и приложением
Способ 1: Отдельная папка (для одного проекта)
Подходит, если UIkit используется только в одном проекте, но нужна четкая граница.
Структура:
project/
├── src/
│ ├── uikit/ <-- Отдельная папка для компонентов
│ │ ├── Button/
│ │ ├── Input/
│ │ ├── Card/
│ │ └── index.ts <-- Экспорт всех компонентов
│ ├── features/ <-- Специфичные для приложения
│ │ ├── auth/
│ │ ├── profile/
│ │ └── dashboard/
│ └── App.tsx
Файл uikit/index.ts:
// Единая точка входа для всех UI компонентов
export { Button } from './Button';
export { Input } from './Input';
export { Card } from './Card';
export { Modal } from './Modal';
export { Checkbox } from './Checkbox';
// Типы
export type { ButtonProps } from './Button';
export type { InputProps } from './Input';
Использование в приложении:
// Импортируем только из uikit, не из внутренних папок
import { Button, Input, Card } from '@/uikit';
function LoginPage() {
return (
<Card>
<Input placeholder="Email" />
<Input type="password" placeholder="Password" />
<Button variant="primary">Login</Button>
</Card>
);
}
Преимущества:
- Простая в реализации
- Единое место для всех компонентов
- Легко найти и обновлять
Недостатки:
- Сложнее переиспользовать в других проектах
- UIkit растет вместе с основным проектом
Способ 2: Monorepo с разными пакетами
Подходит, если нужна полная независимость UIkit для переиспользования в разных проектах.
Структура (pnpm workspaces или yarn):
monorepo/
├── packages/
│ ├── ui-kit/ <-- NPM пакет с компонентами
│ │ ├── src/
│ │ │ ├── Button/
│ │ │ ├── Input/
│ │ │ └── index.ts
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── .storybook/
│ ├── app-main/ <-- Основное приложение
│ │ ├── src/
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── app-admin/ <-- Другое приложение
│ ├── src/
│ └── package.json
└── pnpm-workspace.yaml
packages/ui-kit/package.json:
{
"name": "@mycompany/ui-kit",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./styles": "./dist/styles.css"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
Использование в приложении:
// Импортируем из отдельного пакета
import { Button, Input, Card } from '@mycompany/ui-kit';
import '@mycompany/ui-kit/styles'; // Стили компонентов
function LoginPage() {
return (
<Card>
<Input placeholder="Email" />
<Button>Login</Button>
</Card>
);
}
Преимущества:
- UIkit полностью независим
- Можно версионировать отдельно
- Переиспользуем в разных проектах
- Четкий контракт через package.json
- Можно развивать параллельно
Недостатки:
- Сложнее в настройке
- Нужна система управления версиями
- CI/CD должна уметь собирать пакеты
Способ 3: Отдельный npm пакет (публичный или приватный)
Самый чистый способ, если нужна максимальная независимость.
Отдельный репозиторий:
ui-kit-repo/
├── src/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.styles.ts
│ │ ├── Button.test.tsx
│ │ └── index.ts
│ └── index.ts
├── package.json
├── tsconfig.json
├── .storybook/
│ └── main.ts <-- Документация компонентов
└── README.md
Подключение через package.json:
{
"dependencies": {
"@mycompany/ui-kit": "^1.2.0"
}
}
Или для приватного пакета:
{
"dependencies": {
"@mycompany/ui-kit": "github:mycompany/ui-kit#main"
}
}
Архитектурные правила для UIkit
1. UIkit не должен знать о features приложения
// ПЛОХО: Button зависит от auth features
import { useAuthStore } from '@/features/auth';
export function Button(props) {
const { isLoggedIn } = useAuthStore();
// UIkit не должен знать о auth
}
// ХОРОШО: Button принимает всё через props
export interface ButtonProps {
onClick?: () => void;
disabled?: boolean;
children: React.ReactNode;
variant?: 'primary' | 'secondary';
}
export function Button({ onClick, children, ...props }: ButtonProps) {
return <button onClick={onClick} {...props}>{children}</button>;
}
2. UIkit определяет только UI, не бизнес-логику
// ПЛОХО: UIkit контролирует валидацию (бизнес-логика)
function Input(props) {
const [error, setError] = useState('');
const handleChange = (e) => {
if (!isValidEmail(e.target.value)) {
setError('Invalid email'); // Это бизнес-логика!
}
};
}
// ХОРОШО: Компонент просто отображает ошибку
function Input({ error, ...props }: InputProps) {
return (
<div>
<input {...props} />
{error && <span className="error">{error}</span>}
</div>
);
}
// Валидация в features/auth
function LoginForm() {
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState('');
const handleEmailChange = (value) => {
setEmail(value);
setEmailError(isValidEmail(value) ? '' : 'Invalid email');
};
return <Input value={email} error={emailError} onChange={handleEmailChange} />;
}
3. Явные зависимости через props (Dependency Injection)
// ПЛОХО: компонент импортирует из store
import { useUserStore } from '@/store';
function UserCard() {
const user = useUserStore();
return <div>{user.name}</div>;
}
// ХОРОШО: user передается через props
function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>;
}
// Использование в приложении
import { useUserStore } from '@/store';
function ProfilePage() {
const user = useUserStore();
return <UserCard user={user} />; // Явная зависимость
}
Пример структуры UIkit компонента
// uikit/Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';
// uikit/Button/Button.tsx
import { cn } from '@/lib/utils';
import './Button.css';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
children: React.ReactNode;
}
export const Button = ({
variant = 'primary',
size = 'md',
loading,
children,
className,
...props
}: ButtonProps) => {
return (
<button
className={cn(
'button',
`button--${variant}`,
`button--${size}`,
{ 'button--loading': loading },
className
)}
disabled={loading || props.disabled}
{...props}
>
{loading ? 'Loading...' : children}
</button>
);
};
// uikit/Button/Button.test.tsx
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
test('renders with different variants', () => {
render(<Button variant="primary">Click me</Button>);
expect(screen.getByRole('button')).toHaveClass('button--primary');
});
test('shows loading state', () => {
render(<Button loading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
Управление версиями UIkit
// package.json основного приложения
{
"dependencies": {
"@mycompany/ui-kit": "^1.2.0"
}
}
Semantic Versioning:
1.2.3= major.minor.patch- Patch (1.2.3 -> 1.2.4): баг-фиксы, совместимо
- Minor (1.2.3 -> 1.3.0): новые фичи, совместимо
- Major (1.2.3 -> 2.0.0): breaking changes, несовместимо
Документирование UIkit (Storybook)
// uikit/.storybook/stories/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from '../../src/Button';
const meta: Meta<typeof Button> = {
title: 'UIKit/Button',
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Click me',
},
};
export const Loading: Story = {
args: {
loading: true,
children: 'Submit',
},
};
Лучшие практики
- Единственная точка входа (
index.ts) - Документирование (Storybook, JSDoc)
- Полное покрытие тестами (unit + visual)
- Чистые props (не зависит от контекста)
- Типизация (TypeScript strict mode)
- CSS инкапсуляция (CSS Modules или Tailwind)
- Версионирование (semantic versioning)
- CHANGELOG (что изменилось в новой версии)
Вывод
Выбирай способ отделения в зависимости от масштаба:
- Маленький проект (один продукт) → Папка
uikit/ - Средний проект (несколько приложений) → Monorepo с пакетами
- Большой проект (переиспользование во многих местах) → Отдельный npm пакет
Главное правило: UIkit зависит от nothing, всё зависит от UIkit. Это позволяет развивать компоненты независимо и переиспользовать их везде.