Feature-Sliced Design (FSD): Основы и практические примеры архитектуры
Когда я только начинал свою карьеру фронтенд-разработчика, часто сталкивался с проблемами поддержки кода в проектах. Со временем я понял, что структура кода имеет решающее значение. Так я узнал о Feature-Sliced Design. Этот подход помогает разбивать проект на функциональные части, что упрощает работу с кодом и его сопровождение. Давайте разберемся как это работает.
Основные принципы Feature-Sliced Design
FSD (Feature-Sliced Design) нужен для удобной организации кода, особенно в больших проектах, и даёт несколько ключевых преимуществ:
1. Понятность: код разбит на независимые модули (например, авторизация, профиль), что делает структуру логичной и облегчает навигацию.
2. Поддерживаемость: каждый модуль ограничен своей зоной ответственности, что упрощает изменения — работа над одной фичей не ломает другую.
3. Переиспользуемость: изолированные компоненты и логика могут быть легко использованы в других частях приложения без дублирования.
4. Масштабируемость: новые функции можно добавлять как отдельные модули, не нарушая структуру кода.
5. Удобство тестирования: с четкими границами модулей проще писать и поддерживать тесты.
Описание структуры слоев и папок
1. App
Назначение: Слой для инициализации приложения.
Содержит: Глобальные настройки (например, темы), роутинг, провайдеры контекста.
Пример:
App.tsx, AppRouter.tsx
.
2. Entities
Назначение: Здесь хранятся бизнес-сущности — основные модели и их логика.
Содержит: Определения сущностей (например, User, Product), бизнес-логику, которая их касается.
Пример:
entities/User, entities/Product
.
3. Features
Назначение: Модули, которые реализуют конкретные пользовательские действия.
Содержит: Компоненты, хуки и логику, которая выполняет задачу для пользователя, например, авторизацию или добавление товара в корзину.
Пример:
features/Login, features/AddToCart
.
4. Shared
Назначение: Общие утилиты, типы и компоненты, которые используются в разных частях приложения.
Содержит: Переиспользуемые компоненты (например, кнопки), утилиты, глобальные типы.
Пример:
shared/Button, shared/hooks, shared/utils
.
5. Pages
Назначение: Собирает все компоненты, чтобы сформировать страницы приложения.
Содержит: Страницы, которые используют features, entities и shared слои, чтобы создавать полноценные представления.
Пример:
pages/HomePage, pages/ProductPage
.
6. Widgets
Назначение: Крупные, повторяющиеся блоки, которые можно переиспользовать на разных страницах.
Содержит: Модули с логикой и UI (например, блоки новостей, карусели).
Пример:
widgets/NewsCarousel, widgets/UserProfile
.
7. Processes (опционально)
Назначение: Сюда можно выносить сложные процессы, включающие несколько фич.
Содержит: Бизнес-процессы, если такие есть (например, процесс оформления заказа).
Пример:
processes/Checkout
.
Пример структуры React-приложения для Интернет-магазина:
src/
├── app/ // Глобальные настройки приложения
│ ├── store.js // Настройка Redux store, подключение middleware и т.д.
│ └── rootReducer.js // Главный редьюсер, который объединяет все слайсы
│
├── pages/ // Основные страницы приложения
│ ├── HomePage/ // Главная страница
│ │ ├── index.js // Точка входа страницы для упрощённого импорта
│ │ ├── HomePage.jsx // Компонент главной страницы
│ │ └── HomePage.module.css // Стили для главной страницы
│ ├── ProductPage/ // Страница деталей товара
│ │ ├── index.js
│ │ ├── ProductPage.jsx
│ │ └── ProductPage.module.css
│ ├── CartPage/ // Страница корзины
│ │ ├── index.js
│ │ ├── CartPage.jsx
│ │ └── CartPage.module.css
│ └── CheckoutPage/ // Страница оформления заказа
│ ├── index.js
│ ├── CheckoutPage.jsx
│ └── CheckoutPage.module.css
│
├── widgets/ // Повторяющиеся UI-блоки, используемые на нескольких страницах
│ ├── Header/ // Шапка сайта
│ │ ├── index.js
│ │ ├── Header.jsx
│ │ └── Header.module.css
│ ├── Footer/ // Подвал сайта
│ │ ├── index.js
│ │ ├── Footer.jsx
│ │ └── Footer.module.css
│ └── ProductList/ // Виджет со списком товаров
│ ├── index.js
│ ├── ProductList.jsx
│ └── ProductList.module.css
|
├── features/ // Конкретные функции приложения, каждая из которых автономна
│ ├── Product/ // Функционал работы с товарами
│ │ ├── index.js // Экспортирует компоненты и логику фичи
│ │ ├── ProductSlice.js // Redux slice для управления состоянием товаров
│ │ └── Product.module.css
│ ├── Cart/ // Функционал работы с корзиной
│ │ ├── index.js
│ │ ├── CartSlice.js // Redux slice для управления состоянием корзины
│ │ └── Cart.module.css
│ └── Auth/ // Функционал авторизации пользователя
│ ├── index.js
│ ├── AuthSlice.js // Redux slice для состояния пользователя (авторизация, токены и т.д.)
│ └── Auth.module.css
│
├── processes/ // Сложные бизнес-процессы, объединяющие фичи и виджеты
│ ├── UserRegistration/ // Процесс регистрации пользователя
│ │ ├── index.js
│ │ ├── UserRegistration.jsx // Компонент регистрации с формами и валидацией
│ │ └── UserRegistration.module.css
│ ├── AddToCart/ // Процесс добавления товара в корзину
│ │ ├── index.js
│ │ ├── AddToCart.jsx // Компонент добавления в корзину, включает логику для Cart
│ │ └── AddToCart.module.css
│ └── CheckoutProcess/ // Процесс оформления заказа
│ ├── index.js
│ ├── CheckoutProcess.jsx // Компонент оформления заказа с интеграцией оплаты
│ └── CheckoutProcess.module.css
│
├── shared/ // Общие компоненты, которые используются по всему проекту
│ └── components/
│ ├── Button/ // Кнопка, переиспользуемая по всему приложению
│ │ ├── index.js
│ │ ├── Button.jsx
│ │ └── Button.module.css
│ ├── Input/ // Поле ввода, переиспользуемое в формах
│ │ ├── index.js
│ │ ├── Input.jsx
│ │ └── Input.module.css
│ └── Modal/ // Модальное окно для отображения уведомлений и подтверждений
│ ├── index.js
│ ├── Modal.jsx
│ └── Modal.module.css
│
└── utils/ // Утилитарные функции и хелперы
├── api.js // API-методы для взаимодействия с сервером
└── formatPrice.js // Функция для форматирования цен, чтобы они выглядели красиво
Поддержка модульности с алиасами и зависимостями
Модульная архитектура с алиасами и управлением зависимостями в FSD делает проект более структурированным и гибким для роста. Давайте разберём, как это работает и что важно учесть.
1. Алиасы для модулей
Алиасы в FSD позволяют упростить импорт, сократив длинные пути и изоляцию модулей. Это делается с помощью настройки tsconfig.json или webpack.config.js. В tsconfig.json, например, можно прописать алиасы следующим образом:
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@app/*": ["app/*"],
"@entities/*": ["entities/*"],
"@features/*": ["features/*"],
"@shared/*": ["shared/*"],
"@pages/*": ["pages/*"],
"@widgets/*": ["widgets/*"],
"@processes/*": ["processes/*"]
}
}
}
Теперь можно импортировать зависимости в модули по короткому пути, что делает код более читаемым и поддерживаемым.
import { UserModel } from "@entities/User";
import { AddToCard } from "@feature/AddToCard";
2. Изоляция модулей
Каждый модуль в FSD представляет собой отдельный слой ответственности. Например, entities служит для работы с бизнес-логикой и сущностями, features — для пользовательских функций, shared — для общих компонентов, доступных в приложении. Это изолирует логику и данные, ограничивая влияние изменений на весь проект.
3. Управление зависимостями
Важный принцип модульной архитектуры FSD — минимизация зависимости между модулями. Здесь поможет использование инверсии зависимостей (Dependency Injection) и управляемых экспортов. Например, экспортируем только те части модулей, которые нужны в других слоях, а частные элементы (вроде вспомогательных функций) скрываем внутри модуля.
4. Настройка зависимостей и разрешений
Чтобы избежать циклических зависимостей, FSD предполагает, что:
Нижние слои (shared) могут быть импортированы в верхние слои (features, entities, pages).
Верхние слои не могут напрямую импортировать друг друга. Например, features и entities должны общаться через слой shared или API.
Пример ограничения зависимостей:
Для управления доступом и зависимостями можно использовать ESLint с настройками правил для алиасов. В .eslintrc.json можно прописать правила для блокировки циклических и ненужных зависимостей.
Пример:
{
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "@features",
"message": "Avoid direct imports from features. Use only allowed layers."
}
]
}
]
}
}
Типы и DTO (Data Transfer Objects)
В FSD типы и DTO обеспечивают строгую структуру данных и удобство при работе с API, особенно в масштабных проектах.
Типы
Типы описывают внутренние данные приложения и помогают избежать ошибок. Например, тип для пользователя может быть таким:
Пример:
export type Order = {
id: string;
date: string;
customer_name: string;
total_amount: number;
};
DTO
DTO (Data Transfer Objects) описывают данные для обмена с API и отделяют их от внутренней структуры, что упрощает работу с изменениями на сервере.
Пример:
export type OrderDTO = {
id: string;
date: string;
customer_name: string;
total_amount: number;
};
Maппинг DTO к типам
Маппинг преобразует DTO в нужный формат. Это удобно, когда данные API отличаются по структуре.
Пример:
export const mapUserDtoToUser = (dto: OrderDTO): Order => ({
id: dto.id,
date: dto.date,
customer_name: dto.customer_name,
total_amount: dto.total_amount,
});
Зачем это нужно?
Гибкость при изменении API: Корректируем только DTO и маппинг.
Читаемость и строгая структура: Типы делают код понятнее.
Защита внутренней структуры: DTO отделяют внутренние данные от внешних запросов.
Типы и DTO повышают стабильность и гибкость в работе с данными, делая архитектуру надежной.
Заключение
FSD — мощная архитектура, которая дает проекту чёткую структуру, особенно в масштабируемых приложениях. Разделение на слои (entities, features, pages, widgets и т.д.) позволяет изолировать модули, упрощая поддержку и развитие кода.
С использованием алиасов, DTO, строгой типизации и настройки зависимостей, FSD становится гибким инструментом для организации данных и логики. Это облегчило работу для нашей команды, минимизируя ошибки, упрощая адаптацию к изменениям и делая проект удобным для дальнейшего расширения.
P.S. Статья вынесена из песочницы в связи с получением приглашения.