docs: переработать раздел архитектуры (SLM Design)
- описание FSD как базы заменено на SLM Design (Scoped Layered Module Design) - добавлена терминология: слой, модуль, компонент, сегмент - добавлено детальное описание каждого слоя с примерами структуры - описан ключевой принцип колокации и подъёма модулей - добавлены правила именования суффиксов по слоям - добавлены правила импортов (между слоями, внутри модуля, shared) - добавлен жизненный цикл модуля и граничные случаи - добавлены обоснования архитектурных решений
This commit is contained in:
@@ -4,67 +4,468 @@ title: Архитектура
|
|||||||
|
|
||||||
# Архитектура
|
# Архитектура
|
||||||
|
|
||||||
Этот раздел описывает архитектуру проекта: из каких слоёв состоит приложение,
|
Раздел описывает архитектуру проекта: из каких слоёв состоит приложение,
|
||||||
как организован код внутри слоёв и какие правила управляют зависимостями.
|
как организован код внутри слоёв и какие правила управляют зависимостями.
|
||||||
|
|
||||||
## Что важно знать
|
## Что нужно знать
|
||||||
|
|
||||||
Проект использует [FSD (Feature-Sliced Design)](https://feature-sliced.design/docs/get-started/overview)
|
SLM Design (Scoped Layered Module Design) — архитектурный подход
|
||||||
как базовую архитектурную методологию. Если вы не знакомы с FSD — начните с официальной документации.
|
к проектированию фронтенд-приложений, предложенный Громовым Сергеем в 2026 г.
|
||||||
|
|
||||||
Данная архитектура является **надстройкой над FSD**, а не заменой. Все правила FSD действуют
|
Вырос на основе:
|
||||||
по умолчанию — если правило явно не переопределено в этом документе, применяется стандарт FSD.
|
|
||||||
Единственное отклонение: вместо слайсов используются **компоненты**.
|
|
||||||
|
|
||||||
## Слои
|
- [Feature-Sliced Design](https://feature-sliced.design) — слои и направление зависимостей
|
||||||
|
- Screaming Architecture — структура говорит сама за себя
|
||||||
|
- Colocation Principle — код рядом с местом использования
|
||||||
|
|
||||||
| Слой | Назначение |
|
Переосмыслив эти подходы, SLM Design отличается от FSD в трёх аспектах:
|
||||||
|------|-----------|
|
где живёт код (колокация), как он организован (модули)
|
||||||
| `app` | Инициализация: провайдеры, стили, роутинг Next.js |
|
и как масштабируется (подъём при переиспользовании).
|
||||||
| `screens` | Сборка страницы из виджетов и фич |
|
|
||||||
| `layouts` | Каркасы и шаблоны страниц |
|
|
||||||
| `widgets` | Крупные блоки интерфейса |
|
|
||||||
| `features` | Пользовательские сценарии и действия |
|
|
||||||
| `entities` | Бизнес-сущности |
|
|
||||||
| `shared` | Утилиты, UI-кит, инфраструктура |
|
|
||||||
|
|
||||||
Слой `pages` не используется — конфликтует с Next.js. Вместо него: `screens` и `layouts`.
|
## Терминология
|
||||||
|
|
||||||
Зависимости идут строго сверху вниз: `app → screens → layouts → widgets → features → entities → shared`.
|
Архитектура оперирует четырьмя ключевыми понятиями:
|
||||||
|
|
||||||
## Компоненты
|
- **Слой** — содержит модули
|
||||||
|
- **Модуль** — содержит сегменты
|
||||||
|
- **Компонент** — содержит сегменты
|
||||||
|
- **Сегмент** — папка внутри модуля или компонента, группирующая код по назначению: UI-элементы (`ui/`), хуки (`hooks/`), типы (`types/`), стили (`styles/`) и другие
|
||||||
|
|
||||||
Компонент — стандартная UI-единица, такая же как в любом React-проекте. Содержит корневой `.tsx`,
|
Модуль и компонент устроены одинаково — оба имеют сегменты. Разница в том, где они живут и обязателен ли UI.
|
||||||
публичный API (`index.ts`) и сегменты.
|
|
||||||
|
|
||||||
Компоненты располагаются в:
|
```text
|
||||||
- `shared/ui/` — переиспользуемые компоненты без бизнес-контекста
|
Слой
|
||||||
- `ui/` внутри master component'а — дочерние компоненты *(подробнее в разделе [Master component](#master-component))*
|
└── Модуль
|
||||||
|
├── Сегменты (hooks/, stores/, types/, styles/, lib/...)
|
||||||
|
└── ui/
|
||||||
|
└── Компонент
|
||||||
|
├── Сегменты (hooks/, stores/, types/, styles/, lib/...)
|
||||||
|
└── ui/
|
||||||
|
└── Компонент → ...
|
||||||
|
```
|
||||||
|
|
||||||
## Сегменты
|
### Слой
|
||||||
|
|
||||||
Сегмент — папка внутри компонента, группирующая код по техническому назначению. Набор не фиксирован.
|
Архитектурный уровень. Содержит только модули. Определяет назначение кода и правила зависимостей.
|
||||||
|
|
||||||
|
### Модуль
|
||||||
|
|
||||||
|
Единица первого уровня слоя, объединённая по смыслу. Может содержать компонент, логику, типы, стили — или любую комбинацию. Имеет публичный API (`index.ts`) и внутреннюю структуру из сегментов.
|
||||||
|
|
||||||
|
Модуль — не обязательно UI. Feature `analytics` может быть только стором и сервисом. Entity `session` может быть только типами и хуком.
|
||||||
|
|
||||||
|
Модуль не может содержать вложенных модулей. Вложенные единицы с UI размещаются в сегменте `ui/` как компоненты.
|
||||||
|
|
||||||
|
### Компонент
|
||||||
|
|
||||||
|
Вложенная единица внутри сегмента `ui/` модуля (или другого компонента). Публичный `.tsx` файл обязателен. Именуется без суффикса слоя.
|
||||||
|
|
||||||
|
Компонент может иметь собственные сегменты (`hooks/`, `styles/`, `types/` и т.д.), `index.ts` и свой `ui/` с ещё более вложенными компонентами.
|
||||||
|
|
||||||
|
Отличия от модуля:
|
||||||
|
|
||||||
|
| | Модуль | Компонент |
|
||||||
|
|--|--------|-----------|
|
||||||
|
| Где живёт | В корне слоя | В `ui/` модуля или другого компонента |
|
||||||
|
| Публичный `.tsx` | С суффиксом слоя, опционален | Без суффикса, обязателен |
|
||||||
|
| Может не иметь UI | Да | Нет |
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```text
|
||||||
|
features/auth-by-email/ # модуль
|
||||||
|
├── auth-by-email.feature.tsx # публичный .tsx модуля (с суффиксом, опционален)
|
||||||
|
├── ui/ # сегмент: компоненты
|
||||||
|
│ ├── login-form/ # компонент
|
||||||
|
│ │ ├── login-form.tsx # публичный .tsx компонента (без суффикса, обязателен)
|
||||||
|
│ │ ├── ui/ # вложенные компоненты
|
||||||
|
│ │ │ └── password-field/
|
||||||
|
│ │ │ └── password-field.tsx
|
||||||
|
│ │ ├── hooks/
|
||||||
|
│ │ │ └── use-validation.hook.ts
|
||||||
|
│ │ ├── styles/
|
||||||
|
│ │ │ └── login-form.module.css
|
||||||
|
│ │ └── index.ts
|
||||||
|
│ └── reset-password/ # компонент
|
||||||
|
│ ├── reset-password.tsx
|
||||||
|
│ └── index.ts
|
||||||
|
├── hooks/
|
||||||
|
│ └── use-auth.hook.ts
|
||||||
|
├── stores/
|
||||||
|
│ └── auth.store.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сегмент
|
||||||
|
|
||||||
|
Техническая папка внутри модуля или компонента, группирующая код по назначению. Набор не фиксирован — включаются только те сегменты, которые нужны.
|
||||||
|
|
||||||
| Сегмент | Назначение |
|
| Сегмент | Назначение |
|
||||||
|---------|-----------|
|
|---------|-----------|
|
||||||
| `styles/` | Стили |
|
| `ui/` | Вложенные компоненты |
|
||||||
| `types/` | Интерфейсы, типы, enums, DTO |
|
|
||||||
| `ui/` | Компоненты, провайдеры и любые другие элементы интерфейса |
|
|
||||||
| `stores/` | Сторы состояния |
|
|
||||||
| `hooks/` | React-хуки |
|
| `hooks/` | React-хуки |
|
||||||
| `services/` | Внешние источники данных |
|
| `stores/` | Сторы состояния |
|
||||||
|
| `types/` | Интерфейсы, типы, enums, DTO |
|
||||||
|
| `styles/` | Стили |
|
||||||
| `lib/` | Утилиты |
|
| `lib/` | Утилиты |
|
||||||
|
| `services/` | Внешние источники данных |
|
||||||
| `helpers/` | Вспомогательные функции |
|
| `helpers/` | Вспомогательные функции |
|
||||||
| `config/` | Константы, конфигурация |
|
| `config/` | Константы, конфигурация |
|
||||||
|
|
||||||
## Master component
|
## Ключевой принцип
|
||||||
|
|
||||||
Master component — это обычный компонент, на который наложен ряд дополнительных правил.
|
> Модуль живёт на самом низком уровне, где он используется.
|
||||||
Эти правила определяют его место в архитектуре и границы зависимостей.
|
> Поднимается выше только при переиспользовании в 2+ местах.
|
||||||
|
|
||||||
- Может располагаться только в слоях: `screens`, `layouts`, `widgets`, `features`, `entities`
|
Если модуль используется только в одном месте — он остаётся на текущем уровне.
|
||||||
- Импортирует master component'ы только из слоёв ниже по иерархии
|
Как только он начинает использоваться в 2+ местах — выносится на уровень выше.
|
||||||
- Корневой `.tsx` именуется с суффиксом слоя: `header.widget.tsx`, `auth.feature.tsx`
|
В крайнем случае — в `shared/`, где он доступен всем.
|
||||||
- Корневой `.tsx` необязателен — `index.ts` может экспортировать несколько сущностей напрямую
|
|
||||||
- Дочерние компоненты в `ui/` доступны снаружи только через `index.ts`
|
## Слои
|
||||||
- Компоненты внутри одного `ui/` могут импортировать друг друга
|
|
||||||
|
Каждый нижний слой не знает о существовании верхних. Импорты идут строго сверху вниз.
|
||||||
|
|
||||||
|
```
|
||||||
|
app → layouts → screens → widgets → features → entities → shared
|
||||||
|
```
|
||||||
|
|
||||||
|
| Слой | Что лежит | Импортирует |
|
||||||
|
|------|-----------|-------------|
|
||||||
|
| **App** | Роутинг, провайдеры, глобальные стили. Композиция layout + screen для маршрута. | Все слои ниже |
|
||||||
|
| **Layouts** | Каркас страницы, общий для группы маршрутов. | widgets, features, entities, shared |
|
||||||
|
| **Screens** | Контент конкретной страницы. | widgets, features, entities, shared |
|
||||||
|
| **Widgets** | Составные блоки с данными/логикой, переиспользуемые в 2+ местах. | features, entities, shared |
|
||||||
|
| **Features** | Пользовательское действие или интерактивный сценарий. | entities, shared |
|
||||||
|
| **Entities** | Бизнес-сущность с отображением и типами. | shared |
|
||||||
|
| **Shared** | Переиспользуемые компоненты, утилиты, стили без бизнес-логики. | ничего |
|
||||||
|
|
||||||
|
Принципы:
|
||||||
|
|
||||||
|
- Импорты строго сверху вниз
|
||||||
|
- Модули одного слоя не знают друг о друге
|
||||||
|
- Layout получает контекстно-зависимые блоки через пропсы от app, а не импортирует их сам
|
||||||
|
- `entities/` и `features/` создаются осознанно — это не результат «вынесения» компонента из screen
|
||||||
|
|
||||||
|
### App
|
||||||
|
|
||||||
|
Точка входа приложения: роутинг (Next.js App Router), провайдеры, глобальные стили.
|
||||||
|
Находится на самом высоком уровне абстракции — может импортировать любой слой ниже.
|
||||||
|
Никакой бизнес-логики — только композиция.
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/
|
||||||
|
├── layout.tsx # RootLayout: провайдеры, глобальные стили
|
||||||
|
├── page.tsx # Главная: MainLayout + HomeScreen
|
||||||
|
├── knv-new/
|
||||||
|
│ └── page.tsx # КНВ: MainLayout + KnvScreen
|
||||||
|
└── catalog/
|
||||||
|
└── page.tsx # Каталог: MainLayout + CatalogScreen
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// app/knv-new/page.tsx
|
||||||
|
import { MainLayout } from '@/layouts/main'
|
||||||
|
import { KnvScreen } from '@/screens/knv'
|
||||||
|
|
||||||
|
export default function KnvNewPage() {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<KnvScreen />
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Если layout требует разный контент в зависимости от страницы — app передаёт его через пропсы:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// app/knv-new/page.tsx
|
||||||
|
import { MainLayout } from '@/layouts/main'
|
||||||
|
import { KnvScreen } from '@/screens/knv'
|
||||||
|
import { KnvHeader } from '@/widgets/knv-header'
|
||||||
|
|
||||||
|
export default function KnvNewPage() {
|
||||||
|
return (
|
||||||
|
<MainLayout header={<KnvHeader />}>
|
||||||
|
<KnvScreen />
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layouts
|
||||||
|
|
||||||
|
Каркас страницы — общие элементы, одинаковые для группы маршрутов (header, footer, sidebar).
|
||||||
|
|
||||||
|
Если компонент внутри layout начинает использоваться в 2+ местах — он выносится в `widgets/` или `shared/ui/`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/layouts/
|
||||||
|
└── main/
|
||||||
|
├── main.layout.tsx
|
||||||
|
├── ui/
|
||||||
|
│ ├── header/
|
||||||
|
│ │ └── header.tsx
|
||||||
|
│ └── footer/
|
||||||
|
│ └── footer.tsx
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Screens
|
||||||
|
|
||||||
|
Контент конкретной страницы. Собирает локальные секции и переиспользуемые модули из нижних слоёв.
|
||||||
|
|
||||||
|
Если компонент внутри screen начинает использоваться в 2+ местах — он выносится в `widgets/` или `shared/ui/`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/screens/
|
||||||
|
└── knv/
|
||||||
|
├── knv.screen.tsx
|
||||||
|
├── ui/
|
||||||
|
│ ├── hero-section/
|
||||||
|
│ │ └── hero-section.tsx
|
||||||
|
│ ├── products-section/
|
||||||
|
│ │ └── products-section.tsx
|
||||||
|
│ └── diseases-section/
|
||||||
|
│ └── diseases-section.tsx
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Widgets
|
||||||
|
|
||||||
|
Составные блоки с данными и логикой, переиспользуемые в 2+ местах.
|
||||||
|
|
||||||
|
Если блок с логикой нужен только в одном месте — это компонент внутри `screens/{name}/ui/` или `layouts/{name}/ui/`, а не widget.
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/widgets/
|
||||||
|
└── popular-products-slider/
|
||||||
|
├── popular-products-slider.widget.tsx
|
||||||
|
├── ui/
|
||||||
|
│ └── slider-card/
|
||||||
|
│ └── slider-card.tsx
|
||||||
|
├── hooks/
|
||||||
|
│ └── use-products.hook.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
Пользовательское действие или интерактивный сценарий: авторизация, заказ, добавление в корзину.
|
||||||
|
|
||||||
|
Feature создаётся осознанно, когда есть действие пользователя с бизнес-логикой. Компонент опционален — feature может экспортировать хуки, сторы, компоненты или всё вместе.
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/features/
|
||||||
|
└── auth-by-email/
|
||||||
|
├── auth-by-email.feature.tsx
|
||||||
|
├── ui/
|
||||||
|
│ ├── login-form/
|
||||||
|
│ │ └── login-form.tsx
|
||||||
|
│ └── reset-password/
|
||||||
|
│ └── reset-password.tsx
|
||||||
|
├── hooks/
|
||||||
|
│ └── use-auth.hook.ts
|
||||||
|
├── stores/
|
||||||
|
│ └── auth.store.ts
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entities
|
||||||
|
|
||||||
|
Бизнес-сущность с отображением и типами: препарат, заболевание, врач, пользователь.
|
||||||
|
|
||||||
|
Entity создаётся осознанно, когда появляется бизнес-сущность. Компонент опционален — entity может быть только типами и хуком.
|
||||||
|
|
||||||
|
Отличие от `shared/ui/`: entity-компонент знает о бизнес-домене (принимает `Product`, а не абстрактные пропсы).
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/entities/
|
||||||
|
├── product/
|
||||||
|
│ ├── product.entity.tsx
|
||||||
|
│ ├── ui/
|
||||||
|
│ │ └── product-card/
|
||||||
|
│ │ └── product-card.tsx
|
||||||
|
│ ├── types/
|
||||||
|
│ │ └── product.type.ts
|
||||||
|
│ └── index.ts
|
||||||
|
│
|
||||||
|
├── session/
|
||||||
|
│ ├── types/
|
||||||
|
│ │ └── session.type.ts
|
||||||
|
│ ├── hooks/
|
||||||
|
│ │ └── use-session.hook.ts
|
||||||
|
│ └── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shared
|
||||||
|
|
||||||
|
Переиспользуемые компоненты, утилиты, стили без бизнес-логики. Не знает о бизнес-домене — работает с абстрактными данными.
|
||||||
|
|
||||||
|
Структурирован как набор сегментов:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/shared/
|
||||||
|
├── ui/
|
||||||
|
│ ├── icon/
|
||||||
|
│ │ └── icon.tsx
|
||||||
|
│ ├── carousel/
|
||||||
|
│ │ ├── carousel.tsx
|
||||||
|
│ │ ├── ui/
|
||||||
|
│ │ │ ├── carousel-slide/
|
||||||
|
│ │ │ │ └── carousel-slide.tsx
|
||||||
|
│ │ │ └── carousel-dots/
|
||||||
|
│ │ │ └── carousel-dots.tsx
|
||||||
|
│ │ └── index.ts
|
||||||
|
│ ├── container/
|
||||||
|
│ └── button/
|
||||||
|
├── lib/
|
||||||
|
│ ├── format-date.ts
|
||||||
|
│ └── cn.ts
|
||||||
|
├── styles/
|
||||||
|
│ ├── variables.css
|
||||||
|
│ └── media.css
|
||||||
|
└── sprites/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Модуль
|
||||||
|
|
||||||
|
### Структура
|
||||||
|
|
||||||
|
```text
|
||||||
|
{name}/
|
||||||
|
├── {name}.{суффикс}.tsx # компонент (опционален)
|
||||||
|
├── ui/ # вложенные компоненты
|
||||||
|
├── hooks/ # хуки
|
||||||
|
├── stores/ # сторы
|
||||||
|
├── types/ # типы, интерфейсы, enums
|
||||||
|
├── styles/ # стили
|
||||||
|
├── lib/ # утилиты
|
||||||
|
├── services/ # внешние источники данных
|
||||||
|
├── helpers/ # вспомогательные функции
|
||||||
|
├── config/ # константы, конфигурация
|
||||||
|
└── index.ts # публичный API
|
||||||
|
```
|
||||||
|
|
||||||
|
### Именование компонента
|
||||||
|
|
||||||
|
Суффикс слоя получают **только модули первого уровня слоя** — те, что лежат непосредственно в корне слоя. Все компоненты (в `ui/`, любой глубины) именуются без суффикса. Без исключений.
|
||||||
|
|
||||||
|
| Слой | Суффикс | Пример |
|
||||||
|
|------|---------|--------|
|
||||||
|
| Layouts | `.layout.tsx` | `main.layout.tsx` |
|
||||||
|
| Screens | `.screen.tsx` | `knv.screen.tsx` |
|
||||||
|
| Widgets | `.widget.tsx` | `popular-products-slider.widget.tsx` |
|
||||||
|
| Features | `.feature.tsx` | `auth-by-email.feature.tsx` |
|
||||||
|
| Entities | `.entity.tsx` | `product.entity.tsx` |
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
|
||||||
|
```text
|
||||||
|
features/auth-by-email/auth-by-email.feature.tsx # модуль первого уровня → суффикс
|
||||||
|
features/auth-by-email/ui/login-form/login-form.tsx # компонент в ui/ → без суффикса
|
||||||
|
shared/ui/carousel/carousel.tsx # компонент в shared → без суффикса
|
||||||
|
```
|
||||||
|
|
||||||
|
### Правила импорта
|
||||||
|
|
||||||
|
Три уровня правил:
|
||||||
|
|
||||||
|
**Между слоями** — импорты строго сверху вниз:
|
||||||
|
|
||||||
|
```
|
||||||
|
app → layouts → screens → widgets → features → entities → shared
|
||||||
|
```
|
||||||
|
|
||||||
|
**Внутри модуля (не shared)** — сегменты доступны друг другу и компонентам. Компоненты внутри одного `ui/` не импортируют друг друга:
|
||||||
|
|
||||||
|
```text
|
||||||
|
features/auth-by-email/
|
||||||
|
├── ui/
|
||||||
|
│ ├── login-form/ # НЕ может импортировать reset-password
|
||||||
|
│ └── reset-password/ # НЕ может импортировать login-form
|
||||||
|
```
|
||||||
|
|
||||||
|
Если двум компонентам нужен общий код — он поднимается на уровень выше:
|
||||||
|
|
||||||
|
```text
|
||||||
|
features/auth/ui/login-form/ui/email-input/ # нужен соседу
|
||||||
|
→ features/auth/ui/email-input/ # поднимаем на уровень
|
||||||
|
→ shared/ui/email-input/ # если нужен за пределами фичи
|
||||||
|
```
|
||||||
|
|
||||||
|
Компоненты наследуют правила зависимостей **родительского слоя**:
|
||||||
|
|
||||||
|
- Компонент внутри `features/auth/ui/login-form/` может импортировать `entities/` и `shared/` — как и сам feature
|
||||||
|
- Компонент внутри `widgets/hero/ui/hero-stats/` может импортировать `features/`, `entities/`, `shared/` — как и сам widget
|
||||||
|
|
||||||
|
**Shared** — без ограничений на внутренние импорты. Компоненты в `shared/ui/` могут импортировать друг друга (`button` использует `icon`), `ui/` может использовать `lib/` и другие сегменты. Shared — фундамент, его компоненты строятся друг на друге.
|
||||||
|
|
||||||
|
Правила импорта между слоями enforceable через ESLint — настройка границ слоёв и запрет обратных зависимостей.
|
||||||
|
|
||||||
|
## Жизненный цикл модуля
|
||||||
|
|
||||||
|
Модуль не проектируется «на вырост». Он рождается на самом низком уровне
|
||||||
|
и поднимается только когда появляется реальная потребность.
|
||||||
|
|
||||||
|
Пример пути компонента `product-card`:
|
||||||
|
|
||||||
|
1. **Начало:** `screens/catalog/ui/product-card/` — нужен только на странице каталога.
|
||||||
|
2. **Переиспользование:** появился на странице поиска — выносим выше.
|
||||||
|
Куда именно зависит от природы:
|
||||||
|
- Составной блок с данными и логикой → `widgets/product-card/`
|
||||||
|
- Представление бизнес-сущности → `entities/product/ui/product-card/`
|
||||||
|
- Абстрактный UI без бизнес-логики → `shared/ui/product-card/`
|
||||||
|
|
||||||
|
Основной триггер подъёма — переиспользование в 2+ местах.
|
||||||
|
Но если очевидно что модуль будет переиспользоваться — разумно разместить его на нужном уровне сразу.
|
||||||
|
|
||||||
|
Как происходит подъём на практике:
|
||||||
|
|
||||||
|
1. Разработчик обнаруживает что компонент нужен в другом месте
|
||||||
|
2. Определяет целевой слой по природе компонента (widget / entity / shared)
|
||||||
|
3. Перемещает папку, обновляет импорты, добавляет суффикс если это модуль первого уровня слоя
|
||||||
|
4. Ревью подтверждает что подъём обоснован
|
||||||
|
|
||||||
|
Подъём — это обычный рефакторинг в рамках задачи, а не отдельная активность.
|
||||||
|
|
||||||
|
## Граничные случаи
|
||||||
|
|
||||||
|
| Ситуация | Решение | Почему |
|
||||||
|
|----------|---------|--------|
|
||||||
|
| Фильтр каталога только на одной странице, но с хуками и стором | Модуль в `screens/catalog/` с сегментами `hooks/`, `stores/` | Не feature — feature это действие пользователя с бизнес-логикой (авторизация, заказ), а не UI с состоянием |
|
||||||
|
| Компонент используется в 2 местах на одной странице | Остаётся в `screens/{name}/ui/` | Переиспользование внутри одного screen — не повод выносить в widget |
|
||||||
|
| Entity без UI (только типы и хук) | Нормально | Модуль не обязан иметь UI. `entities/session/` с типами и хуком — валидный модуль |
|
||||||
|
| Компонент нужен и в layout, и в screen | Выносить в `widgets/` или `shared/ui/` | Два разных слоя используют один компонент → переиспользование → подъём |
|
||||||
|
| Модуль screen содержит бизнес-логику (хуки, стор, сервисы) | Нормально | Это не значит что он должен стать feature. Модуль с логикой внутри screen — обычное дело, пока он не переиспользуется |
|
||||||
|
| Компонент в `shared/ui/button` хочет использовать `shared/ui/icon` | Разрешено | Shared — исключение: внутренние импорты без ограничений. Это фундамент, его компоненты строятся друг на друге |
|
||||||
|
|
||||||
|
## Запрещено
|
||||||
|
|
||||||
|
- **Не создавать feature без бизнес-логики** — кнопка без состояния и сайд-эффектов это компонент в `ui/`, а не feature
|
||||||
|
- **Не класть доменные типы в shared** — если тип знает о Product, Disease, User — он живёт в `entities/`, не в `shared/`
|
||||||
|
- **Не создавать entity или feature как результат «вынесения»** — они создаются осознанно: появилась бизнес-сущность → entity, появилось действие пользователя → feature
|
||||||
|
- **Не импортировать соседние компоненты в `ui/` (кроме shared)** — если двум компонентам нужен общий код, он поднимается на уровень выше
|
||||||
|
- **Не хранить в shared «помойку для всего»** — shared содержит переиспользуемые компоненты и утилиты без бизнес-логики, а не код который «непонятно куда положить»
|
||||||
|
|
||||||
|
## Почему так, а не иначе
|
||||||
|
|
||||||
|
### Почему `ui/` а не `modules/`
|
||||||
|
|
||||||
|
Внутри сегмента `ui/` всегда лежат единицы с обязательным UI (компоненты). Название точно отражает содержимое. `modules/` был нейтральнее, но скрывал природу вложенных единиц и создавал путаницу — модуль внутри модуля размывал понятие «модуль как единица слоя».
|
||||||
|
|
||||||
|
### Почему модуль и компонент — разные понятия
|
||||||
|
|
||||||
|
Модуль — единица первого уровня слоя, может не иметь UI. Компонент — вложенная единица с обязательным UI. Разделение снимает вопрос «а если нет `.tsx` — это всё ещё компонент?» и делает название сегмента `ui/` честным.
|
||||||
|
|
||||||
|
### Почему shared без ограничений на внутренние импорты
|
||||||
|
|
||||||
|
Shared — фундамент. Его компоненты строятся друг на друге: `button` использует `icon`, `carousel` использует `button`. Запрещать это — бороться с реальностью. Поднимать некуда — shared уже нижний слой.
|
||||||
|
|
||||||
|
### Почему нет слоя pages
|
||||||
|
|
||||||
|
Роутинг живёт в `app/` (Next.js App Router). Отдельный слой `pages` конфликтовал бы с файловой структурой Next.js и дублировал ответственность `app/`.
|
||||||
|
|
||||||
|
### Почему компоненты в одном `ui/` не импортируют друг друга (кроме shared)
|
||||||
|
|
||||||
|
Независимые компоненты легко выносить в другой слой при переиспользовании. Если компонент A зависит от соседа B — при подъёме A придётся тянуть B. Это усложняет рефакторинг и нарушает принцип колокации.
|
||||||
|
|||||||
Reference in New Issue
Block a user