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)
|
||||
как базовую архитектурную методологию. Если вы не знакомы с FSD — начните с официальной документации.
|
||||
SLM Design (Scoped Layered Module Design) — архитектурный подход
|
||||
к проектированию фронтенд-приложений, предложенный Громовым Сергеем в 2026 г.
|
||||
|
||||
Данная архитектура является **надстройкой над FSD**, а не заменой. Все правила FSD действуют
|
||||
по умолчанию — если правило явно не переопределено в этом документе, применяется стандарт FSD.
|
||||
Единственное отклонение: вместо слайсов используются **компоненты**.
|
||||
Вырос на основе:
|
||||
|
||||
## Слои
|
||||
- [Feature-Sliced Design](https://feature-sliced.design) — слои и направление зависимостей
|
||||
- Screaming Architecture — структура говорит сама за себя
|
||||
- Colocation Principle — код рядом с местом использования
|
||||
|
||||
| Слой | Назначение |
|
||||
|------|-----------|
|
||||
| `app` | Инициализация: провайдеры, стили, роутинг Next.js |
|
||||
| `screens` | Сборка страницы из виджетов и фич |
|
||||
| `layouts` | Каркасы и шаблоны страниц |
|
||||
| `widgets` | Крупные блоки интерфейса |
|
||||
| `features` | Пользовательские сценарии и действия |
|
||||
| `entities` | Бизнес-сущности |
|
||||
| `shared` | Утилиты, UI-кит, инфраструктура |
|
||||
Переосмыслив эти подходы, SLM Design отличается от FSD в трёх аспектах:
|
||||
где живёт код (колокация), как он организован (модули)
|
||||
и как масштабируется (подъём при переиспользовании).
|
||||
|
||||
Слой `pages` не используется — конфликтует с Next.js. Вместо него: `screens` и `layouts`.
|
||||
## Терминология
|
||||
|
||||
Зависимости идут строго сверху вниз: `app → screens → layouts → widgets → features → entities → shared`.
|
||||
Архитектура оперирует четырьмя ключевыми понятиями:
|
||||
|
||||
## Компоненты
|
||||
- **Слой** — содержит модули
|
||||
- **Модуль** — содержит сегменты
|
||||
- **Компонент** — содержит сегменты
|
||||
- **Сегмент** — папка внутри модуля или компонента, группирующая код по назначению: UI-элементы (`ui/`), хуки (`hooks/`), типы (`types/`), стили (`styles/`) и другие
|
||||
|
||||
Компонент — стандартная UI-единица, такая же как в любом React-проекте. Содержит корневой `.tsx`,
|
||||
публичный API (`index.ts`) и сегменты.
|
||||
Модуль и компонент устроены одинаково — оба имеют сегменты. Разница в том, где они живут и обязателен ли UI.
|
||||
|
||||
Компоненты располагаются в:
|
||||
- `shared/ui/` — переиспользуемые компоненты без бизнес-контекста
|
||||
- `ui/` внутри master component'а — дочерние компоненты *(подробнее в разделе [Master component](#master-component))*
|
||||
```text
|
||||
Слой
|
||||
└── Модуль
|
||||
├── Сегменты (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/` | Стили |
|
||||
| `types/` | Интерфейсы, типы, enums, DTO |
|
||||
| `ui/` | Компоненты, провайдеры и любые другие элементы интерфейса |
|
||||
| `stores/` | Сторы состояния |
|
||||
| `ui/` | Вложенные компоненты |
|
||||
| `hooks/` | React-хуки |
|
||||
| `services/` | Внешние источники данных |
|
||||
| `stores/` | Сторы состояния |
|
||||
| `types/` | Интерфейсы, типы, enums, DTO |
|
||||
| `styles/` | Стили |
|
||||
| `lib/` | Утилиты |
|
||||
| `services/` | Внешние источники данных |
|
||||
| `helpers/` | Вспомогательные функции |
|
||||
| `config/` | Константы, конфигурация |
|
||||
|
||||
## Master component
|
||||
## Ключевой принцип
|
||||
|
||||
Master component — это обычный компонент, на который наложен ряд дополнительных правил.
|
||||
Эти правила определяют его место в архитектуре и границы зависимостей.
|
||||
> Модуль живёт на самом низком уровне, где он используется.
|
||||
> Поднимается выше только при переиспользовании в 2+ местах.
|
||||
|
||||
- Может располагаться только в слоях: `screens`, `layouts`, `widgets`, `features`, `entities`
|
||||
- Импортирует master component'ы только из слоёв ниже по иерархии
|
||||
- Корневой `.tsx` именуется с суффиксом слоя: `header.widget.tsx`, `auth.feature.tsx`
|
||||
- Корневой `.tsx` необязателен — `index.ts` может экспортировать несколько сущностей напрямую
|
||||
- Дочерние компоненты в `ui/` доступны снаружи только через `index.ts`
|
||||
- Компоненты внутри одного `ui/` могут импортировать друг друга
|
||||
Если модуль используется только в одном месте — он остаётся на текущем уровне.
|
||||
Как только он начинает использоваться в 2+ местах — выносится на уровень выше.
|
||||
В крайнем случае — в `shared/`, где он доступен всем.
|
||||
|
||||
## Слои
|
||||
|
||||
Каждый нижний слой не знает о существовании верхних. Импорты идут строго сверху вниз.
|
||||
|
||||
```
|
||||
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