diff --git a/docs/ru/basics/architecture.md b/docs/ru/basics/architecture.md index aaa6d0e..2a58206 100644 --- a/docs/ru/basics/architecture.md +++ b/docs/ru/basics/architecture.md @@ -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 ( + + + + ) +} +``` + +Если 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 ( + }> + + + ) +} +``` + +### 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. Это усложняет рефакторинг и нарушает принцип колокации.