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. Статья вынесена из песочницы в связи с получением приглашения.


ссылка на оригинал статьи https://habr.com/ru/articles/857192/