javascript

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

  • воскресенье, 10 ноября 2024 г. в 00:00:05
https://habr.com/ru/articles/857192/

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