Pull to refresh

Feature-Sliced Design (FSD): Основы и практические примеры архитектуры

Level of difficultyEasy

Когда я только начинал свою карьеру фронтенд-разработчика, часто сталкивался с проблемами поддержки кода в проектах. Со временем я понял, что структура кода имеет решающее значение. Так я узнал о 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 становится гибким инструментом для организации данных и логики. Это облегчило работу для нашей команды, минимизируя ошибки, упрощая адаптацию к изменениям и делая проект удобным для дальнейшего расширения.

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.