Антон Ларичев

Введение
Feature-Sliced Design (FSD) — это архитектурная методология, которая решает одну из главных проблем фронтенд-разработки: хаотичную организацию кода в растущих проектах. Если ваш проект дорос до десятков экранов и сотен компонентов, а файл helpers.ts стал свалкой утилит, пора задуматься о рефакторинге на Feature-Sliced Design.
В этой статье мы разберём реальный кейс: как выглядел проект до и после миграции на FSD, какие решения принимались по ходу рефакторинга, и какие результаты это принесло команде. Все примеры — на React и TypeScript, но подход одинаково применим к Vue и другим фреймворкам.
Что было до рефакторинга: типичная структура проекта
Рассмотрим структуру проекта интернет-магазина с каталогом, корзиной, личным кабинетом и админкой. До рефакторинга фронтенда проект выглядел так:
src/
components/
Header.tsx
Footer.tsx
ProductCard.tsx
CartItem.tsx
UserProfile.tsx
AdminTable.tsx
Modal.tsx
Button.tsx
... (ещё 80 компонентов)
pages/
Home.tsx
Catalog.tsx
Cart.tsx
Profile.tsx
helpers/
utils.ts // 1200 строк
api.ts // все запросы в одном файле
formatters.ts
store/
index.ts
userSlice.ts
cartSlice.ts
productsSlice.ts
styles/
global.css
Проблемы этой структуры
Первая проблема — папка components превратилась в плоский список из 80+ файлов. Кнопка Button лежит рядом с AdminTable, хотя у них нет ничего общего. Второй болевой точкой стал файл utils.ts на 1200 строк, где функция форматирования цены соседствовала с валидацией email. Третья проблема — неявные зависимости: ProductCard импортирует напрямую из store, helpers и других компонентов, создавая паутину связей.
// components/ProductCard.tsx — до рефакторинга
import { useAppSelector } from '../store';
import { addToCart } from '../store/cartSlice';
import { formatPrice } from '../helpers/utils';
import { fetchProductReviews } from '../helpers/api';
import { Modal } from './Modal';
import { Button } from './Button';
import { ReviewList } from './ReviewList';
Семь импортов из разных частей проекта. Компонент знает о корзине, API, хранилище и модальных окнах одновременно. Масштабируемость проекта при такой структуре стремится к нулю.
Структура проекта после миграции на FSD
После рефакторинга тот же проект приобрёл чёткую слоистую архитектуру FSD:
src/
app/
providers/
styles/
index.tsx
pages/
catalog/
cart/
profile/
home/
widgets/
header/
product-list/
cart-summary/
features/
add-to-cart/
search-products/
toggle-favorite/
entities/
product/
model/
ui/
api/
index.ts // public API
cart/
user/
shared/
ui/
Button.tsx
Modal.tsx
Input.tsx
lib/
format-price.ts
format-date.ts
api/
base-api.ts
config/
Каждый слой FSD имеет чёткую ответственность. Shared содержит только переиспользуемый код без бизнес-логики. Entities описывают бизнес-сущности. Features — пользовательские действия. Widgets — составные блоки интерфейса. Pages — маршруты приложения.
Как выглядит компонент после рефакторинга фронтенда
Тот же ProductCard после миграции на FSD:
// entities/product/ui/ProductCard.tsx
import { formatPrice } from '@/shared/lib/format-price';
import { Button } from '@/shared/ui';
import type { Product } from '../model/types';
interface ProductCardProps {
product: Product;
actions?: React.ReactNode; // слот для фич
}
export const ProductCard = ({ product, actions }: ProductCardProps) => {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<span>{formatPrice(product.price)}</span>
{actions}
</div>
);
};
Компонент стал чистым: он знает только о shared-слое и своей сущности. Действия вроде "добавить в корзину" передаются через слот, а не импортируются напрямую. Это и есть ключевой принцип FSD — слои могут зависеть только от нижележащих слоёв.
Как перейти на Feature-Sliced Design постепенно
Миграция на FSD не требует остановки разработки. Вот стратегия, которая сработала на нашем проекте:
Шаг 1: создать слой shared
Начните с выделения общих UI-компонентов и утилит в shared/. Разбейте монолитный utils.ts на отдельные модули с понятными именами. Каждый модуль экспортирует public API через index.ts:
// shared/lib/format-price.ts
export const formatPrice = (price: number, currency = 'RUB'): string => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency,
}).format(price);
};
Шаг 2: выделить entities
Определите бизнес-сущности проекта. В нашем случае это product, cart, user и order. Для каждой создайте слайс с подпапками model/, ui/, api/ и файлом index.ts как public API.
Шаг 3: извлечь features
Пользовательские действия — add-to-cart, search-products, toggle-favorite — выносятся в отдельные фичи. Каждая фича инкапсулирует логику действия и его UI:
// features/add-to-cart/ui/AddToCartButton.tsx
import { Button } from '@/shared/ui';
import { useAddToCart } from '../model/use-add-to-cart';
export const AddToCartButton = ({ productId }: { productId: number }) => {
const { add, isLoading } = useAddToCart();
return (
<Button onClick={() => add(productId)} disabled={isLoading}>
В корзину
</Button>
);
};
Шаг 4: собрать widgets и pages
Виджеты комбинируют entities и features. Страницы компонуют виджеты. На этом этапе паутина зависимостей распутывается окончательно.
Результаты рефакторинга: что изменилось
После трёх недель постепенной миграции команда получила измеримые результаты:
- Онбординг новых разработчиков сократился с 2 недель до 3 дней — структура проекта FSD самодокументируема
- Количество конфликтов при мерже снизилось на 70% — команды работают в изолированных слайсах
- Время на код-ревью уменьшилось на 40% — понятно, где что лежит и что от чего зависит
- Линтер Steiger автоматически проверяет соблюдение правил FSD и не пропускает нарушения в CI
Частые ошибки при внедрении FSD
Первая ошибка — пытаться мигрировать весь проект за один спринт. Это демотивирует команду и блокирует фича-разработку. Мигрируйте модуль за модулем.
Вторая ошибка — нарушение правила зависимостей. Если entities/product импортирует что-то из features/ — это сигнал, что логику нужно вынести в shared или пересмотреть границы слайсов.
Третья ошибка — отсутствие public API. Каждый слайс должен экспортировать только то, что нужно внешним потребителям, через index.ts. Прямые импорты из внутренних файлов слайса создают хрупкие связи.
Заключение
Feature-Sliced Design — это не серебряная пуля, а инструмент, который работает на проектах с несколькими разработчиками и растущей кодовой базой. Рефакторинг на FSD требует дисциплины, но результат — предсказуемая модульная архитектура, в которой каждый файл имеет своё место. Начните с shared-слоя, подключите Steiger для контроля и мигрируйте постепенно — через месяц вы не захотите возвращаться к плоской структуре.






Комментарии
0