# Feature-Sliced Design ## documentation - [Примеры](/examples.md): Список сайтов, сделанных людьми с FSD - [🧭 Навигация](/nav.md): Feature-Sliced Design Navigation help page - [Поиск по сайту](/search.md) - [Версии Feature-Sliced Design](/versions.md): Feature-Sliced Design Versions page listing all documented site versions - [💫 Community](/community.md): Community resources, additional materials - [Team](/community/team.md): Core-team - [Альтернативы](/docs/about/alternatives.md): История архитектурных подходов - [Миссия](/docs/about/mission.md): Здесь описаны цели и ограничения применимости методологии, которыми мы руководствуемся при разработке методологии - [Мотивация](/docs/about/motivation.md): Главная идея Feature-Sliced Design - облегчить и удешевить разработку комплексных и развивающихся проектов, на основании объединения результатов исследований, обсуждения опыта разного рода широкого круга разработчиков. - [Продвижение в компании](/docs/about/promote/for-company.md): Нужна ли методология проекту и компании? - [Продвижение в команде](/docs/about/promote/for-team.md): - Онбординг новых людей - [Аспекты интеграции](/docs/about/promote/integration.md): Что получаем в конечном счете? - [Частичное применение](/docs/about/promote/partial-application.md): Как частично применять методологию? Имеет ли смысл? Что если игнорировать? - [Абстракции](/docs/about/understanding/abstractions.md): Закон дырявых абстракций - [Об архитектуре](/docs/about/understanding/architecture.md): Проблемы - [Типы знаний в проекте](/docs/about/understanding/knowledge-types.md): В любом проекте можно выделить следующие "типы знаний": - [Нейминг](/docs/about/understanding/naming.md): У разных разработчиков различный опыт и контекст, что может привести к недопониманию в команде, когда одни и те же сущности называются по-разному. Например: - [О понимании потребностей и о формулировке задач](/docs/about/understanding/needs-driven.md): — Не получается сформулировать цель, которую будет решать новая фича? А может проблема в том, что сама задача не сформулирована? **Смысл ещё и в том, чтобы методология помогла вытащить наружу проблемное определение задач и целей** - [Сигналы архитектуры](/docs/about/understanding/signals.md): Если есть ограничение со стороны архитектуры - значит на то есть явные причины, и последствия, если их игнорировать - [Рекомендации по брендингу](/docs/branding.md): Визуальная айдентика FSD основана на его ключевых концепциях: Layered, Sliced self-contained parts, Parts & Compose, Segmented. - [Памятка по декомпозиции](/docs/get-started/cheatsheet.md): Используйте её как быстрый справочник, когда вы решаете, как разбить ваш интерфейс по слоям. Ниже также доступна PDF-версия, чтобы вы могли распечатать её и держать под подушкой. - [FAQ](/docs/get-started/faq.md): Свой вопрос можно задать в Telegram-чате, Discord-сообществе и GitHub Discussions. - [Обзор](/docs/get-started/overview.md): Feature-Sliced Design (FSD) — это архитектурная методология для проектирования фронтенд-приложений. Проще говоря, это набор правил и соглашений по организации кода. Главная цель этой методологии — сделать проект понятнее и стабильнее в условиях постоянно меняющихся бизнес-требований. - [Туториал](/docs/get-started/tutorial.md): Часть 1. На бумаге - [Обработка API-запросов](/docs/guides/examples/api-requests.md): handling-api-requests} - [Авторизация](/docs/guides/examples/auth.md): В общих чертах авторизация состоит из следующих этапов: - [Автокомплит](/docs/guides/examples/autocompleted.md): Про декомпозицию по слоям - [Browser API](/docs/guides/examples/browser-api.md): Про работу с Browser API: localStorage, audioApi, bluetoothAPI и т.п. - [CMS](/docs/guides/examples/cms.md): Фичи бывают разные - [Обратная связь](/docs/guides/examples/feedback.md): Errors, Alerts, Notifications, ... - [i18n](/docs/guides/examples/i18n.md): Куда положить? Как с этим работать? - [Метрика](/docs/guides/examples/metric.md): Про способы инициализировать метрики в приложении - [Монорепозитории](/docs/guides/examples/monorepo.md): Про применимость для монорепозиториев, про bff, про микроаппы - [Лейауты страниц](/docs/guides/examples/page-layout.md): Это руководство рассматривает абстракцию лейаута страницы — когда несколько страниц имеют одинаковую структуру, отличаясь только основным содержимым. - [Desktop/Touch платформы](/docs/guides/examples/platforms.md): Про применение методологии для desktop/touch - [SSR](/docs/guides/examples/ssr.md): Про реализацию SSR с применением методологии - [Темизация](/docs/guides/examples/theme.md): Куда положить работу с темой и палитрой? - [Типы](/docs/guides/examples/types.md): В этом руководстве рассматриваются типы данных из типизированных языков, таких как TypeScript, и где они вписываются в FSD. - [White Labels](/docs/guides/examples/white-labels.md): Figma, brand uikit, templates, адаптируемость к брендам - [Кросс-импорты](/docs/guides/issues/cross-imports.md): Кросс-импорты появляются тогда, когда слой/абстракция начинает брать слишком много ответственности, чем должна. Именно поэтому методология выделяет новые слои, которые позволяют расцепить эти кросс-импорты - [Десегментация](/docs/guides/issues/desegmented.md): Ситуация - [Роутинг](/docs/guides/issues/routes.md): Ситуация - [Миграция с кастомной архитектуры](/docs/guides/migration/from-custom.md): Это руководство описывает подход, который может быть полезен при миграции с кастомной самодельной архитектуры на Feature-Sliced Design. - [Миграция с v1](/docs/guides/migration/from-v1.md): Зачем v2? - [Миграция с v2.0 на v2.1](/docs/guides/migration/from-v2-0.md): Основным изменением в v2.1 является новая ментальная модель разложения интерфейса — сначала страницы. - [Использование с Electron](/docs/guides/tech/with-electron.md): Electron-приложения имеют особую архитектуру, состоящую из нескольких процессов с разными ответственностями. Применение FSD в таком контексте требует адаптации структуры под специфику Electron. - [Использование с Next.js](/docs/guides/tech/with-nextjs.md): FSD совместим с Next.js как в варианте App Router, так и в варианте Pages Router, если устранить главный конфликт — папки app и pages. - [Использование с NuxtJS](/docs/guides/tech/with-nuxtjs.md): В NuxtJS проекте возможно реализовать FSD, однако возникают конфликты из-за различий между требованиями к структуре проекта NuxtJS и принципами FSD: - [Использование с React Query](/docs/guides/tech/with-react-query.md): Проблема «куда положить ключи» - [Использование с SvelteKit](/docs/guides/tech/with-sveltekit.md): В SvelteKit проекте возможно реализовать FSD, однако возникают конфликты из-за различий между требованиями к структуре проекта SvelteKit и принципами FSD: - [Docs for LLMs](/docs/llms.md): This page provides links and guidance for LLM crawlers. - [Слои](/docs/reference/layers.md): Слои - это первый уровень организационной иерархии в Feature-Sliced Design. Их цель - разделить код на основе того, сколько ответственности ему требуется и от скольких других модулей в приложении он зависит. Каждый слой несет особое семантическое значение, чтобы помочь вам определить, сколько ответственности следует выделить вашему коду. - [Публичный API](/docs/reference/public-api.md): Публичный API — это контракт между группой модулей, например, слайсом, и кодом, который его использует. Он также действует как ворота, разрешая доступ только к определенным объектам и только через этот публичный API. - [Слайсы и сегменты](/docs/reference/slices-segments.md): Слайсы - [Feature-Sliced Design](/index.md): Architectural methodology for frontend projects --- # Full Documentation Content v2 ![](/documentation/ru/assets/ideal-img/tiny-bunny.dd60f55.640.png) Tiny Bunny Mini Game Mini-game "21 points" in the universe of the visual novel "Tiny Bunny". reactredux-toolkittypescript [Website](https://sanua356.github.io/tiny-bunny/)[Source](https://github.com/sanua356/tiny-bunny) --- # 🧭 Навигация ## Устаревшие ссылки После реструктуризации документации, некоторые ссылки на статьи изменились. Ниже можно найти страницу, которую вы, возможно, искали. Но для совместимости есть редиректы со старых ссылок ### 🚀 Get Started ⚡️ Simplified and merged [Tutorial](/documentation/ru/docs/get-started/tutorial.md) [**old**:](/documentation/ru/docs/get-started/tutorial.md) [/docs/get-started/quick-start](/documentation/ru/docs/get-started/tutorial.md) [**new**: ](/documentation/ru/docs/get-started/tutorial.md) [/docs/get-started/tutorial](/documentation/ru/docs/get-started/tutorial.md) [Basics](/documentation/ru/docs/get-started/overview.md) [**old**:](/documentation/ru/docs/get-started/overview.md) [/docs/get-started/basics](/documentation/ru/docs/get-started/overview.md) [**new**: ](/documentation/ru/docs/get-started/overview.md) [/docs/get-started/overview](/documentation/ru/docs/get-started/overview.md) [Decompose Cheatsheet](/documentation/ru/docs/get-started/cheatsheet.md) [**old**:](/documentation/ru/docs/get-started/cheatsheet.md) [/docs/get-started/tutorial/decompose; /docs/get-started/tutorial/design-mockup; /docs/get-started/onboard/cheatsheet](/documentation/ru/docs/get-started/cheatsheet.md) [**new**: ](/documentation/ru/docs/get-started/cheatsheet.md) [/docs/get-started/cheatsheet](/documentation/ru/docs/get-started/cheatsheet.md) ### 🍰 Alternatives ⚡️ Moved and merged to /about/alternatives as advanced materials [Architecture approaches alternatives](/documentation/ru/docs/about/alternatives.md) [**old**:](/documentation/ru/docs/about/alternatives.md) [/docs/about/alternatives/big-ball-of-mud; /docs/about/alternatives/design-principles; /docs/about/alternatives/ddd; /docs/about/alternatives/clean-architecture; /docs/about/alternatives/frameworks; /docs/about/alternatives/atomic-design; /docs/about/alternatives/smart-dumb-components; /docs/about/alternatives/feature-driven](/documentation/ru/docs/about/alternatives.md) [**new**: ](/documentation/ru/docs/about/alternatives.md) [/docs/about/alternatives](/documentation/ru/docs/about/alternatives.md) ### 🍰 Promote & Understanding ⚡️ Moved to /about as advanced materials [Knowledge types](/documentation/ru/docs/about/understanding/knowledge-types.md) [**old**:](/documentation/ru/docs/about/understanding/knowledge-types.md) [/docs/reference/knowledge-types](/documentation/ru/docs/about/understanding/knowledge-types.md) [**new**: ](/documentation/ru/docs/about/understanding/knowledge-types.md) [/docs/about/understanding/knowledge-types](/documentation/ru/docs/about/understanding/knowledge-types.md) [Needs driven](/documentation/ru/docs/about/understanding/needs-driven.md) [**old**:](/documentation/ru/docs/about/understanding/needs-driven.md) [/docs/concepts/needs-driven](/documentation/ru/docs/about/understanding/needs-driven.md) [**new**: ](/documentation/ru/docs/about/understanding/needs-driven.md) [/docs/about/understanding/needs-driven](/documentation/ru/docs/about/understanding/needs-driven.md) [About architecture](/documentation/ru/docs/about/understanding/architecture.md) [**old**:](/documentation/ru/docs/about/understanding/architecture.md) [/docs/concepts/architecture](/documentation/ru/docs/about/understanding/architecture.md) [**new**: ](/documentation/ru/docs/about/understanding/architecture.md) [/docs/about/understanding/architecture](/documentation/ru/docs/about/understanding/architecture.md) [Naming adaptability](/documentation/ru/docs/about/understanding/naming.md) [**old**:](/documentation/ru/docs/about/understanding/naming.md) [/docs/concepts/naming-adaptability](/documentation/ru/docs/about/understanding/naming.md) [**new**: ](/documentation/ru/docs/about/understanding/naming.md) [/docs/about/understanding/naming](/documentation/ru/docs/about/understanding/naming.md) [Signals of architecture](/documentation/ru/docs/about/understanding/signals.md) [**old**:](/documentation/ru/docs/about/understanding/signals.md) [/docs/concepts/signals](/documentation/ru/docs/about/understanding/signals.md) [**new**: ](/documentation/ru/docs/about/understanding/signals.md) [/docs/about/understanding/signals](/documentation/ru/docs/about/understanding/signals.md) [Abstractions of architecture](/documentation/ru/docs/about/understanding/abstractions.md) [**old**:](/documentation/ru/docs/about/understanding/abstractions.md) [/docs/concepts/abstractions](/documentation/ru/docs/about/understanding/abstractions.md) [**new**: ](/documentation/ru/docs/about/understanding/abstractions.md) [/docs/about/understanding/abstractions](/documentation/ru/docs/about/understanding/abstractions.md) ### 📚 Reference guidelines (isolation & units) ⚡️ Moved to /reference as theoretical materials (old concepts) [Decouple of entities](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [/docs/concepts/decouple-entities](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [Low Coupling & High Cohesion](/documentation/ru/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [**old**:](/documentation/ru/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [/docs/concepts/low-coupling](/documentation/ru/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [**new**: ](/documentation/ru/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [/docs/reference/slices-segments#zero-coupling-high-cohesion](/documentation/ru/docs/reference/slices-segments.md#zero-coupling-high-cohesion) [Cross-communication](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [/docs/concepts/cross-communication](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [App splitting](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/concepts/app-splitting](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Decomposition](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/units/decomposition](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Units](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/units](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Layers](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/units/layers](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Layer overview](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers/overview](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [App layer](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/units/layers/app](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Processes layer](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/units/layers/processes](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Pages layer](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/units/layers/pages](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Widgets layer](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/units/layers/widgets](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Widgets layer](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers/widgets](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Features layer](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/units/layers/features](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Entities layer](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/units/layers/entities](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Shared layer](/documentation/ru/docs/reference/layers.md) [**old**:](/documentation/ru/docs/reference/layers.md) [/docs/reference/units/layers/shared](/documentation/ru/docs/reference/layers.md) [**new**: ](/documentation/ru/docs/reference/layers.md) [/docs/reference/layers](/documentation/ru/docs/reference/layers.md) [Segments](/documentation/ru/docs/reference/slices-segments.md) [**old**:](/documentation/ru/docs/reference/slices-segments.md) [/docs/reference/units/segments](/documentation/ru/docs/reference/slices-segments.md) [**new**: ](/documentation/ru/docs/reference/slices-segments.md) [/docs/reference/slices-segments](/documentation/ru/docs/reference/slices-segments.md) ### 🎯 Bad Practices handbook ⚡️ Moved to /guides as practice materials [Cross-imports](/documentation/ru/docs/guides/issues/cross-imports.md) [**old**:](/documentation/ru/docs/guides/issues/cross-imports.md) [/docs/concepts/issues/cross-imports](/documentation/ru/docs/guides/issues/cross-imports.md) [**new**: ](/documentation/ru/docs/guides/issues/cross-imports.md) [/docs/guides/issues/cross-imports](/documentation/ru/docs/guides/issues/cross-imports.md) [Desegmented](/documentation/ru/docs/guides/issues/desegmented.md) [**old**:](/documentation/ru/docs/guides/issues/desegmented.md) [/docs/concepts/issues/desegmented](/documentation/ru/docs/guides/issues/desegmented.md) [**new**: ](/documentation/ru/docs/guides/issues/desegmented.md) [/docs/guides/issues/desegmented](/documentation/ru/docs/guides/issues/desegmented.md) [Routes](/documentation/ru/docs/guides/issues/routes.md) [**old**:](/documentation/ru/docs/guides/issues/routes.md) [/docs/concepts/issues/routes](/documentation/ru/docs/guides/issues/routes.md) [**new**: ](/documentation/ru/docs/guides/issues/routes.md) [/docs/guides/issues/routes](/documentation/ru/docs/guides/issues/routes.md) ### 🎯 Examples ⚡️ Grouped and simplified into /guides/examples as practical examples [Viewer logic](/documentation/ru/docs/guides/examples/auth.md) [**old**:](/documentation/ru/docs/guides/examples/auth.md) [/docs/guides/examples/viewer](/documentation/ru/docs/guides/examples/auth.md) [**new**: ](/documentation/ru/docs/guides/examples/auth.md) [/docs/guides/examples/auth](/documentation/ru/docs/guides/examples/auth.md) [Monorepo](/documentation/ru/docs/guides/examples/monorepo.md) [**old**:](/documentation/ru/docs/guides/examples/monorepo.md) [/docs/guides/monorepo](/documentation/ru/docs/guides/examples/monorepo.md) [**new**: ](/documentation/ru/docs/guides/examples/monorepo.md) [/docs/guides/examples/monorepo](/documentation/ru/docs/guides/examples/monorepo.md) [White Labels](/documentation/ru/docs/guides/examples/white-labels.md) [**old**:](/documentation/ru/docs/guides/examples/white-labels.md) [/docs/guides/white-labels](/documentation/ru/docs/guides/examples/white-labels.md) [**new**: ](/documentation/ru/docs/guides/examples/white-labels.md) [/docs/guides/examples/white-labels](/documentation/ru/docs/guides/examples/white-labels.md) ### 🎯 Migration ⚡️ Grouped and simplified into /guides/migration as migration guidelines [Migration from V1](/documentation/ru/docs/guides/migration/from-v1.md) [**old**:](/documentation/ru/docs/guides/migration/from-v1.md) [/docs/guides/migration-from-v1](/documentation/ru/docs/guides/migration/from-v1.md) [**new**: ](/documentation/ru/docs/guides/migration/from-v1.md) [/docs/guides/migration/from-v1](/documentation/ru/docs/guides/migration/from-v1.md) [Migration from Legacy](/documentation/ru/docs/guides/migration/from-custom.md) [**old**:](/documentation/ru/docs/guides/migration/from-custom.md) [/docs/guides/migration-from-legacy](/documentation/ru/docs/guides/migration/from-custom.md) [**new**: ](/documentation/ru/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-custom](/documentation/ru/docs/guides/migration/from-custom.md) ### 🎯 Tech ⚡️ Grouped into /guides/tech as tech-specific usage guidelines [Usage with NextJS](/documentation/ru/docs/guides/tech/with-nextjs.md) [**old**:](/documentation/ru/docs/guides/tech/with-nextjs.md) [/docs/guides/usage-with-nextjs](/documentation/ru/docs/guides/tech/with-nextjs.md) [**new**: ](/documentation/ru/docs/guides/tech/with-nextjs.md) [/docs/guides/tech/with-nextjs](/documentation/ru/docs/guides/tech/with-nextjs.md) ### Rename 'legacy' to 'custom' ⚡️ 'Legacy' is derogatory, we don't get to call people's projects legacy [Rename 'legacy' to custom](/documentation/ru/docs/guides/migration/from-custom.md) [**old**:](/documentation/ru/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-legacy](/documentation/ru/docs/guides/migration/from-custom.md) [**new**: ](/documentation/ru/docs/guides/migration/from-custom.md) [/docs/guides/migration/from-custom](/documentation/ru/docs/guides/migration/from-custom.md) ### Deduplication of Reference ⚡️ Cleaned up the Reference section and deduplicated the material [Isolation of modules](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [**old**:](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/isolation](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [**new**: ](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) [/docs/reference/layers#import-rule-on-layers](/documentation/ru/docs/reference/layers.md#import-rule-on-layers) --- [Перейти к основному содержимому](#__docusaurus_skipToContent_fallback) [![logo](/documentation/ru/img/brand/logo-primary.png)![logo](/documentation/ru/img/brand/logo-primary.png)](/documentation/ru/.md) [****](/documentation/ru/.md)[📖 Документация](/documentation/ru/docs/get-started/overview.md)[💫 Сообщество](/documentation/ru/community.md)[📝 Блог](/documentation/ru/blog)[🛠 Примеры](/documentation/ru/examples.md) [v2.1](/documentation/ru/docs/get-started/overview.md) * [v2.1](/documentation/ru/docs/get-started/overview.md) * [v1.0](https://feature-sliced.github.io/featureslices.dev/v1.0.html) * [v0.1](https://feature-sliced.github.io/featureslices.dev/v0.1.html) * [feature-driven](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) * [All versions](/documentation/ru/versions.md) [Русский](#) * [Русский](/documentation/ru/search.md) * [English](/documentation/search) * [O'zbekcha](/documentation/uz/search) * [한국어](/documentation/kr/search) * [日本語](/documentation/ja/search) * [Help Us Translate](https://github.com/feature-sliced/documentation/issues/244) [](https://discord.gg/S8MzWTUsmp)[](https://github.com/feature-sliced/documentation) Поиск # Поиск по сайту Введите фразу для поиска [](https://www.algolia.com/) Спецификация * [Документация](/documentation/ru/docs/get-started/overview.md) * [Сообщество](/documentation/ru/community.md) * [Помощь](/documentation/ru/nav.md) * [Обсуждения](https://github.com/feature-sliced/documentation/discussions) Сообщество * [Discord](https://discord.gg/S8MzWTUsmp) * [Telegram (RU)](https://t.me/feature_sliced) * [Twitter](https://twitter.com/feature_sliced) * [Open Collective](https://opencollective.com/feature-sliced) * [YouTube](https://www.youtube.com/c/FeatureSlicedDesign) Остальное * [GitHub](https://github.com/feature-sliced) * [Contribution Guide](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md) * [Лицензия](https://github.com/feature-sliced/documentation/blob/master/LICENSE) * [Docs for LLMs](/documentation/ru/docs/llms.md) [![Feature-Sliced Design - Architectural methodology for frontend projects](/documentation/ru/img/brand/logo-primary.png)![Feature-Sliced Design - Architectural methodology for frontend projects](/documentation/ru/img/brand/logo-primary.png)](https://github.com/feature-sliced) Copyright © 2018-2025 Feature-Sliced Design --- # Версии Feature-Sliced Design ### Feature-Sliced Design v2.1 (Current) Здесь можно найти документацию для текущей опубликованной версии | v2.1 | [Release Notes](https://github.com/feature-sliced/documentation/releases/tag/v2.1) | [Documentation](/documentation/ru/docs/get-started/overview.md) | [Migration from v1](/documentation/ru/docs/guides/migration/from-v1.md) | [Migration from v2.0](/documentation/ru/docs/guides/migration/from-v1.md) | | ---- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------- | ### Feature Slices v1 (Legacy) Здесь можно найти документацию для старых версий feature-slices | v1.0 | [Documentation](https://feature-sliced.github.io/featureslices.dev/v1.0.html) | | ---- | ----------------------------------------------------------------------------- | | v0.1 | [Documentation](https://feature-sliced.github.io/featureslices.dev/v0.1.html) | ### Feature Driven (Legacy) Здесь можно найти документацию для старых версий feature-driven | v0.1 | [Documentation](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) | | ------------- | --------------------------------------------------------------------------------------- | | Example (kof) | [Github](https://github.com/kof/feature-driven-architecture) | --- # 💫 Community Community resources, additional materials ## Main[​](#main "Прямая ссылка на этот заголовок") [Awesome Resources](https://github.com/feature-sliced/awesome) [A curated list of awesome FSD videos, articles, packages](https://github.com/feature-sliced/awesome) [Team](/documentation/ru/community/team.md) [Core-team, Champions, Contributors, Companies](/documentation/ru/community/team.md) [Brandbook](/documentation/ru/docs/branding.md) [Recommendations for FSD's branding usage](/documentation/ru/docs/branding.md) [Contributing](#) [HowTo, Workflow, Support](#) --- # Team WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/192) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Core-team[​](#core-team "Прямая ссылка на этот заголовок") ### Champions[​](#champions "Прямая ссылка на этот заголовок") ## Contributors[​](#contributors "Прямая ссылка на этот заголовок") ## Companies[​](#companies "Прямая ссылка на этот заголовок") --- # Альтернативы WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/62) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* История архитектурных подходов ## Big Ball of Mud[​](#big-ball-of-mud "Прямая ссылка на этот заголовок") WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/258) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Что это; Почему так распространено; Когда это начинает приносить проблемы; Как быть; И как помогает в этом FSD * [(Статья) Oleg Isonen - Last words on UI architecture before an AI takes over](https://oleg008.medium.com/last-words-on-ui-architecture-before-an-ai-takes-over-468c78f18f0d) * [(Доклад) Юлия Николаева, iSpring - Big Ball of Mud и другие проблемы монолита, с которыми мы справились](https://youtu.be/gna4Ynz1YNI) * [(Статья) DDD - Big Ball of mud](https://thedomaindrivendesign.io/big-ball-of-mud/) ## Smart & Dumb components[​](#smart--dumb-components "Прямая ссылка на этот заголовок") WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/214) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > О подходе; О применимости в фронтенде; позиция FSD Про устарелость, про новый взгляд со стороны методологии Почему компонентно/контейнерный подход - зло * [(Статья) Dan Abramov - Presentational and Container Components (TLDR: deprecated)](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) ## Design Principles[​](#design-principles "Прямая ссылка на этот заголовок") WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/59) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Про что речь; позиция FSD SOLID, GRASP, KISS, YAGNI, ... - и почему они плохо работают вместе на практике И как она агрегирует эти практики * [(Доклад) Илья Азин - Feature-Sliced Design (фрагмент про Принципы проектирования)](https://youtu.be/SnzPAr_FJ7w?t=380) ## DDD[​](#ddd "Прямая ссылка на этот заголовок") WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/1) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > О подходе; Почему плохо работает на практике В чем отличие, чем улучшает применимость, где перенимает практики ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Статья) DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/) * [(Доклад) Илья Азин - Feature-Sliced Design (фрагмент про Clean Architecture, DDD)](https://youtu.be/SnzPAr_FJ7w?t=528) ## Clean Architecture[​](#clean-architecture "Прямая ссылка на этот заголовок") WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/165) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > О подходе; О применимости в фронтенде; позиция FSD В чем схожи (многим), чем отличаются * [(Тред) Про use-case/interactor в методологии](https://t.me/feature_sliced/3897) * [(Тред) Про DI в методологии](https://t.me/feature_sliced/4592) * [(Статья) Александр Беспоясов - Чистая архитектура на фронтенде](https://bespoyasov.ru/blog/clean-architecture-on-frontend/) * [(Статья) DDD, Hexagonal, Onion, Clean, CQRS, … How I put it all together](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/) * [(Доклад) Илья Азин - Feature-Sliced Design (фрагмент про Clean Architecture, DDD)](https://youtu.be/SnzPAr_FJ7w?t=528) * [(Статья) Заблуждения Clean Architecture](https://habr.com/ru/company/mobileup/blog/335382/) ## Frameworks[​](#frameworks "Прямая ссылка на этот заголовок") WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/58) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > О применимости в фронтенде; Почему они не решают проблему; Почему это не единственный подход; позиция FSD Framework-agnostic, conventional-подход * [(Статья) Про причины создания методологии (фрагмент про фреймворки)](/documentation/ru/docs/about/motivation.md) * [(Тред) Про применимость методологии для разных фреймворков](https://t.me/feature_sliced/3867) ## Atomic Design[​](#atomic-design "Прямая ссылка на этот заголовок") ### Что это?[​](#что-это "Прямая ссылка на этот заголовок") В Atomic Design сфера ответственности разделена на стандартизированные слои.  Atomic Design разбивается на **5 слоев** (сверху вниз): 1. `pages` - Назначение аналогично слою `pages` в FSD. 2. `templates` - Компоненты задающие структуру страницы, без привязки к контенту. 3. `organisms` - Модули состоящие из молекул, обладающие бизнес логикой. 4. `moleculs` - Более сложные компоненты, в которых, как правило, нет бизнес логики. 5. `atoms` - UI компоненты без бизнес логики. Модули на одном слое взаимодействуют только с модулями, находящимися на слоях ниже, как в FSD. То есть, молекулы строятся из атомов, организмы из молекул, шаблоны из организмов, страницы из шаблонов. Также Atomic Design подразумевает использование **Public API** внутри модулей для изоляции. ### Применимость во фронтенде[​](#применимость-во-фронтенде "Прямая ссылка на этот заголовок") Atomic Design относительно часто встречается в проектах. Atomic Design популярнее среди веб-дизайнеров,  нежели в разработке. Веб-дизайнеры часто используют Atomic Design для создания масштабируемых и легко поддерживаемых дизайнов.  В разработке Atomic Design часто смешивается с другими архитектурными методологиями. Однако, так как Atomic Design концентрирует внимание на UI компонентах и их композицию, возникает проблема реализации бизнес логики в рамках архитектуры. Проблема заключается в том, что Atomic Design не предусматривает четкого уровня ответственности для бизнес-логики,  что приводит к распределению по различным компонентам и уровням, усложняя поддержку и тестирование.  Бизнес-логика становится размыта, что затрудняет четкое разделение ответственности и делает код менее модульным и переиспользуемым. ### Как оно сочетается с FSD?[​](#как-оно-сочетается-с-fsd "Прямая ссылка на этот заголовок") В контексте FSD некоторые элементы Atomic Design могут быть применены для  создания гибких и масштабируемых UI компонентов. Слои `atoms` и `molecules` можно реализовать в  `shared/ui` в FSD, что упрощает переиспользование и поддержку базовых UI элементов.  ``` ├── shared │   ├── ui  │   │   ├── atoms │   │   ├── molecules │   ... ``` Сравнение FSD и Atomic Design показывает, что обе методологии стремятся к модульности и переиспользованию,  но акцентируют внимание на разных аспектах. Atomic Design ориентирован на визуальные компоненты  и их композицию. FSD фокусируется на разбиении функциональности приложения на независимые модули и их взаимосвязи. * [Методология Atomic Design](https://atomicdesign.bradfrost.com/table-of-contents/) * [(Тред) Про применимость в shared/ui](https://t.me/feature_sliced/1653) * [(Видео) Кратко о Atomic Design](https://youtu.be/Yi-A20x2dcA) * [(Доклад) Илья Азин - Feature-Sliced Design (фрагмент про Atomic Design)](https://youtu.be/SnzPAr_FJ7w?t=587) ## Feature Driven[​](#feature-driven "Прямая ссылка на этот заголовок") WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/219) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > О подходе; О применимости в фронтенде; позиция FSD Про совместимость, историческое развитие и сравнение * [(Доклад) Oleg Isonen - Feature Driven Architecture](https://youtu.be/BWAeYuWFHhs) * [Feature Driven - Краткая спецификация (с точки зрения FSD)](https://github.com/feature-sliced/documentation/tree/rc/feature-driven) --- # Миссия Здесь описаны цели и ограничения применимости методологии, которыми мы руководствуемся при разработке методологии * Мы видим нашу цель, как баланс между идеологией и простотой * Мы не сможем сделать серебряную пулю, которая подходит всем **Тем не менее, хотелось бы, чтобы методология была близка и доступна достаточно обширному кругу разработчиков** ## Цели[​](#goals "Прямая ссылка на этот заголовок") ### Интуитивная понятность для широкого круга разработчиков[​](#intuitive-clarity-for-a-wide-range-of-developers "Прямая ссылка на этот заголовок") Методология должна быть доступна - большей части команды в проектах *Т.к. даже со всем будущим инструментарием - будет недостаточно того, чтобы методологию понимали только прожженные сеньоры/лиды* ### Решение повседневных проблем[​](#solving-everyday-problems "Прямая ссылка на этот заголовок") В методологии должны быть изложены причины и решения наших повседневных проблем при разработке проектов **А также - приложить ко всему этому инструментарий (cli, линтеры)** Чтобы разработчики могли использовать *выверенный опытом* подход, позволяющий обходить давние проблемы архитектуры и разработки > *@sergeysova: Представьте: разработчик пишет код в рамках методологии, и у него проблемы возникают раз в 10 реже, просто потому что другие люди продумали решение многих проблем.* ## Ограничения[​](#limitations "Прямая ссылка на этот заголовок") Мы не хотим *навязывать нашу точку зрения*, и одновременно понимаем - что *многие наши привычки, как разработчиков, мешают изо дня в день* У всех свой уровень опыта проектирования и разработки систем, **поэтому стоит понимать следующее:** * **Не выйдет**: очень просто, очень понятно, для всех > *@sergeysova: Некоторые концепции невозможно интуитивно понять, пока не столкнешься с проблемами и не проведешь за решением годы.* > > * *Пример из математики — теория графов.* > * *Пример из физики — квантовая механика.* > * *Пример из программирования — архитектура приложений.* * **Возможны и желательны**: простота, расширяемость ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [Проблемы архитектуры](/documentation/ru/docs/about/understanding/architecture.md#problems) --- # Мотивация Главная идея **Feature-Sliced Design** - облегчить и удешевить разработку комплексных и развивающихся проектов, на основании [объединения результатов исследований, обсуждения опыта разного рода широкого круга разработчиков](https://github.com/feature-sliced/documentation/discussions). Очевидно, что это не будет серебряной пулей, и само собой, у методологии будут свои [границы применимости](/documentation/ru/docs/about/mission.md). Тем не менее, возникают резонные вопросы, касаемо *целесообразности такой методологии в целом* примечание Более подробно [обсудили в дискуссии](https://github.com/feature-sliced/documentation/discussions/27) ## Почему не хватает существующих решений?[​](#intuitive-clarity-for-a-wide-range-of-developers "Прямая ссылка на этот заголовок") > Речь обычно о таких аргументах: > > * *"Зачем нужна отдельная новая методология, если уже есть давно зарекомендовавшие себя подходы и принципы проектирования `SOLID`, `KISS`, `YAGNI`, `DDD`, `GRASP`, `DRY` и т.д."* > * *"Все проблемы проекта решаются хорошей документацией, тестами и выстроенными процессами"* > * *"Проблем бы и не было - если бы все разработчики следовали всему выше перечисленному"* > * *"Все придумано уже до вас, вы просто не можете этим пользоваться"* > * *"Возьмите {FRAMEWORK\_NAME} - там решено уже все за вас"* ### Одних принципов недостаточно[​](#principles-alone-are-not-enough "Прямая ссылка на этот заголовок") **Только существования принципов недостаточно для проектирования хорошей архитектуры** Не все их знают до конца, еще меньше правильно понимают и применяют *Принципы проектирования слишком общие, и не дают конкретного ответа на вопрос: "А как спроектировать структуру и архитектуру масштабируемого и гибкого приложения?"* ### Процессы не всегда работают[​](#processes-dont-always-work "Прямая ссылка на этот заголовок") *Документация/Тесты/Процессы* - это, конечно, хорошо, но увы, даже при больших затратах на них - **они не всегда решают поставленных проблем по архитектуре и внедрению новых людей в проект** * Время входа каждого разработчика в проект не сильно уменьшается, т.к. документация чаще всего выйдет огромной / устаревшей * Постоянно следить за тем, что каждый понимает архитектуру одинаково - требует также колоссального количества ресурсов * Не забываем и про bus-factor ### Существующие фреймворки не везде могут быть применены[​](#existing-frameworks-cannot-be-applied-everywhere "Прямая ссылка на этот заголовок") * Имеющиеся решения, как правило, имеют высокий порог входа, из-за чего сложно найти новых разработчиков * Также, чаще всего, выбор технологии уже определен до наступления серьезных проблем в проекте, а потому нужно уметь "работать с тем что есть" - **не привязываясь к технологии** > Q: *"У меня в проекте `React/Vue/Redux/Effector/Mobx/{YOUR_TECH}` - как мне лучше выстроить структуру сущностей и связи между ними?"* ### По итогу[​](#as-a-result "Прямая ссылка на этот заголовок") Получаем *"уникальные как снежинки"* проекты, каждый из которых требует длительного погружения сотрудника, и знания, которые вряд ли будут применимы на другом проекте > @sergeysova: *"Это ровно та ситуация, которая сейчас есть в нашей сфере frontend разработки: каждый лид напридумает себе различных архитектур и структур проекта, при этом не факт, что эти структуры пройдут проверку временем, в итоге кроме него развивать проект могут максимум два человека и каждого нового разработчика нужно погружать снова."* ## Зачем методология разработчикам?[​](#why-do-developers-need-the-methodology "Прямая ссылка на этот заголовок") ### Концентрация на бизнес-фичах, а не на проблемах архитектуры[​](#focus-on-business-features-not-on-architecture-problems "Прямая ссылка на этот заголовок") Методология позволяет экономить ресурсы на проектировании масштабируемой и гибкой архитектуры, вместо этого направляя внимание разработчиков на разработку основной функциональности. При этом стандартизируются и сами архитектурные решения из проекта в проект. *Отдельный вопрос, что методология должна заслужить доверие комьюнити, чтобы другой разработчик мог в имеющиеся у него сроки ознакомиться и положиться на нее при решении проблем своего проекта* ### Проверенное опытом решение[​](#an-experience-proven-solution "Прямая ссылка на этот заголовок") Методология рассчитана на разработчиков, нацеленных на *проверенное опытом решение по проектированию комплексной бизнес-логики* *Однако ясно, что методология - это в целом про набор best-practices, статьи, рассматривающие определенные проблемы и кейсы при разработке. Поэтому - польза от методологии будет и для остального круга разработчиков - кто так или иначе сталкивается с проблемами при разработке и проектировании* ### Здоровье проекта[​](#project-health "Прямая ссылка на этот заголовок") Методология позволит *заблаговременно решать и отслеживать проблемы проекта, не требуя огромного количества ресурсов* **Чаще всего тех.долг копится и копится со временем, и ответственность за его разрешение лежит и на лиде, и на команде** Методология же позволит *заранее предупреждать* возможные проблемы при масштабировании и развитии проекта ## Зачем методология бизнесу?[​](#why-does-a-business-need-a-methodology "Прямая ссылка на этот заголовок") ### Быстрый onboarding[​](#fast-onboarding "Прямая ссылка на этот заголовок") С методологией можно нанять человека в проект, который **уже предварительно знаком с таким подходом, а не обучать заново** *Люди начинают быстрее вникать и приносить пользу проекту, а также появляются дополнительные гарантии найти людей на следующие итерации проекта* ### Проверенное опытом решение[​](#an-experience-proven-solution-1 "Прямая ссылка на этот заголовок") С методологией бизнес получит *решение для большинства вопросов, возникающих при разработке систем* Поскольку чаще всего бизнес хочет получить фреймворк/решение, которое бы решало львиную долю проблем при развитии проекта ### Применимость для разных стадий проекта[​](#applicability-for-different-stages-of-the-project "Прямая ссылка на этот заголовок") Методология может принести пользу проекту *как на этапе поддержки и развития проекта, так и на этапе MVP* Да, на MVP чаще всего важнее *"фичи, а не заложенная на будущее архитектура"*. Но даже в условиях ограниченных сроков, зная best-practices из методологии - можно *"обойтись малой кровью"*, при проектировании MVP-версии системы, находя разумный компромисс (нежели лепить фичи "как попало") *То же самое можно сказать и про тестирование* ## Когда наша методология не нужна?[​](#when-is-our-methodology-not-needed "Прямая ссылка на этот заголовок") * Если проект будет жить короткое время * Если проект не нуждается в поддерживаемой архитектуре * Если бизнес не воспринимает связь кодовой базы и скорости доставки фич * Если бизнесу важнее поскорей закрыть заказы, без дальнейшей поддержки ### Размеры бизнеса[​](#business-size "Прямая ссылка на этот заголовок") * **Малый бизнес** - чаще всего нуждается в готовом и очень быстром решении. Только при росте бизнеса (хотя бы до почти среднего), он понимает - чтобы клиенты продолжали пользоваться, нужно в том числе уделить время качеству и стабильности разрабатываемых решений * **Средний бизнес** - обычно понимает все проблемы разработки, и даже если приходится *"устраивать гонку за фичами"*, он все равно уделяет время на доработки по качеству, рефакторинг и тесты (и само собой - на расширяемую архитектуру) * **Большой бизнес** - обычно уже имеет обширную аудиторию, штат сотрудников, и гораздо более обширный набор своих практик, и наверное даже - свой подход к архитектуре, поэтому идея взять чужую - им приходит не так часто ## Планы[​](#plans "Прямая ссылка на этот заголовок") Основная часть целей [изложена здесь](/documentation/ru/docs/about/mission.md#goals), но помимо этого, стоит проговорить и наши ожидания от методологии в будущем ### Объединение опыта[​](#combining-experience "Прямая ссылка на этот заголовок") Сейчас мы пытаемся объединить весь наш разнородный опыт `core-team`, и получить по итогу закаленную практикой методологию Конечно, мы можем получить по итогу Angular 3.0., но гораздо важней здесь - **исследовать саму проблему проектирования архитектуры сложных систем** *И да - у нас есть претензии и к текущей версии методологии, но мы хотим общими усилиями прийти к единому и оптимальному решению (учитывая, в том числе, и опыт комьюнити)* ### Жизнь вне спецификации[​](#life-outside-the-specification "Прямая ссылка на этот заголовок") Если все сложится хорошо, то методология не будет ограничиваться только спецификацией и тулкитом * Возможно будут и доклады, статьи * Возможно будут `CODE_MODEs` для миграций на другие технологии проектов, написанных согласно методологии * Не исключено, что по итогу сможем дойти и до мейнтейнеров крупных технологических решений * *Особенно для React, по сравнению с другими фреймворками - это главная проблема, т.к. он не говорит как решать определенные проблемы* ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Обсуждение) Методология не нужна?](https://github.com/feature-sliced/documentation/discussions/27) * [О миссии методологии: цели и ограничения](/documentation/ru/docs/about/mission.md) * [Типы знаний в проекте](/documentation/ru/docs/about/understanding/knowledge-types.md) --- # Продвижение в компании WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/206) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Нужна ли методология проекту и компании?[​](#do-the-project-and-the-company-need-a-methodology "Прямая ссылка на этот заголовок") > Про оправданность применения и техдолг ## Как подать методологию бизнесу?[​](#how-can-i-submit-a-methodology-to-a-business "Прямая ссылка на этот заголовок") ## Как подготовить и оправдать план по переезду на методологию?[​](#how-to-prepare-and-justify-a-plan-to-move-to-the-methodology "Прямая ссылка на этот заголовок") --- # Продвижение в команде WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/182) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* * Онбординг новых людей * Гайдлайны по разработке (где искать модули и т.п.) * Новый подход к задачам ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Тред) Простота старых подходов и значение осознанности](https://t.me/feature_sliced/3360) * [(Тред) Про удобство поиска по слоям](https://t.me/feature_sliced/1918) --- # Аспекты интеграции ## Что получаем в конечном счете?[​](#summary "Прямая ссылка на этот заголовок") См. первые 5 минут: [YouTube video player](https://www.youtube.com/embed/TFA6zRO_Cl0?start=2110) ## Также[​](#also "Прямая ссылка на этот заголовок") **Преимущества**: * [Overview](/documentation/ru/docs/get-started/overview.md#advantages) * CodeReview * Onboarding **Недостатки:** * Ментальная сложность * Высокий порог входа * "Layers hell" * Типичные проблемы feature-based подхода --- # Частичное применение WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/199) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Как частично применять методологию? Имеет ли смысл? Что если игнорировать? --- # Абстракции WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/186) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Закон дырявых абстракций[​](#the-law-of-leaky-abstractions "Прямая ссылка на этот заголовок") ## Почему так много абстракций[​](#why-are-there-so-many-abstractions "Прямая ссылка на этот заголовок") > Абстракции помогают справляться со сложностью проекта. Вопрос в том - будут ли эти абстракции специфичны только для этого проекта, или же мы попытаемся вывести общие абстракции на основании специфики фронтенда > Архитектура и приложения в целом изначально сложны, и вопрос только в том как эту сложность лучше распределять и описывать ## Про скоупы ответственности[​](#about-scopes-of-responsibility "Прямая ссылка на этот заголовок") > Про опциональные абстракции ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [Про необходимость в новых слоях](https://t.me/feature_sliced/2801) * [Про сложность в понимании методологии и слоев](https://t.me/feature_sliced/2619) --- # Об архитектуре ## Проблемы[​](#problems "Прямая ссылка на этот заголовок") Обычно, разговор об архитектуре поднимается, когда разработка стопорится из-за тех или иных проблем в проекте. ### Bus-factor & Onboarding[​](#bus-factor--onboarding "Прямая ссылка на этот заголовок") Проект и его архитектуру понимает лишь ограниченный круг людей **Примеры:** * *"Сложно добавить человека в разработку"* * *"На каждую проблему - у каждого свое мнение как обходить" (позавидуем ангуляру)* * *"Не понимаю что происходит в этом большом куске монолита"* ### Неявные и неконтролируемые последствия[​](#implicit-and-uncontrolled-consequences "Прямая ссылка на этот заголовок") Множество неявных сайд-эффектов при разработке/рефакторинге *("все зависит от всего")* **Примеры:** * *"Фича импортирует фичу"* * *"Я обновил(а) стор одной страницы, а отвалилась функциональность на другой"* * *"Логика размазана по всему приложению, и невозможно отследить - где начало, где конец"* ### Неконтролируемое переиспользование логики[​](#uncontrolled-reuse-of-logic "Прямая ссылка на этот заголовок") Сложно переиспользовать/модифицировать существующую логику При этом, обычно есть [две крайности](https://github.com/feature-sliced/documentation/discussions/14): * Либо под каждый модуль пишется логика полностью с нуля *(с возможными повторениями в имеющейся кодовой базе)* * Либо идет тенденция переносить все-все реализуемые модули в `shared` папки, тем самым создавая из нее большую свалку из модулей *(где большинство используется только в одном месте)* **Примеры:** * *"У меня в проекте есть n-реализаций одной и той же бизнес-логики, за что приходится ежедневно расплачиваться"* * *"В проекте есть 6 разных компонентов кнопки/попапа/..."* * *"Свалка хелперов"* ## Требования[​](#requirements "Прямая ссылка на этот заголовок") Поэтому кажется логичным предъявить желаемые *требования к идеальной архитектуре:* примечание Везде где говорится "легко", подразумевается "относительно легко для широкого круга разработчиков", т.к. ясно, что [не получится сделать идеального решения для абсолютно всех](/documentation/ru/docs/about/mission.md#limitations) ### Explicitness[​](#explicitness "Прямая ссылка на этот заголовок") * Должно быть **легко осваивать и объяснять** команде проект и его архитектуру * Структура должна отображать реальные **бизнес-ценности проекта** * Должны быть явными **сайд-эффекты и связи** между абстракциями * Должно быть **легко обнаруживать дублирование логики**, не мешая уникальным реализациям * Не должно быть **распыления логики** по всему проекту * Не должно быть **слишком много разнородных абстракций и правил** для хорошей архитектуры ### Control[​](#control "Прямая ссылка на этот заголовок") * Хорошая архитектура должна **ускорять решение задач, внедрение фич** * Должна быть возможность контролировать разработку проекта * Должно быть легко **расширять, модифицировать, удалять код** * Должна соблюдаться **декомпозиция и изолированность** функциональности * Каждый компонент системы должен быть **легко заменяемым и удаляемым** * *[Не нужно оптимизировать под изменения](https://youtu.be/BWAeYuWFHhs?t=1631) - мы не можем предсказывать будущее* * *[Лучше - оптимизировать под удаление](https://youtu.be/BWAeYuWFHhs?t=1666) - на основании того контекста, который уже имеется* ### Adaptability[​](#adaptability "Прямая ссылка на этот заголовок") * Хорошая архитектура должна быть применима **к большинству проектов** * *С уже существующими инфраструктурными решениями* * *На любой стадии развития* * Не должно быть зависимости от фреймворка и платформы * Должна быть возможность **легко масштабировать проект и команду**, с возможностью параллелизации разработки * Должно быть легко **подстраиваться под изменяющиеся требования и обстоятельства** ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(React Berlin Talk) Oleg Isonen - Feature Driven Architecture](https://youtu.be/BWAeYuWFHhs) * [(React SPB Meetup #1) Sergey Sova - Feature Slices](https://t.me/feature_slices) * [(Статья) Про модуляризацию проектов](https://alexmngn.medium.com/why-react-developers-should-modularize-their-applications-d26d381854c1) * [(Статья) Про Separation of Concerns и структурирование по фичам](https://ryanlanciaux.com/blog/2017/08/20/a-feature-based-approach-to-react-development/) --- # Типы знаний в проекте В любом проекте можно выделить следующие "типы знаний": * **Фундаментальные знания**
Знания, которые не сильно меняются со временем, такие как алгоритмы, computer science, механизмы работы языка программирования и его API. * **Технологический стек**
Знания о наборе технических решений, используемых в проекте, включая языки программирования, фреймворки и библиотеки. * **Проектные знания**
Знания, специфичные для текущего проекта и бесполезные вне этого проекта. Эти знания необходимы новым членам команды, чтоб вносить эффективный вклад. примечание **Feature-Sliced Design** призван уменьшить зависимость от "проектных знаний", взять на себя больше ответственности, и облегчить онбординг новых членов команды. ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Видео) Илья Климов — О типах знаний](https://youtu.be/4xyb_tA-uw0?t=249) --- # Нейминг У разных разработчиков различный опыт и контекст, что может привести к недопониманию в команде, когда одни и те же сущности называются по-разному. Например: * Компоненты для отображения могут называться "ui", "components", "ui-kit", "views", … * Код, который повторно используется во всем приложении, может называться "core", "shared", "app", … * Код бизнес-логики может называться "store", "model", "state", … ## Нейминг в Feature-Sliced Design[​](#naming-in-fsd "Прямая ссылка на этот заголовок") В методологии используются такие специфические термины, как: * "app", "process", "page", "feature", "entity", "shared" как имена слоев, * "ui', "model", "lib", "api", "config" как имена сегментов. Очень важно придерживаться этих терминов, чтобы предотвратить путаницу среди членов команды и новых разработчиков, присоединяющихся к проекту. Использование стандартных названий также помогает при обращении за помощью к сообществу. ## Конфликты нейминга[​](#when-can-naming-interfere "Прямая ссылка на этот заголовок") Конфликты нейминга могут возникать, когда термины, которые используются в методологии FSD, пересекаются с терминами, используемыми в бизнесе: * `FSD#process` vs моделируемый процесс в приложении, * `FSD#page` vs страница журнала, * `FSD#model` vs модель автомобиля. Например, разработчик, увидев в коде слово "процесс", потратит лишнее время, пытаясь понять, какой процесс подразумевается. Такие **коллизии могут нарушить процесс разработки**. Когда глоссарий проекта содержит терминологию, характерную для FSD, крайне важно проявлять осторожность при обсуждении этих терминов с командой и техническими незаинтересованными сторонами. Чтобы эффективно общаться с командой, рекомендуется использовать аббревиатуру "FSD" для префиксации терминов методологии. Например, когда речь идет о процессе, можно сказать: "Мы можем поместить этот процесс на слой FSD features". И наоборот, при общении с нетехническими заинтересованными сторонами лучше ограничивать использование терминологии FSD, а также воздержаться от упоминания внутренней структуры кодовой базы. ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Обсуждение) Адаптивность нейминга](https://github.com/feature-sliced/documentation/discussions/16) * [(Обсуждение) Опрос по неймингу сущностей](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-464894) * [(Обсуждение) "processes" vs "flows" vs ...](https://github.com/feature-sliced/documentation/discussions/20) * [(Обсуждение) "model" vs "store" vs ...](https://github.com/feature-sliced/documentation/discussions/68) --- # О понимании потребностей и о формулировке задач TL;DR — *Не получается сформулировать цель, которую будет решать новая фича? А может проблема в том, что сама задача не сформулирована? **Смысл ещё и в том, чтобы методология помогла вытащить наружу проблемное определение задач и целей*** — *Проект не живет в статике - требования и функциональность постоянно меняются. Со временем, код превращается в кашу, т.к. на старте проект был спроектирован только под изначальный слепок пожеланий. **И задача хорошей архитектуры в том числе - чтобы быть заточенной под изменяющиеся условия разработки.*** ## Зачем?[​](#why "Прямая ссылка на этот заголовок") Чтобы подобрать четкое имя сущности и понять ее составляющие, **нужно отчетливо понимать - какая задача будет решена с помощью всего этого кода.** > *@sergeysova: Во время разработки, мы пытаемся каждой сущности или функции дать имя, которое четко отражает намерения и смысл выполняемого кода.* *Ведь, без понимания задачи, нельзя написать правильные тесты, покрывающие самые важные кейсы, проставить ошибки помогающие пользователю в нужных местах, даже банально не прерывать флоу пользователя из-за исправимых не критичных ошибок.* ## О каких задачах речь?[​](#what-tasks-are-we-talking-about "Прямая ссылка на этот заголовок") Frontend занимается разработкой приложений и интерфейсов для конечных пользователей, значит мы решаем задачи этих потребителей. Когда к нам приходит человек, **он хочет решить какую-то свою боль или закрыть потребность.** *Задача менеджеров и аналитиков - сформулировать эту потребность, а разработчиков реализовать с учетом особенностей веб-разработки (потеря связи, ошибка бэкенда, опечатка, промазал курсором или пальцем).* **Эта самая цель, с которой пришёл пользователь и есть задача разработчиков.** > *Одна маленькая решенная задача и есть feature в методологии Feature-Sliced Design — нужно нарезать весь скоуп задач проекта на маленькие цели.* ## Как это влияет на разработку?[​](#how-does-this-affect-development "Прямая ссылка на этот заголовок") ### Декомпозиция задачи[​](#task-decomposition "Прямая ссылка на этот заголовок") Когда разработчик принимается реализовывать задачу, для упрощения понимания и поддержки кода, он мысленно **нарезает ее на этапы**: * сначала *разбить на верхнеуровневые сущности* и *реализовать их*, * затем эти сущности *разбить на более мелкие* * и так далее *В процессе разбиения на сущности, разработчик вынужден дать им название, которое четко отражало бы его замысел и при чтении листинга помогало понять какую задачу решает код* *При этом не забываем, что пытаемся помочь пользователю уменьшить боль или реализовать потребности* ### Понимание сути задачи[​](#understanding-the-essence-of-the-task "Прямая ссылка на этот заголовок") Но чтобы дать четкое название сущности, **разработчик должен знать предостаточно о ее назначении** * как он собирается использовать эту сущность, * какую часть задачи пользователя она реализует, где ещё эту сущность можно применить, * в каких ещё задачах она может поучаствовать, * и так далее Сделать вывод не сложно: **пока разработчик будет размышлять над названием сущностей в рамках методологии, он сможет найти плохо сформулированные задачи ещё до написания кода.** > Как дать название сущности, если плохо понимаешь, какие задачи она может решать, как вообще можно разбить задачу на сущности, если плохо ее понимаешь? ## Как сформулировать?[​](#how-to-formulate-it "Прямая ссылка на этот заголовок") **Чтобы сформулировать задачу, которая решается фичей, нужно понимать саму задачу**, а это уже область ответственности менеджера проекта и аналитиков. *Методология может лишь подсказать разработчику, на какие задачи стоит обратить пристальное внимание менеджеру продукта.* > *@sergeysova: Весь frontend это в первую очередь отображение информации, любой компонент в первую очередь что-то отображает, а значит задача "показать пользователю что-то" не имеет практической ценности.* > > *Даже без учета специфики frontend можно спросить "а зачем это нужно показывать", так можно продолжать спрашивать до тех пор пока не вылезет боль или потребность потребителя.* Как только мы смогли дойти до базовых потребностей или болей, можно идти обратно и разбираться, **а как именно ваш продукт или сервис может помочь пользователю с его целями** Любая новая задача в вашем трекере направлена на решение задач бизнеса, а бизнес пытается решить задачи пользователя одновременно заработав на нём. А значит, каждая задача несёт в себе определенные цели, даже если они не прописаны в тексте описания. ***Разработчик должен четко понимать, какую цель преследует та или иная задача**, но при этом не каждая компания может позволить себе идеально выстроить процессы, хоть это и отдельный разговор, тем не менее, разработчик вполне может сам "пингануть" нужных менеджеров, чтобы выяснить это и сделать свою часть работы эффективно.* ## А в чем выгода?[​](#and-what-is-the-benefit "Прямая ссылка на этот заголовок") Посмотрим теперь на весь процесс от начала до конца. ### 1. Понимание задач пользователей[​](#1-understanding-user-tasks "Прямая ссылка на этот заголовок") Когда разработчик понимает его боли и то, как бизнес их закрывает, он может предлагать решения, которые бизнесу не доступны в силу специфики веб-разработки. > Но конечно, все это может работать только если разработчику небезразлично то, что он делает и ради чего, а иначе *зачем тогда методология и какие-то подходы?* ### 2. Структуризация и упорядочивание[​](#2-structuring-and-ordering "Прямая ссылка на этот заголовок") С пониманием задач приходит **четкая структура как в голове, так и в задачах вместе с кодом** ### 3. Понимание фичи и ее составляющих[​](#3-understanding-the-feature-and-its-components "Прямая ссылка на этот заголовок") **Одна фича - это одна полезная функциональность для пользователя** * Когда в одной фиче - реализуется несколько - это и есть **нарушение границ** * Фича может быть неделимой и разрастающейся - **и это неплохо** * **Плохо** - когда фича не отвечает на вопрос *"А в чем бизнес-ценность для пользователя?"* * Не может быть фичи `карта-офиса` * А вот `бронирование-переговорки-на-карте`, `поиск-сотрудника`, `смена-рабочего-места` - **да** > *@sergeysova: Смысл в том, чтобы в фиче лежал только код, реализующий непосредственно саму функциональность, без лишних подробностей и внутренних решений (в идеале)* > > *Открываешь код фичи **и видишь только то, что относится к задаче** - не больше* ### 4. Profit[​](#4-profit "Прямая ссылка на этот заголовок") Бизнес крайне редко разворачивает свой курс кардинально в другую сторону, а значит **отражение задач бизнеса в коде frontend приложения это весьма существенный профит.** *Тогда не придётся объяснять каждому новому члену команды, что делает тот или иной код, и вообще ради чего он добавлялся - **все будет объясняться через задачи бизнеса, которые уже отражены в коде.*** > То, что называется ["Язык бизнеса" в Domain Driven Development](https://thedomaindrivendesign.io/developing-the-ubiquitous-language) *** ## Вернемся к реальности[​](#back-to-reality "Прямая ссылка на этот заголовок") Если бизнес-процессы осмыслены и на стадии дизайна даны хорошие имена - *то перенести это понимание и логику в код не особо проблемно.* **Однако на деле**, задачи и функциональность обычно развиваются "слишком" итеративно и (или) нет времени продумывать дизайн. **В итоге фича сегодня имеет смысл, а при расширении этой фичи через месяц можно переписать пол проекта.** > *\[[Из обсуждения](https://t.me/sergeysova/318)]: Разработчик пытается думать на 2-3 шага вперед, учитывая будущие хотелки, но тут упирается в собственный опыт* > > *Прожженный опытом инженер обычно сразу смотрит на 10 шагов вперед, и понимает где одну фичу разделить, а где объединить с другой* > > *Но бывает и так, что приходит задача, с которой не приходилось сталкиваться по опыту, и неоткуда взять понимание - как грамотней декомпозировать, с наименьшими печальными последствиями в будущем* ## Роль методологии[​](#the-role-of-methodology "Прямая ссылка на этот заголовок") **Методология помогает решить проблемы разработчиков, чтобы тем было проще решать проблемы пользователей.** Нет решения задач разработчиков только ради разработчиков Но чтобы разработчик решил свои задачи, **нужно понять задачи пользователя** - наоборот не выйдет ### Требования к методологии[​](#methodology-requirements "Прямая ссылка на этот заголовок") Становится ясно, что нужно выделить как минимум два требования для **Feature-Sliced Design**: 1. Методология должна рассказывать **как создавать фичи, процессы и сущности** * А значит должна четко объяснять *как разделять код между ними*, из чего следует, что именование этих сущностей также должно быть заложено в спецификации. 2. Методология должна помогать архитектуре **[легко адаптироваться под изменяющиеся требования проекта](/documentation/ru/docs/about/understanding/architecture.md#adaptability)** ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Пост) Стимуляция к четкой формулировке задач (+ обсуждение)](https://t.me/sergeysova/318) > ***Текущая статья** является адаптацией этого обсуждения, по ссылке можно ознакомиться с полной неурезанной версией* * [(Обсуждение) Как разбить функциональность и что из себя она представляет](https://t.me/atomicdesign/18972) * [(Статья) "How to better organize your applications"](https://alexmngn.medium.com/how-to-better-organize-your-react-applications-2fd3ea1920f1) --- # Сигналы архитектуры WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/194) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Если есть ограничение со стороны архитектуры - значит на то есть явные причины, и последствия, если их игнорировать > Методология и архитектура дает сигналы, а то как с этим справляться - зависит от того, какие риски готовы взять на себя и что наиболее подойдет вашей команде) ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Тред) Про сигналы от архитектуры и дата-флоу](https://t.me/feature_sliced/2070) * [(Тред) Про фундаментальность архитектуры](https://t.me/feature_sliced/2492) * [(Тред) Про подсвечивание слабых мест](https://t.me/feature_sliced/3979) * [(Тред) Как понять, что модель данных разбухла](https://t.me/feature_sliced/4228) --- # Рекомендации по брендингу Визуальная айдентика FSD основана на его ключевых концепциях: `Layered`, `Sliced self-contained parts`, `Parts & Compose`, `Segmented`. Но мы также стремимся к простой и красивой айдентике, которая бы отражала философию FSD и была бы легко узнаваемой. **Пожалуйста, используйте айдентику FSD "as-is", без изменений, но с нашими ассетами для вашего удобства.** Этот бренд-гайд поможет вам использовать айдентику FSD корректно. Совместимость FSD прежде имел [другую легаси-айдентику](https://drive.google.com/drive/folders/11Y-3qZ_C9jOFoW2UbSp11YasOhw4yBdl?usp=sharing). Старый дизайн не отражал главные концепции методологии. Также это было создано как грубый черновик, который был должен быть актуализирован. Для совместимого и долгосрочного использования бренда, мы тщательно работали над ребрендингом в течение года (2021-2022). **Чтобы вы могли быть уверенными в использовании айдентики FSD 🍰** *Но используйте именно актуальную айдентику, не старую!* ## Название[​](#title "Прямая ссылка на этот заголовок") * ✅ **Правильно:** `Feature-Sliced Design`, `FSD` * ❌ **Неправильно:** `Feature-Sliced`, `Feature Sliced`, `FeatureSliced`, `feature-sliced`, `feature sliced`, `FS` ## Эмодзи[​](#emojii "Прямая ссылка на этот заголовок") Образ торта 🍰 хорошо отражает ключевые концепции FSD, поэтому оно было выбрано как наше фирменное эмодзи. > Пример: *"🍰 Architectural design methodology for Frontend projects"* ## Лого & Палитра[​](#logo--palettte "Прямая ссылка на этот заголовок") FSD имеет несколько вариаций логотипа для разных контекстов, но рекомендовано использовать **primary** | | | | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | | Тема | Лого (Ctrl/Cmd + Click для скачивания) | Использование | | primary
(#29BEDC, #517AED) | [![logo-primary](/documentation/ru/img/brand/logo-primary.png)](/documentation/ru/img/brand/logo-primary.png) | Предпочтительно в большинстве случаев | | flat
(#3193FF) | [![logo-flat](/documentation/ru/img/brand/logo-flat.png)](/documentation/ru/img/brand/logo-flat.png) | Для одноцветного контекста | | monochrome
(#FFF) | [![logo-monocrhome](/documentation/ru/img/brand/logo-monochrome.png)](/documentation/ru/img/brand/logo-monochrome.png) | Для черно-белого контекста | | square
(#3193FF) | [![logo-square](/documentation/ru/img/brand/logo-square.png)](/documentation/ru/img/brand/logo-square.png) | Для квадратных размеров | ## Баннеры & Схемы[​](#banners--schemes "Прямая ссылка на этот заголовок") [![banner-primary](/documentation/ru/img/brand/banner-primary.jpg)](/documentation/ru/img/brand/banner-primary.jpg) [![banner-monochrome](/documentation/ru/img/brand/banner-monochrome.jpg)](/documentation/ru/img/brand/banner-monochrome.jpg) ## Social Preview[​](#social-preview "Прямая ссылка на этот заголовок") Работа в процессе... ## Шаблон для презентаций[​](#presentation-template "Прямая ссылка на этот заголовок") Работа в процессе... ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [Обсуждение (github)](https://github.com/feature-sliced/documentation/discussions/399) * [История ребрендинга с референсами (figma)](https://www.figma.com/file/RPphccpoeasVB0lMpZwPVR/FSD-Brand?node-id=0%3A1) --- # Памятка по декомпозиции Используйте её как быстрый справочник, когда вы решаете, как разбить ваш интерфейс по слоям. Ниже также доступна PDF-версия, чтобы вы могли распечатать её и держать под подушкой. ## Выбор слоя[​](#choosing-a-layer "Прямая ссылка на этот заголовок") [Скачать PDF](/documentation/ru/assets/files/choosing-a-layer-ru-da11f37d28524420f3c747671072cf49.pdf) ![Определения всех слоёв и вопросы для самопроверки](/documentation/ru/assets/images/choosing-a-layer-ru-b9d9bdfa29418ef5443937d8d2dc479e.jpg) ## Примеры[​](#examples "Прямая ссылка на этот заголовок") ### Tweet[​](#tweet "Прямая ссылка на этот заголовок") ![decomposed-tweet-bordered-bgLight](/documentation/ru/assets/images/decompose-twitter-7b9a50f879d763c49305b3bf0751ee35.png) ### GitHub[​](#github "Прямая ссылка на этот заголовок") ![decomposed-github-bordered](/documentation/ru/assets/images/decompose-github-a0eeb839a4b5ef5c480a73726a4451b0.jpg) ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Тред) Общая логика для фич и сущностей](https://t.me/feature_sliced/4262) * [(Тред) Декомпозиция разбухшей логики](https://t.me/feature_sliced/4210) * [(Тред) Про понимание зон ответственности при декомпозиции](https://t.me/feature_sliced/4088) * [(Тред) Декомпозиция виджета ProductList](https://t.me/feature_sliced/3828) * [(Статья) Разные подходы при декомпозиции логики](https://www.pluralsight.com/guides/how-to-organize-your-react-+-redux-codebase) * [(Тред) Про разницу между фичами и сущностями](https://t.me/feature_sliced/3776) * [(Тред) Про разницу между фичами и сущностями (2)](https://t.me/feature_sliced/3248) * [(Тред) Про применение критериев при декомпозиции](https://t.me/feature_sliced/3833) --- # FAQ к сведению Свой вопрос можно задать в [Telegram-чате](https://t.me/feature_sliced), [Discord-сообществе](https://discord.gg/S8MzWTUsmp) и [GitHub Discussions](https://github.com/feature-sliced/documentation/discussions). ### Существует ли тулкит или линтер?[​](#is-there-a-toolkit-or-a-linter "Прямая ссылка на этот заголовок") Да! У нас есть линтер [Steiger](https://github.com/feature-sliced/steiger) для проверки архитектуры вашего проекта и [генераторы папок](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools) через CLI или IDE. ### Где хранить layout/template страниц?[​](#where-to-store-the-layouttemplate-of-pages "Прямая ссылка на этот заголовок") Если вам нужны простые шаблоны разметки, вы можете хранить их в `shared/ui`. Если вам нужно использовать более высокие слои, есть несколько вариантов: * Возможно, вам вообще не нужны лейауты? Если макет состоит всего из нескольких строк, разумно будет дублировать код в каждой странице, а не пытаться абстрагировать его. * Если вам нужны лейауты, вы можете хранить их как отдельные виджеты или страницы, и компоновать их в конфигурации роутера в App. Вложенный роутинг — еще один вариант. ### В чем отличие feature от entity?[​](#what-is-the-difference-between-feature-and-entity "Прямая ссылка на этот заголовок") *Entity* — это понятие из реальной жизни, с которым работает ваше приложение.. *Feature* — это взаимодействие, представляющее реальную ценность для пользователей; что-то, что люди хотят делать с сущностями. Для получения дополнительной информации, а также примеров, см. страницу [про слайсы](/documentation/ru/docs/reference/layers.md#entities) в разделе Reference. ### Могу ли я вкладывать страницы/фичи/сущности друг в друга?[​](#can-i-embed-pagesfeaturesentities-into-each-other "Прямая ссылка на этот заголовок") Да, но это вложение должно происходить в более высоких слоях. Например, внутри виджета вы можете импортировать обе фичи, а затем вставить одну фичу в другую через пропсы/вложение. Вы не можете импортировать одну фичу из другой фичи, это запрещено [**правилом импортов для слоёв**](/documentation/ru/docs/reference/layers.md#import-rule-on-layers). ### А что с Atomic Design?[​](#what-about-atomic-design "Прямая ссылка на этот заголовок") Текущая версия методологии не обязывает, но и не запрещает использовать Atomic Design вместе с Feature-Sliced Design. При этом Atomic Design [хорошо применяется](https://t.me/feature_sliced/1653) для `ui` сегмента модулей. ### Есть ли какие-нибудь полезные ресурсы/статьи/т.д. по FSD?[​](#are-there-any-useful-resourcesarticlesetc-about-fsd "Прямая ссылка на этот заголовок") Да! ### Зачем мне нужен Feature-Sliced Design?[​](#why-do-i-need-feature-sliced-design "Прямая ссылка на этот заголовок") Он помогает вам и вашей команде быстро ознакомиться с проектом с точки зрения его основных компонентов, приносящих бизнес-ценность. Стандартизированная архитектура помогает ускорить онбординг и разрешать споры о структуре кода. См. страницу [Мотивация](/documentation/ru/docs/about/motivation.md), чтобы узнать больше о том, почему FSD был создан. ### Нужна ли архитектура/методология начинающему разработчику?[​](#does-a-novice-developer-need-an-architecturemethodology "Прямая ссылка на этот заголовок") Скорее да, чем нет *Обычно, когда проектируешь разрабатываешь проект в одно лицо - все идет гладко. Но если появляются паузы в разработке, добавляются новые разработчики в команду - тогда-то и наступают проблемы* ### Как мне работать с контекстом авторизации?[​](#how-do-i-work-with-the-authorization-context "Прямая ссылка на этот заголовок") Ответили [здесь](/documentation/ru/docs/guides/examples/auth.md) --- # Обзор **Feature-Sliced Design** (FSD) — это архитектурная методология для проектирования фронтенд-приложений. Проще говоря, это набор правил и соглашений по организации кода. Главная цель этой методологии — сделать проект понятнее и стабильнее в условиях постоянно меняющихся бизнес-требований. Помимо набора правил, FSD — это также целый инструментарий. У нас есть [линтер](https://github.com/feature-sliced/steiger) для проверки архитектуры вашего проекта, [генераторы папок](https://github.com/feature-sliced/awesome?tab=readme-ov-file#tools) через CLI или IDE, а также богатая библиотека [примеров](/documentation/ru/examples.md). ## Подходит ли FSD мне?[​](#is-it-right-for-me "Прямая ссылка на этот заголовок") FSD можно внедрять в проектах и командах любого размера. Она подходит для вашего проекта, если: * Вы занимаетесь **фронтенд**-разработкой (интерфейсы для сайтов, мобильных/десктопных приложений, и т. д.) * Вы разрабатываете **приложение**, а не библиотеку И это все! Нет никаких ограничений на используемый вами язык программирования, фреймворк или стейт-менеджер. Ещё вы можете внедрять FSD постепенно, использовать его в монорепозиториях, и масштабировать его хоть до луны, разделяя ваше приложение на пакеты и внедряя FSD в каждом из них по отдельности. Если у вас уже есть архитектура, и вы подумываете перейти на FSD, убедитесь, что текущая архитектура **создает проблемы** в вашей команде. Например, если ваш проект стал слишком большим и переплетённым, чтоб эффективно разрабатывать новые функции, или если вы ожидаете, что в команду придет много новых участников. Если текущая архитектура работает, возможно, ее не стоит менять. Но если вы всё же решите перейти, ознакомьтесь с рекомендациями в разделе [Миграция](/documentation/ru/docs/guides/migration/from-custom.md). ## Базовый пример[​](#basic-example "Прямая ссылка на этот заголовок") Вот простой проект, реализующий FSD: * `📁 app` * `📁 pages` * `📁 shared` Эти папки верхнего уровня называются *слоями*. Давайте посмотрим глубже: * `📂 app` * `📁 routes` * `📁 analytics` * `📂 pages` * `📁 home` * `📂 article-reader` * `📁 ui` * `📁 api` * `📁 settings` * `📂 shared` * `📁 ui` * `📁 api` Папки внутри `📂 pages` называются *слайсами*. Они делят слой по домену (в данном случае, по страницам). Папки внутри `📂 app`, `📂 shared` и `📂 pages/article-reader` называются *сегментами*, и они делят слайсы (или слои) по техническому назначению, то есть по тому, для чего предназначен код. ## Понятия[​](#concepts "Прямая ссылка на этот заголовок") Слои, слайсы и сегменты образуют иерархию, как показано на схеме: ![Иерархия концепций FSD, описанная ниже](/documentation/ru/assets/images/visual_schema-e826067f573946613dcdc76e3f585082.jpg) На картинке выше: три столбика, обозначенные слева направо как "Слои", "Слайсы" и "Сегменты" соответственно. Столбик "Слои" содержит семь делений, расположенных сверху вниз и обозначенных "app", "processes", "pages", "widgets", "features", "entities" и "shared". Деление "processes" зачеркнуто. Деление "entities" соединено со вторым столбиком "Слайсы", показывая, что второй столбик является содержимым "entities". Столбик "Слайсы" содержит три деления, расположенных сверху вниз и обозначенных "user", "post" и "comment". Деление "post" соединено со столбиком "Сегменты" таким же образом, что и содержимое "post". Столбик "Сегменты" содержит три деления, расположенных сверху вниз и обозначенных "ui", "model" и "api". ### Слои[​](#layers "Прямая ссылка на этот заголовок") Слои стандартизированы во всех проектах FSD. Вам не обязательно использовать все слои, но их названия важны. На данный момент их семь (сверху вниз): 1. **App** — всё, благодаря чему приложение запускается — роутинг, точки входа, глобальные стили, провайдеры и т. д. 2. **Processes** (процессы, устаревший) — сложные межстраничные сценарии. 3. **Pages** (страницы) — полные страницы или большие части страницы при вложенном роутинге. 4. **Widgets** (виджеты) — большие самодостаточные куски функциональности или интерфейса, обычно реализующие целый пользовательский сценарий. 5. **Features** (фичи) — *повторно используемые* реализации целых фич продукта, то есть действий, приносящих бизнес-ценность пользователю. 6. **Entities** (сущности) — бизнес-сущности, с которыми работает проект, например `user` или `product`. 7. **Shared** — переиспользуемый код, особенно когда он отделён от специфики проекта/бизнеса, хотя это не обязательно. Важно Слои **App** и **Shared**, в отличие от других слоев, не имеют слайсов и состоят из сегментов напрямую. Однако для всех остальных слоёв — **Entities**, **Features**, **Widgets** и **Pages**, сохраняется структура, в которой необходимо сначала создать слайс, внутри которого создавать сегменты. Фишка слоев в том, что модули на одном слое могут знать только о модулях со слоев строго ниже, и как следствие, импортировать только с них. ### Слайсы[​](#slices "Прямая ссылка на этот заголовок") Дальше идут слайсы, они делят слой по предметной области. Вы можете называть ваши слайсы как угодно, и создавать их сколько угодно. Слайсы помогают не теряться в проекте, потому что группируют тесно связанный по смыслу код. Слайсы не могут использовать другие слайсы на том же слое, и это обеспечивает сильную связанность кода внутри слайса и слабую сцепленность между слайсами. ### Сегменты[​](#segments "Прямая ссылка на этот заголовок") Слайсы, а также слои App и Shared, состоят из сегментов, а сегменты группируют код по его назначению. Имена сегментов не зафиксированы стандартом, но существует несколько общепринятых имен для наиболее распространенных целей: * `ui` — всё, что связано с отображением: UI-компоненты, форматтеры дат, стили и т.д. * `api` — взаимодействие с бэкендом: функции запросов, типы данных, мапперы. * `model` — модель данных: схемы валидации, интерфейсы, хранилища и бизнес-логика. * `lib` — библиотечный код, который нужен другим модулям этого слайса. * `config` — файлы конфигурации и фиче-флаги. Обычно этих сегментов достаточно для большинства слоев, поэтому свои собственные сегменты обычно создают только в Shared или App, но это не жёсткое правило. ## Преимущества[​](#advantages "Прямая ссылка на этот заголовок") * **Однородность**
Поскольку структура стандартизирована, проекты становятся более единообразными, что облегчает набор новых участников в команду. * **Устойчивость к изменениям и рефакторингу**
Модуль на одном слое не может использовать другие модули на том же слое или слоях выше.
Это позволяет вам вносить изолированные правки без непредвиденных последствий для остальной части приложения. * **Контролируемое переиспользование логики**
В зависимости от уровня вы можете сделать код либо очень переиспользуемым, либо очень локальным.
Это сохраняет баланс между соблюдением принципа **DRY** и практичностью. * **Ориентация на потребности бизнеса и пользователей**
Приложение разделено на бизнес-домены, и при именовании поощряется использование терминологии бизнеса, чтобы вы могли делать полезную работу в продукте, не вникая полностью во все другие несвязанные части проекта. ## Постепенное внедрение[​](#incremental-adoption "Прямая ссылка на этот заголовок") Если у вас есть существующая кодовая база, которую вы хотите перенести на FSD, мы предлагаем следующую стратегию. На нашем собственном опыте миграции она хорошо себя зарекомендовала. 1. Начните постепенно формировать слои App и Shared, чтобы создать фундамент. 2. Раскидайте весь существующий интерфейсный код по виджетам и страницам, даже если у них пока что есть зависимости, нарушающие правила FSD. 3. Постепенно исправляйте нарушения правил на импорты, а по ходу извлекайте сущности и, возможно, фичи. Рекомендуется воздержаться от добавления новых крупных сущностей во время рефакторинга, а также рефакторинга по частям. ## Следующие шаги[​](#next-steps "Прямая ссылка на этот заголовок") * **Хотите разобраться в том, как мыслить по-FSD-шному?** Прочтите [Туториал](/documentation/ru/docs/get-started/tutorial.md). * **Предпочитаете учиться на примерах?** У нас их много в разделе [Примеры](/documentation/ru/examples.md). * **Есть вопросы?** Загляните в наш [чат Telegram](https://t.me/feature_sliced) и спросите у сообщества. --- # Туториал ## Часть 1. На бумаге[​](#часть-1-на-бумаге "Прямая ссылка на этот заголовок") В этом руководстве мы рассмотрим приложение Real World App, также известное как Conduit. Conduit является упрощённым клоном [Medium](https://medium.com/) — он позволяет вам читать и писать статьи в блогах, а также комментировать статьи других людей. ![Главная страница Conduit](/documentation/ru/assets/images/realworld-feed-anonymous-8cbba45f488931979f6c8da8968ad685.jpg) Это довольно небольшое приложение, поэтому мы не станем сильно усложнять разработку излишней декомпозицией. Вероятнее всего, что всё приложение поместится в три слоя: **App**, **Pages** и **Shared**. Если нет, будем вводить дополнительные слои по ходу. Готовы? ### Начните с перечисления страниц[​](#начните-с-перечисления-страниц "Прямая ссылка на этот заголовок") Если мы посмотрим на скриншот выше, мы можем предположить, что по крайней мере, есть следующие страницы: * Домашняя (лента статей) * Войти и зарегистрироваться * Просмотр статей * Редактор статей * Просмотр профилей людей * Редактор профиля (настройки) Каждая из этих страниц станет отдельным *слайсом* на *слое* Pages. Вспомните из обзора, что слайсы — это просто папки внутри слоев, а слои — это просто папки с заранее определенными названиями, например, `pages`. Таким образом, наша папка Pages будет выглядеть так: ``` 📂 pages/ 📁 feed/ (лента) 📁 sign-in/ (войти/зарегистрироваться) 📁 article-read/ (просмотр статей) 📁 article-edit/ (редактор статей) 📁 profile/ (профиль) 📁 settings/ (настройки) ``` Ключевое отличие Feature-Sliced Design от произвольной структуры кода заключается в том, что страницы не могут зависеть друг от друга. То есть одна страница не может импортировать код с другой страницы. Это связано с **правилом импорта для слоёв**: *Модуль (файл) в слайсе может импортировать другие слайсы только в том случае, если они расположены на слоях строго ниже.* В этом случае страница является слайсом, поэтому модули (файлы) внутри этой страницы могут импортировать код только из слоев ниже, а не из других страниц. ### Пристальный взгляд на ленту[​](#пристальный-взгляд-на-ленту "Прямая ссылка на этот заголовок") ![Перспектива анонимного посетителя](/documentation/ru/assets/images/realworld-feed-anonymous-8cbba45f488931979f6c8da8968ad685.jpg) *Перспектива анонимного посетителя* ![Перспектива авторизованного пользователя](/documentation/ru/assets/images/realworld-feed-authenticated-15427d9ff7baae009b47b501bee6c059.jpg) *Перспектива авторизованного пользователя* На странице ленты есть три динамических области: 1. Ссылки для логина, показывающие статус авторизации 2. Список тэгов, фильтрующих ленту 3. Одна—две ленты статей, у каждой статьи кнопка лайка Ссылки для логина — часть заголовка, общего для всех страниц, так что пока что отложим их. #### Список тэгов[​](#список-тэгов "Прямая ссылка на этот заголовок") Чтобы создать список тэгов, нам нужно получить все доступные тэги, отобразить каждый тэг как чип ([chip](https://m3.material.io/components/chips/overview)) и сохранить выбранные тэги в хранилище на стороне клиента. Эти операции относятся к категориям «взаимодействие с API», «пользовательский интерфейс» и «хранение данных». В Feature-Sliced Design код делится по назначению с помощью *сегментов*. Сегменты — это папки в слайсах, и они могут иметь произвольные названия, описывающие их цель, но некоторые цели настолько распространены, что существует несколько общепринятых названий: * 📂 `api/` для взаимодействия с бэкендом * 📂 `ui/` для кода, отвечающего за отображение и внешний вид * 📂 `model/` для хранения данных и бизнес-логики * 📂 `config/` для фиче-флагов, переменных окружения и других форм конфигурации Мы поместим код, который получает тэги, в `api`, сам компонент тэга в `ui`, а взаимодействие с хранилищем в `model`. #### Статьи[​](#статьи "Прямая ссылка на этот заголовок") Следуя той же логике, мы можем разбить ленту статей на те же три сегмента: * 📂 `api/`: получить постраничный список статей с количеством лайков, оставить лайк * 📂 `ui/`: * список вкладок, который может отображать дополнительную вкладку при выборе тэга * отдельная статья * рабочая пагинация * 📂 `model/`: клиентское хранилище загруженных постов и текущей страницы (при необходимости) ### Переиспользование общего кода[​](#переиспользование-общего-кода "Прямая ссылка на этот заголовок") Страницы, как правило, очень отличаются по своей цели, но что-то остается одинаковым по всему приложению — например, UI-кит, соответствующий языку дизайна, или соглашение на бэкенде, что все делается через REST API с конкретным методом аутентификации. Поскольку слайсы должны быть изолированными, переиспользование кода происходит за счёт слоя ниже, **Shared**. Shared отличается от других слоев тем, что он содержит сегменты, а не слайсы. Таким образом, слой Shared представляет собой гибрид между слоем и слайсом. Обычно код в Shared не планируется заранее, а извлекается по ходу разработки, потому что только во время разработки становится ясно, какие части кода действительно переиспользуются. Тем не менее, полезно держать в голове, какой код имеет смысл хранить в Shared: * 📂 `ui/` — UI-кит, только внешний вид, без бизнес-логики. Например, кнопки, диалоги, поля форм. * 📂 `api/` — удобные обёртки вокруг запросов на бэкенд (например, обёртка над `fetch()` в случае веба) * 📂 `config/` — обработка переменных окружения * 📂 `i18n/` — конфигурация поддержки разных языков * 📂 `router/` — примитивы и константы маршрутизации Это лишь примеры сегментов в Shared, вы можете опустить любой из них или создать свой собственный. Единственное, что нужно помнить при создании новых сегментов — названия сегментов должны описывать **цель (почему), а не суть (что)**. Такие названия как `components` , `hooks` или `modals` *не стоит* использовать, потому что они описывают, что содержат эти файлы по сути, а не то, с какой целью писался этот код. Как следствие таких названий, команде приходится копаться в таких папках, чтоб найти нужное. Помимо этого, несвязанный код лежит рядом, из-за чего при рефакторинге затрагивается большая часть приложения, что усложняет ревью и тестирование. ### Определите строгий публичный API[​](#определите-строгий-публичный-api "Прямая ссылка на этот заголовок") В контексте Feature-Sliced Design термин *публичный API* означает, что слайс или сегмент объявляет, что из него могут импортировать другие модули в проекте. Например, в JavaScript это может быть файл `index.js`, который переэкспортирует объекты из других файлов в слайсе. Это обеспечивает свободу рефакторинга внутри слайса до тех пор, пока контракт с внешним миром (т.е. публичный API) остается неизменным. Для слоя Shared, на котором нет слайсов, обычно удобнее определить публичный API (он же индекс) на уровне сегментов, а не один индекс на весь слой. В таком случае импорты из Shared естественным образом организуются по назначению. Для других слоев, на которых слайсы есть, верно обратное — обычно практичнее определить один индекс на слайс и позволить слайсу самому контролировать набор сегментов внутри, потому что другие слои обычно имеют гораздо меньше экспортов и чаще рефакторятся. Наши слайсы/сегменты будут выглядеть друг для друга следующим образом: ``` 📂 pages/ 📂 feed/ 📄 index 📂 sign-in/ 📄 index 📂 article-read/ 📄 index 📁 … 📂 shared/ 📂 ui/ 📄 index 📂 api/ 📄 index 📁 … ``` Все, что находится внутри папок типа `pages/feed` или `shared/ui` , известно только этим папкам, и нет никаких гарантий по содержанию этих папок. ### Крупные переиспользуемые блоки интерфейса[​](#крупные-переиспользуемые-блоки-интерфейса "Прямая ссылка на этот заголовок") Ранее мы хотели отдельно вернуться к переиспользуемому заголовку приложения. Собирать его заново на каждой странице было бы непрактично, поэтому мы его переиспользуем. У нас уже есть слой Shared для переиспользования кода, однако, в случае крупных блоков интерфейса в Shared есть нюанс — слой Shared не должен знать о слоях выше. Между слоями Shared и Pages есть три других слоя: Entities, Features и Widgets. В других проектах на этих слоях может лежать что-то, что хочется использовать в крупном переиспользуемом блоке, и тогда мы не сможем поместить этот блок в Shared, потому что тогда ему придется импортировать со слоёв выше, а это запрещено. Тут приходит на помощь слой Widgets. Он расположен выше Shared, Entities и Features, поэтому он может использовать их всех. В нашем случае заголовок очень простой — это статический логотип и навигация верхнего уровня. Навигация должна спросить у API, авторизован ли сейчас пользователь, но это может быть решено простым импортом из сегмента `api`. Поэтому мы оставим наш заголовок в Shared. ### Пристальный взгляд на страницу с формой[​](#пристальный-взгляд-на-страницу-с-формой "Прямая ссылка на этот заголовок") Давайте также рассмотрим страницу, на которой можно не только читать, но и редактировать. К примеру, редактор статей: ![Редактор статей в Conduit](/documentation/ru/assets/images/realworld-editor-authenticated-10de4d01479270886859e08592045b1e.jpg) Она выглядит тривиально, но содержит несколько аспектов разработки приложений, которые мы еще не исследовали — валидацию форм, состояние ошибки и постоянное хранение данных. Для создания этой страницы нам нужно несколько полей и кнопок из Shared, которые мы соберём в форму в сегменте `ui` этой страницы. Затем, в сегменте `api` мы определим изменяющий запрос, чтобы создать статью на бэкенде. Чтобы проверить запрос перед отправкой, нам нужна схема валидации, и хорошим местом для нее является сегмент `model` , поскольку это модель данных. Там же мы сгенерируем сообщение об ошибке, а отобразим его с помощью ещё одного компонента в сегменте `ui`. Чтобы улучшить пользовательский опыт, мы также можем сохранять введённые данные постоянно, чтобы предотвратить случайную потерю при закрытии браузера. Это тоже подходит под сегмент `model`. ### Итоги[​](#итоги "Прямая ссылка на этот заголовок") Мы разобрали несколько страниц и пришли к базовой структуре нашего приложения: 1. Слой Shared 1. `ui` будет содержать наш переиспользуемый UI-кит 2. `api` будет содержать наши примитивы для взаимодействия с бэкендом 3. Остальное разложим по ходу написания кода 2. Слой Pages — для каждой страницы отдельный слайс 1. `ui` будет содержать саму страницу и составляющие её блоки 2. `api` будет содержать более специализированные функции получения данных, использующие `shared/api` 3. `model` может содержать клиентское хранилище данных, которые мы будем отображать Давайте создадим это приложение! ## Часть 2. В коде[​](#часть-2-в-коде "Прямая ссылка на этот заголовок") Теперь, когда у нас есть план, давайте воплотим его в жизнь. Мы будем использовать React и [Remix](https://remix.run/). Для этого проекта уже есть готовый шаблон, cклонируйте его с GitHub, чтобы начать работу: Установите зависимости с помощью `npm install` и запустите сервер с помощью `npm run dev`. Откройте [http://localhost:3000](http://localhost:3000/), и вы увидите пустое приложение. ### Разложим по страницам[​](#разложим-по-страницам "Прямая ссылка на этот заголовок") Давайте начнем с создания пустых компонентов для всех наших страниц. Выполните следующую команду в своем проекте: ``` npx fsd pages feed sign-in article-read article-edit profile settings --segments ui ``` Это создаст папки наподобие `pages/feed/ui/` и индексный файл `pages/feed/index.ts` для каждой страницы. ### Подключим страницу фида[​](#подключим-страницу-фида "Прямая ссылка на этот заголовок") Давайте подключим корневой маршрут (`/`) нашего приложения к странице фида. Создайте компонент `FeedPage.tsx` в `pages/feed/ui` и поместите в него следующее: pages/feed/ui/FeedPage.tsx ``` export function FeedPage() { return (

conduit

A place to share your knowledge.

); } ``` Затем ре-экспортируйте этот компонент в публичном API страницы фида, файл `pages/feed/index.ts`: pages/feed/index.ts ``` export { FeedPage } from "./ui/FeedPage"; ``` Теперь подключите его к корневому маршруту. В Remix маршрутизация работает на файлах, и файлы маршрутов находятся в папке `app/routes`, что хорошо сочетается с Feature-Sliced Design. Используйте компонент `FeedPage` в `app/routes/_index.tsx`: app/routes/\_index.tsx ``` import type { MetaFunction } from "@remix-run/node"; import { FeedPage } from "pages/feed"; export const meta: MetaFunction = () => { return [{ title: "Conduit" }]; }; export default FeedPage; ``` Затем, если вы запустите dev-сервер и откроете приложение, вы должны увидеть баннер Conduit! ![Баннер Conduit](/documentation/ru/assets/images/conduit-banner-a20e38edcd109ee21a8b1426d93a66b3.jpg) ### API-клиент[​](#api-клиент "Прямая ссылка на этот заголовок") Чтобы общаться с бэкендом RealWorld, давайте создадим удобный API-клиент в Shared. Создайте два сегмента, `api` для клиента и `config` для таких переменных как базовый URL бэкенда: ``` npx fsd shared --segments api config ``` Затем создайте `shared/config/backend.ts`: shared/config/backend.ts ``` export { mockBackendUrl as backendBaseUrl } from "mocks/handlers"; ``` shared/config/index.ts ``` export { backendBaseUrl } from "./backend"; ``` Поскольку проект RealWorld предоставляет [спецификацию OpenAPI](https://github.com/gothinkster/realworld/blob/main/api/openapi.yml), мы можем автоматически сгенерировать типы для нашего API-клиента. Мы будем использовать [пакет `openapi-fetch`](https://openapi-ts.pages.dev/openapi-fetch/), в котором дополнительно есть генератор типов. Выполните следующую команду, чтобы сгенерировать актуальные типы для API: ``` npm run generate-api-types ``` В результате будет создан файл `shared/api/v1.d.ts`. Мы воспользуемся этим файлом в `shared/api/client.ts` для создания типизированного клиента API: shared/api/client.ts ``` import createClient from "openapi-fetch"; import { backendBaseUrl } from "shared/config"; import type { paths } from "./v1"; export const { GET, POST, PUT, DELETE } = createClient({ baseUrl: backendBaseUrl }); ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; ``` ### Реальные данные в ленте[​](#реальные-данные-в-ленте "Прямая ссылка на этот заголовок") Теперь мы можем перейти к получению статей из бэкенда и добавлению их в ленту. Начнем с реализации компонента предпросмотра статьи. Создайте `pages/feed/ui/ArticlePreview.tsx` со следующим содержимым: pages/feed/ui/ArticlePreview\.tsx ``` export function ArticlePreview({ article }) { /* TODO */ } ``` Поскольку мы пишем на TypeScript, было бы неплохо иметь типизированный объект статьи Article. Если мы изучим сгенерированный `v1.d.ts`, то увидим, что объект Article доступен через `components["schemas"]["Article"]`. Поэтому давайте создадим файл с нашими моделями данных в Shared и экспортируем модели: shared/api/models.ts ``` import type { components } from "./v1"; export type Article = components["schemas"]["Article"]; ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; ``` Теперь мы можем вернуться к компоненту предпросмотра статьи и заполнить разметку данными. Обновите компонент, добавив в него следующее содержимое: pages/feed/ui/ArticlePreview\.tsx ``` import { Link } from "@remix-run/react"; import type { Article } from "shared/api"; interface ArticlePreviewProps { article: Article; } export function ArticlePreview({ article }: ArticlePreviewProps) { return (
{article.author.username} {new Date(article.createdAt).toLocaleDateString(undefined, { dateStyle: "long", })}

{article.title}

{article.description}

Read more...
    {article.tagList.map((tag) => (
  • {tag}
  • ))}
); } ``` Кнопка "Мне нравится" пока ничего не делает, мы исправим это, когда перейдем на страницу чтения статей и реализуем функцию "Мне нравится". Теперь мы можем получить статьи и отобразить кучу этих карточек предпросмотра. Получение данных в Remix осуществляется с помощью *загрузчиков* — серверных функций, которые собирают те данные, которые нужны странице. Загрузчики взаимодействуют с API от имени страницы, поэтому мы поместим их в сегмент `api` страницы: pages/feed/api/loader.ts ``` import { json } from "@remix-run/node"; import { GET } from "shared/api"; export const loader = async () => { const { data: articles, error, response } = await GET("/articles"); if (error !== undefined) { throw json(error, { status: response.status }); } return json({ articles }); }; ``` Чтобы подключить его к странице, нам нужно экспортировать его с именем `loader` из файла маршрута: pages/feed/index.ts ``` export { FeedPage } from "./ui/FeedPage"; export { loader } from "./api/loader"; ``` app/routes/\_index.tsx ``` import type { MetaFunction } from "@remix-run/node"; import { FeedPage } from "pages/feed"; export { loader } from "pages/feed"; export const meta: MetaFunction = () => { return [{ title: "Conduit" }]; }; export default FeedPage; ``` И последний шаг — отображение этих карточек в ленте. Обновите `FeedPage` следующим кодом: pages/feed/ui/FeedPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const { articles } = useLoaderData(); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}
); } ``` ### Фильтрация по тегам[​](#фильтрация-по-тегам "Прямая ссылка на этот заголовок") Что касается тегов, то наша задача — получить их из бэкенда и запомнить выбранный пользователем тег. Мы уже знаем, как загружать из бэкенда — это еще один запрос от функции-загрузчика. Мы будем использовать удобную функцию `promiseHash` из пакета `remix-utils`, который уже установлен. Обновите файл загрузчика, `pages/feed/api/loader.ts`, следующим кодом: pages/feed/api/loader.ts ``` import { json } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async () => { return json( await promiseHash({ articles: throwAnyErrors(GET("/articles")), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` Вы можете заметить, что мы вынесли обработку ошибок в общую функцию `throwAnyErrors`. Она выглядит довольно полезной, так что, возможно, мы захотим переиспользовать её позже, а пока давайте просто заметим этот факт. Теперь перейдем к списку тегов. Он должен быть интерактивным - щелчок по тегу должен выбрать этот тег. По традиции Remix, мы будем использовать параметры запроса в URL в качестве хранилища для выбранного тега. Пусть браузер позаботится о хранилище, а мы сосредоточимся на более важных вещах. Обновите `pages/feed/ui/FeedPage.tsx` следующим кодом: pages/feed/ui/FeedPage.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const { articles, tags } = useLoaderData(); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}

Popular Tags

{tags.tags.map((tag) => ( ))}
); } ``` Затем нам нужно использовать параметр поиска тегов в нашем загрузчике. Измените функцию `loader` в `pages/feed/api/loader.ts` на следующую: pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag } } }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` И всё, сегмент `model` нам не понадобился. Remix — клёвая штука. ### Пагинация[​](#пагинация "Прямая ссылка на этот заголовок") Аналогичным образом мы можем реализовать пагинацию. Не стесняйтесь попробовать реализовать её сами или же просто скопируйте код ниже. В любом случае, осуждать вас некому. pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } /** Amount of articles on one page. */ export const LIMIT = 20; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; const page = parseInt(url.searchParams.get("page") ?? "", 10); return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag, limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` pages/feed/ui/FeedPage.tsx ``` import { Form, useLoaderData, useSearchParams } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import { LIMIT, type loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; export function FeedPage() { const [searchParams] = useSearchParams(); const { articles, tags } = useLoaderData(); const pageAmount = Math.ceil(articles.articlesCount / LIMIT); const currentPage = parseInt(searchParams.get("page") ?? "1", 10); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}
    {Array(pageAmount) .fill(null) .map((_, index) => index + 1 === currentPage ? (
  • {index + 1}
  • ) : (
  • ), )}

Popular Tags

{tags.tags.map((tag) => ( ))}
); } ``` Ну вот, это тоже сделали. Есть еще список вкладок, который можно реализовать аналогичным образом, но давайте повременим с этим, пока не реализуем аутентификацию. Кстати, о ней! ### Аутентификация[​](#authentication "Прямая ссылка на этот заголовок") Аутентификация включает в себя две страницы — одну для входа в систему и другую для регистрации. Они, в основном, очень схожие, поэтому имеет смысл держать их в одном слайсе, `sign-in`, чтобы при необходимости можно было переиспользовать код. Создайте `RegisterPage.tsx` в сегменте `ui` в `pages/sign-in` со следующим содержимым: pages/sign-in/ui/RegisterPage.tsx ``` import { Form, Link, useActionData } from "@remix-run/react"; import type { register } from "../api/register"; export function RegisterPage() { const registerData = useActionData(); return (

Sign up

Have an account?

{registerData?.error && (
    {registerData.error.errors.body.map((error) => (
  • {error}
  • ))}
)}
); } ``` Сейчас нам нужно исправить сломанный импорт. Он обращается к новому сегменту, поэтому создайте его: ``` npx fsd pages sign-in -s api ``` Однако прежде чем мы сможем реализовать бэкенд-часть регистрации, нам нужен некоторый инфраструктурный код для Remix для обработки сессий. Отправим его в Shared, на случай, если он понадобится какой-либо другой странице. Поместите следующий код в `shared/api/auth.server.ts`. Этот код очень специфичен для Remix, так что не беспокойтесь, если там не все понятно, просто скопируйте и вставьте: shared/api/auth.server.ts ``` import { createCookieSessionStorage, redirect } from "@remix-run/node"; import invariant from "tiny-invariant"; import type { User } from "./models"; invariant( process.env.SESSION_SECRET, "SESSION_SECRET must be set for authentication to work", ); const sessionStorage = createCookieSessionStorage<{ user: User; }>({ cookie: { name: "__session", httpOnly: true, path: "/", sameSite: "lax", secrets: [process.env.SESSION_SECRET], secure: process.env.NODE_ENV === "production", }, }); export async function createUserSession({ request, user, redirectTo, }: { request: Request; user: User; redirectTo: string; }) { const cookie = request.headers.get("Cookie"); const session = await sessionStorage.getSession(cookie); session.set("user", user); return redirect(redirectTo, { headers: { "Set-Cookie": await sessionStorage.commitSession(session, { maxAge: 60 * 60 * 24 * 7, // 7 days }), }, }); } export async function getUserFromSession(request: Request) { const cookie = request.headers.get("Cookie"); const session = await sessionStorage.getSession(cookie); return session.get("user") ?? null; } export async function requireUser(request: Request) { const user = await getUserFromSession(request); if (user === null) { throw redirect("/login"); } return user; } ``` А также экспортируйте модель `User` из файла `models.ts`, расположенного рядом с ним: shared/api/models.ts ``` import type { components } from "./v1"; export type Article = components["schemas"]["Article"]; export type User = components["schemas"]["User"]; ``` Прежде чем этот код заработает, необходимо установить переменную окружения `SESSION_SECRET`. Создайте файл `.env` в корне проекта, пропишите в нем `SESSION_SECRET=`, а затем пробегитесь по клавиатуре, чтобы создать длинную случайную строку. У вас должно получиться что-то вроде этого: .env ``` SESSION_SECRET=несмейтеэтокопировать ``` Наконец, добавьте несколько экспортов в публичный API, чтобы использовать этот код: shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; export { createUserSession, getUserFromSession, requireUser } from "./auth.server"; ``` Теперь мы можем написать код, который будет общаться с бэкендом RealWorld для регистрации. Мы сохраним его в `pages/sign-in/api`. Создайте файл `register.ts` и поместите в него следующий код: pages/sign-in/api/register.ts ``` import { json, type ActionFunctionArgs } from "@remix-run/node"; import { POST, createUserSession } from "shared/api"; export const register = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const username = formData.get("username")?.toString() ?? ""; const email = formData.get("email")?.toString() ?? ""; const password = formData.get("password")?.toString() ?? ""; const { data, error } = await POST("/users", { body: { user: { email, password, username } }, }); if (error) { return json({ error }, { status: 400 }); } else { return createUserSession({ request: request, user: data.user, redirectTo: "/", }); } }; ``` pages/sign-in/index.ts ``` export { RegisterPage } from './ui/RegisterPage'; export { register } from './api/register'; ``` Почти готово! Осталось подключить страницу и действие регистрации к маршруту `/register`. Создайте `register.tsx` в `app/routes`: app/routes/register.tsx ``` import { RegisterPage, register } from "pages/sign-in"; export { register as action }; export default RegisterPage; ``` Теперь, если вы перейдете на [http://localhost:3000/register,](http://localhost:3000/register) вы сможете создать пользователя! Остальная часть приложения пока что на это не отреагирует, мы займемся этим в ближайшее время. Аналогичным образом мы можем реализовать страницу входа в систему. Попробуйте сами или просто возьмите код и двигайтесь дальше: pages/sign-in/api/sign-in.ts ``` import { json, type ActionFunctionArgs } from "@remix-run/node"; import { POST, createUserSession } from "shared/api"; export const signIn = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const email = formData.get("email")?.toString() ?? ""; const password = formData.get("password")?.toString() ?? ""; const { data, error } = await POST("/users/login", { body: { user: { email, password } }, }); if (error) { return json({ error }, { status: 400 }); } else { return createUserSession({ request: request, user: data.user, redirectTo: "/", }); } }; ``` pages/sign-in/ui/SignInPage.tsx ``` import { Form, Link, useActionData } from "@remix-run/react"; import type { signIn } from "../api/sign-in"; export function SignInPage() { const signInData = useActionData(); return (

Sign in

Need an account?

{signInData?.error && (
    {signInData.error.errors.body.map((error) => (
  • {error}
  • ))}
)}
); } ``` pages/sign-in/index.ts ``` export { RegisterPage } from './ui/RegisterPage'; export { register } from './api/register'; export { SignInPage } from './ui/SignInPage'; export { signIn } from './api/sign-in'; ``` app/routes/login.tsx ``` import { SignInPage, signIn } from "pages/sign-in"; export { signIn as action }; export default SignInPage; ``` Теперь давайте дадим пользователям возможность попасть на эти страницы. ### Хэдер[​](#хэдер "Прямая ссылка на этот заголовок") Как мы уже говорили в первой части, хэдер приложения обычно размещается либо в Widgets, либо в Shared. Мы поместим его в Shared, потому что он очень прост, и вся бизнес-логика может быть сохранена за его пределами. Давайте создадим для него место: ``` npx fsd shared ui ``` Теперь создайте `shared/ui/Header.tsx` со следующим содержимым: shared/ui/Header.tsx ``` import { useContext } from "react"; import { Link, useLocation } from "@remix-run/react"; import { CurrentUser } from "../api/currentUser"; export function Header() { const currentUser = useContext(CurrentUser); const { pathname } = useLocation(); return ( ); } ``` Экспортируйте этот компонент из `shared/ui`: shared/ui/index.ts ``` export { Header } from "./Header"; ``` В хэдере мы полагаемся на контекст, расположенный в `shared/api`. Создайте ещё его: shared/api/currentUser.ts ``` import { createContext } from "react"; import type { User } from "./models"; export const CurrentUser = createContext(null); ``` shared/api/index.ts ``` export { GET, POST, PUT, DELETE } from "./client"; export type { Article } from "./models"; export { createUserSession, getUserFromSession, requireUser } from "./auth.server"; export { CurrentUser } from "./currentUser"; ``` Теперь давайте добавим хэдер на страницу. Мы хотим, чтобы он был на каждой странице, поэтому имеет смысл просто добавить его в корневой маршрут и обернуть аутлет (место, в которое будет отрендерена страница) провайдером контекста `CurrentUser`. Таким образом, все наше приложение, включая хэдер, получит доступ к объекту текущего пользователя. Мы также добавим загрузчик для получения объекта текущего пользователя из cookies. Добавьте следующее в `app/root.tsx`: app/root.tsx ``` import { cssBundleHref } from "@remix-run/css-bundle"; import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node"; import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData, } from "@remix-run/react"; import { Header } from "shared/ui"; import { getUserFromSession, CurrentUser } from "shared/api"; export const links: LinksFunction = () => [ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), ]; export const loader = ({ request }: LoaderFunctionArgs) => getUserFromSession(request); export default function App() { const user = useLoaderData(); return (
); } ``` В итоге на главной странице должно получиться следующее: ![Страница фида Conduit, на которой есть хэдер, фид и теги. Вкладки по-прежнему отсутствуют.](/documentation/ru/assets/images/realworld-feed-without-tabs-5da4c9072101ac20e82e2234bd3badbe.jpg) Страница фида Conduit, на которой есть хэдер, фид и теги. Вкладки по-прежнему отсутствуют. ### Вкладки[​](#вкладки "Прямая ссылка на этот заголовок") Теперь, когда мы можем определить состояние аутентификации, давайте также быстренько реализуем вкладки и лайки, чтоб закончить со страницей ленты. Нам нужна еще одна форма, но этот файл страницы становится слишком большим, поэтому давайте перенесем эти формы в соседние файлы. Мы создадим `Tabs.tsx`, `PopularTags.tsx` и `Pagination.tsx` со следующим содержимым: pages/feed/ui/Tabs.tsx ``` import { useContext } from "react"; import { Form, useSearchParams } from "@remix-run/react"; import { CurrentUser } from "shared/api"; export function Tabs() { const [searchParams] = useSearchParams(); const currentUser = useContext(CurrentUser); return (
    {currentUser !== null && (
  • )}
  • {searchParams.has("tag") && (
  • {searchParams.get("tag")}
  • )}
); } ``` pages/feed/ui/PopularTags.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import type { loader } from "../api/loader"; export function PopularTags() { const { tags } = useLoaderData(); return (

Popular Tags

{tags.tags.map((tag) => ( ))}
); } ``` pages/feed/ui/Pagination.tsx ``` import { Form, useLoaderData, useSearchParams } from "@remix-run/react"; import { ExistingSearchParams } from "remix-utils/existing-search-params"; import { LIMIT, type loader } from "../api/loader"; export function Pagination() { const [searchParams] = useSearchParams(); const { articles } = useLoaderData(); const pageAmount = Math.ceil(articles.articlesCount / LIMIT); const currentPage = parseInt(searchParams.get("page") ?? "1", 10); return (
    {Array(pageAmount) .fill(null) .map((_, index) => index + 1 === currentPage ? (
  • {index + 1}
  • ) : (
  • ), )}
); } ``` И теперь мы можем значительно упростить саму страницу с фидом: pages/feed/ui/FeedPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticlePreview } from "./ArticlePreview"; import { Tabs } from "./Tabs"; import { PopularTags } from "./PopularTags"; import { Pagination } from "./Pagination"; export function FeedPage() { const { articles } = useLoaderData(); return (

conduit

A place to share your knowledge.

{articles.articles.map((article) => ( ))}
); } ``` Нам также нужно учесть новую вкладку в функции-загрузчике: pages/feed/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET, requireUser } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { /* unchanged */ } /** Amount of articles on one page. */ export const LIMIT = 20; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const selectedTag = url.searchParams.get("tag") ?? undefined; const page = parseInt(url.searchParams.get("page") ?? "", 10); if (url.searchParams.get("source") === "my-feed") { const userSession = await requireUser(request); return json( await promiseHash({ articles: throwAnyErrors( GET("/articles/feed", { params: { query: { limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, headers: { Authorization: `Token ${userSession.token}` }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); } return json( await promiseHash({ articles: throwAnyErrors( GET("/articles", { params: { query: { tag: selectedTag, limit: LIMIT, offset: !Number.isNaN(page) ? page * LIMIT : undefined, }, }, }), ), tags: throwAnyErrors(GET("/tags")), }), ); }; ``` Прежде чем мы отложим страницу ленты, давайте добавим код, который будет обрабатывать лайки к постам. Измените ваш `ArticlePreview.tsx` на следующий: pages/feed/ui/ArticlePreview\.tsx ``` import { Form, Link } from "@remix-run/react"; import type { Article } from "shared/api"; interface ArticlePreviewProps { article: Article; } export function ArticlePreview({ article }: ArticlePreviewProps) { return (
{article.author.username} {new Date(article.createdAt).toLocaleDateString(undefined, { dateStyle: "long", })}

{article.title}

{article.description}

Read more...
    {article.tagList.map((tag) => (
  • {tag}
  • ))}
); } ``` Этот код отправит POST-запрос на `/article/:slug` с `_action=favorite`, чтобы отметить статью как любимую. Пока это не работает, но как только мы начнем работать над читалкой статей, мы реализуем и это. И на этом мы официально закончили работу над фидом! Ура! ### Читалка статей[​](#читалка-статей "Прямая ссылка на этот заголовок") Во-первых, нам нужны данные. Давайте создадим загрузчик: ``` npx fsd pages article-read -s api ``` pages/article-read/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import invariant from "tiny-invariant"; import type { FetchResponse } from "openapi-fetch"; import { promiseHash } from "remix-utils/promise"; import { GET, getUserFromSession } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ request, params }: LoaderFunctionArgs) => { invariant(params.slug, "Expected a slug parameter"); const currentUser = await getUserFromSession(request); const authorization = currentUser ? { Authorization: `Token ${currentUser.token}` } : undefined; return json( await promiseHash({ article: throwAnyErrors( GET("/articles/{slug}", { params: { path: { slug: params.slug }, }, headers: authorization, }), ), comments: throwAnyErrors( GET("/articles/{slug}/comments", { params: { path: { slug: params.slug }, }, headers: authorization, }), ), }), ); }; ``` pages/article-read/index.ts ``` export { loader } from "./api/loader"; ``` Теперь мы можем подключить его к маршруту `/article/:slug`, создав файл маршрута `article.$slug.tsx`: app/routes/article.$slug.tsx ``` export { loader } from "pages/article-read"; ``` Сама страница состоит из трех основных блоков — заголовка статьи с действиями (повторяется дважды), тела статьи и раздела комментариев. Это разметка страницы, она не особенно интересна: pages/article-read/ui/ArticleReadPage.tsx ``` import { useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { ArticleMeta } from "./ArticleMeta"; import { Comments } from "./Comments"; export function ArticleReadPage() { const { article } = useLoaderData(); return (

{article.article.title}

{article.article.body}

    {article.article.tagList.map((tag) => (
  • {tag}
  • ))}

); } ``` Более интересными являются `ArticleMeta` и `Comments`. Они содержат операции записи, такие как лайкнуть статью, оставить комментарий и т. д. Чтобы они заработали, нам сначала нужно реализовать бэкенд-часть. Создайте файл `action.ts` в сегменте `api` этой страницы: pages/article-read/api/action.ts ``` import { redirect, type ActionFunctionArgs } from "@remix-run/node"; import { namedAction } from "remix-utils/named-action"; import { redirectBack } from "remix-utils/redirect-back"; import invariant from "tiny-invariant"; import { DELETE, POST, requireUser } from "shared/api"; export const action = async ({ request, params }: ActionFunctionArgs) => { const currentUser = await requireUser(request); const authorization = { Authorization: `Token ${currentUser.token}` }; const formData = await request.formData(); return namedAction(formData, { async delete() { invariant(params.slug, "Expected a slug parameter"); await DELETE("/articles/{slug}", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirect("/"); }, async favorite() { invariant(params.slug, "Expected a slug parameter"); await POST("/articles/{slug}/favorite", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async unfavorite() { invariant(params.slug, "Expected a slug parameter"); await DELETE("/articles/{slug}/favorite", { params: { path: { slug: params.slug } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async createComment() { invariant(params.slug, "Expected a slug parameter"); const comment = formData.get("comment"); invariant(typeof comment === "string", "Expected a comment parameter"); await POST("/articles/{slug}/comments", { params: { path: { slug: params.slug } }, headers: { ...authorization, "Content-Type": "application/json" }, body: { comment: { body: comment } }, }); return redirectBack(request, { fallback: "/" }); }, async deleteComment() { invariant(params.slug, "Expected a slug parameter"); const commentId = formData.get("id"); invariant(typeof commentId === "string", "Expected an id parameter"); const commentIdNumeric = parseInt(commentId, 10); invariant( !Number.isNaN(commentIdNumeric), "Expected a numeric id parameter", ); await DELETE("/articles/{slug}/comments/{id}", { params: { path: { slug: params.slug, id: commentIdNumeric } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async followAuthor() { const authorUsername = formData.get("username"); invariant( typeof authorUsername === "string", "Expected a username parameter", ); await POST("/profiles/{username}/follow", { params: { path: { username: authorUsername } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, async unfollowAuthor() { const authorUsername = formData.get("username"); invariant( typeof authorUsername === "string", "Expected a username parameter", ); await DELETE("/profiles/{username}/follow", { params: { path: { username: authorUsername } }, headers: authorization, }); return redirectBack(request, { fallback: "/" }); }, }); }; ``` Реэкспортируйте её из слайса, а затем из маршрута. Пока мы здесь, давайте также подключим саму страницу: pages/article-read/index.ts ``` export { ArticleReadPage } from "./ui/ArticleReadPage"; export { loader } from "./api/loader"; export { action } from "./api/action"; ``` app/routes/article.$slug.tsx ``` import { ArticleReadPage } from "pages/article-read"; export { loader, action } from "pages/article-read"; export default ArticleReadPage; ``` Теперь, несмотря на то, что мы еще не реализовали кнопку лайка в читалке, кнопка лайка в ленте начнет работать! Это потому, что она тоже отправляет запросы на этот маршрут. Попробуйте лайкнуть что-нибудь. `ArticleMeta` и `Comments` — это, опять же, просто формы. Мы уже делали это раньше, давайте возьмем их код и пойдем дальше: pages/article-read/ui/ArticleMeta.tsx ``` import { Form, Link, useLoaderData } from "@remix-run/react"; import { useContext } from "react"; import { CurrentUser } from "shared/api"; import type { loader } from "../api/loader"; export function ArticleMeta() { const currentUser = useContext(CurrentUser); const { article } = useLoaderData(); return (
{article.article.author.username} {article.article.createdAt}
{article.article.author.username == currentUser?.username ? ( <> Edit Article    ) : ( <>    )}
); } ``` pages/article-read/ui/Comments.tsx ``` import { useContext } from "react"; import { Form, Link, useLoaderData } from "@remix-run/react"; import { CurrentUser } from "shared/api"; import type { loader } from "../api/loader"; export function Comments() { const { comments } = useLoaderData(); const currentUser = useContext(CurrentUser); return (
{currentUser !== null ? (
) : (

Sign in   or   Sign up   to add comments on this article.

)} {comments.comments.map((comment) => (

{comment.body}

  {comment.author.username} {comment.createdAt} {comment.author.username === currentUser?.username && (
)}
))}
); } ``` А вместе с этим и наша читалка статей! Кнопки "Подписаться на автора", "Мне нравится" и "Оставить комментарий" теперь должны работать как положено. ![Читалка статей с рабочими кнопками подписки и лайка](/documentation/ru/assets/images/realworld-article-reader-6a420e4f2afe139d2bdd54d62974f0b9.jpg) Читалка статей с рабочими кнопками подписки и лайка ### Редактор статей[​](#редактор-статей "Прямая ссылка на этот заголовок") Это последняя страница, которую мы рассмотрим в этом руководстве, и самая интересная часть здесь — это то, как мы будем проверять данные формы. Сама страница, `article-edit/ui/ArticleEditPage.tsx`, будет довольно простой, дополнительная логика будет скрыта в двух других компонентах: pages/article-edit/ui/ArticleEditPage.tsx ``` import { Form, useLoaderData } from "@remix-run/react"; import type { loader } from "../api/loader"; import { TagsInput } from "./TagsInput"; import { FormErrors } from "./FormErrors"; export function ArticleEditPage() { const article = useLoaderData(); return (
); } ``` Эта страница получает текущую статью (если мы пишем статью не с нуля) и заполняет соответствующие поля формы. Мы уже видели это. Интересной частью является `FormErrors`, потому что он будет получать результат проверки и отображать его пользователю. Давайте посмотрим: pages/article-edit/ui/FormErrors.tsx ``` import { useActionData } from "@remix-run/react"; import type { action } from "../api/action"; export function FormErrors() { const actionData = useActionData(); return actionData?.errors != null ? (
    {actionData.errors.map((error) => (
  • {error}
  • ))}
) : null; } ``` Здесь мы предполагаем, что наш экшн будет возвращать поле `errors`, массив понятных человеку сообщений об ошибках. К экшну мы перейдем чуть позже. Еще один компонент — это поле ввода тегов. Это обычное поле ввода с дополнительным предпросмотром выбранных тегов. Здесь особо не на что смотреть: pages/article-edit/ui/TagsInput.tsx ``` import { useEffect, useRef, useState } from "react"; export function TagsInput({ name, defaultValue, }: { name: string; defaultValue?: Array; }) { const [tagListState, setTagListState] = useState(defaultValue ?? []); function removeTag(tag: string): void { const newTagList = tagListState.filter((t) => t !== tag); setTagListState(newTagList); } const tagsInput = useRef(null); useEffect(() => { tagsInput.current && (tagsInput.current.value = tagListState.join(",")); }, [tagListState]); return ( <> setTagListState(e.target.value.split(",").filter(Boolean)) } />
{tagListState.map((tag) => ( [" ", "Enter"].includes(e.key) && removeTag(tag) } onClick={() => removeTag(tag)} >{" "} {tag} ))}
); } ``` Теперь перейдем к API-части. Загрузчик должен посмотреть на URL, и если в нем есть ссылка на статью, это означает, что мы редактируем существующую статью, и ее данные должны быть загружены. В противном случае ничего не возвращается. Давайте создадим этот загрузчик: pages/article-edit/api/loader.ts ``` import { json, type LoaderFunctionArgs } from "@remix-run/node"; import type { FetchResponse } from "openapi-fetch"; import { GET, requireUser } from "shared/api"; async function throwAnyErrors( responsePromise: Promise>, ) { const { data, error, response } = await responsePromise; if (error !== undefined) { throw json(error, { status: response.status }); } return data as NonNullable; } export const loader = async ({ params, request }: LoaderFunctionArgs) => { const currentUser = await requireUser(request); if (!params.slug) { return { article: null }; } return throwAnyErrors( GET("/articles/{slug}", { params: { path: { slug: params.slug } }, headers: { Authorization: `Token ${currentUser.token}` }, }), ); }; ``` Экшн примет новые значения полей, прогонит их через нашу схему данных и, если все правильно, зафиксирует изменения в бэкенде, либо обновив существующую статью, либо создав новую: pages/article-edit/api/action.ts ``` import { json, redirect, type ActionFunctionArgs } from "@remix-run/node"; import { POST, PUT, requireUser } from "shared/api"; import { parseAsArticle } from "../model/parseAsArticle"; export const action = async ({ request, params }: ActionFunctionArgs) => { try { const { body, description, title, tags } = parseAsArticle( await request.formData(), ); const tagList = tags?.split(",") ?? []; const currentUser = await requireUser(request); const payload = { body: { article: { title, description, body, tagList, }, }, headers: { Authorization: `Token ${currentUser.token}` }, }; const { data, error } = await (params.slug ? PUT("/articles/{slug}", { params: { path: { slug: params.slug } }, ...payload, }) : POST("/articles", payload)); if (error) { return json({ errors: error }, { status: 422 }); } return redirect(`/article/${data.article.slug ?? ""}`); } catch (errors) { return json({ errors }, { status: 400 }); } }; ``` Наша схема данных будет ещё и парсить `FormData`, что позволяет нам удобно получать чистые поля или просто бросать ошибки для обработки в конце. Вот как может выглядеть эта функция парсинга: pages/article-edit/model/parseAsArticle.ts ``` export function parseAsArticle(data: FormData) { const errors = []; const title = data.get("title"); if (typeof title !== "string" || title === "") { errors.push("Give this article a title"); } const description = data.get("description"); if (typeof description !== "string" || description === "") { errors.push("Describe what this article is about"); } const body = data.get("body"); if (typeof body !== "string" || body === "") { errors.push("Write the article itself"); } const tags = data.get("tags"); if (typeof tags !== "string") { errors.push("The tags must be a string"); } if (errors.length > 0) { throw errors; } return { title, description, body, tags: data.get("tags") ?? "" } as { title: string; description: string; body: string; tags: string; }; } ``` Возможно, она покажется немного длинной и повторяющейся, но такова цена, которую мы платим за читаемые сообщения об ошибках. Это может быть и схема Zod, например, но тогда нам придется выводить сообщения об ошибках на фронтенде, а эта форма не стоит таких сложностей. Последний шаг — подключение страницы, загрузчика и действия к маршрутам. Поскольку мы аккуратно поддерживаем и создание, и редактирование, мы можем экспортировать одно и то же действие как из `editor._index.tsx`, так и из `editor.$slug.tsx`: pages/article-edit/index.ts ``` export { ArticleEditPage } from "./ui/ArticleEditPage"; export { loader } from "./api/loader"; export { action } from "./api/action"; ``` app/routes/editor.\_index.tsx, app/routes/editor.$slug.tsx (одинаковое содержимое) ``` import { ArticleEditPage } from "pages/article-edit"; export { loader, action } from "pages/article-edit"; export default ArticleEditPage; ``` Мы закончили! Войдите в систему и попробуйте создать новую статью. Или “забудьте” написать статью и посмотрите, как сработает валидация. ![Редактор статей Conduit, в поле заголовка которого написано “New article”, а остальные поля пусты. Над формой есть две ошибки: “Describe what this article is about” и “Write the article itself”.](/documentation/ru/assets/images/realworld-article-editor-bc3ee45c96ae905fdbb54d6463d12723.jpg) Редактор статей Conduit, в поле заголовка которого написано “New article”, а остальные поля пусты. Над формой есть две ошибки: **“Describe what this article is about”** и **“Write the article itself**”. Страницы профиля и настроек очень похожи на страницы чтения и редактирования статей, они оставлены в качестве упражнения для читателя, то есть для вас :) --- # Обработка API-запросов ## API-запросы в `shared`[​](#shared-api-requests "Прямая ссылка на этот заголовок") Начните с размещения общей логики API-запросов в каталоге `shared/api`. Это упрощает повторное использование запросов во всем приложении, что ускоряет разработку. Для многих проектов этого будет достаточно. Типичная структура файлов будет такой: * 📂 shared * 📂 api * 📄 client.ts * 📄 index.ts * 📂 endpoints * 📄 login.ts Файл `client.ts` централизует настройку HTTP-запросов. Он оборачивает выбранный вами подход (например, `fetch()` или экземпляр `axios`) и обрабатывает общие конфигурации, такие как: * Базовый URL бэкенда. * Заголовки по умолчанию (например, для аутентификации). * Сериализация данных. Вот примеры для `axios` и `fetch`: * Axios * Fetch shared/api/client.ts ``` // Example using axios import axios from 'axios'; export const client = axios.create({ baseURL: 'https://your-api-domain.com/api/', timeout: 5000, headers: { 'X-Custom-Header': 'my-custom-value' } }); ``` shared/api/client.ts ``` export const client = { async post(endpoint: string, body: any, options?: RequestInit) { const response = await fetch(`https://your-api-domain.com/api${endpoint}`, { method: 'POST', body: JSON.stringify(body), ...options, headers: { 'Content-Type': 'application/json', 'X-Custom-Header': 'my-custom-value', ...options?.headers, }, }); return response.json(); } // ... другие методы put, delete, и т.д. }; ``` Организуйте свои отдельные функции API-запросов в shared/api/endpoints, группируя их по API эндпоинтам. примечание Для простоты, в примерах ниже мы опускаем взаимодействие с формами и валидацию. Для получения подробной информации о том, как работать с такими библиотеками как Zod или Valibot, обратитесь к секции [Проверка типов и схемы](/documentation/ru/docs/guides/examples/types.md#type-validation-schemas-and-zod). shared/api/endpoints/login.ts ``` import { client } from '../client'; export interface LoginCredentials { email: string; password: string; } export function login(credentials: LoginCredentials) { return client.post('/login', credentials); } ``` Используйте файл `index.ts` в `shared/api` для экспорта ваших функций запросов. shared/api/index.ts ``` export { client } from './client'; // Если нужно экспортировать клиент export { login } from './endpoints/login'; export type { LoginCredentials } from './endpoints/login'; ``` ## API-запросы, специфичные для слайса[​](#slice-specific-api-requests "Прямая ссылка на этот заголовок") Если API-запрос используется только определенным слайсом (например, одной страницей или фичей) и не будет использоваться повторно, поместите его в сегмент `api` этого слайса. Это позволит аккуратно отделить логику, специфичную для слайса, от всего остального приложения. * 📂 pages * 📂 login * 📄 index.ts * 📂 api * 📄 login.ts * 📂 ui * 📄 LoginPage.tsx pages/login/api/login.ts ``` import { client } from 'shared/api'; interface LoginCredentials { email: string; password: string; } export function login(credentials: LoginCredentials) { return client.post('/login', credentials); } ``` Вам не нужно экспортировать функцию `login()` через публичный API страницы, потому что маловероятно, что какое-либо другое место в приложении будет нуждаться в этом запросе. примечание Избегайте преждевременного размещения API-запросов и типов ответов бэкенда в слое `entities`. Ответы бэкенда могут отличаться от того, что нужно вашим сущностям фронтенда. Логика API в `shared/api` или сегменте `api` слайса позволяет вам преобразовывать данные при необходимости, сохраняя фокус сущностей на проблемах фронтенда. ## Использование генераторов клиентов[​](#client-generators "Прямая ссылка на этот заголовок") Если ваш бэкенд предоставляет OpenAPI спецификацию, инструменты как [orval](https://orval.dev/) или [openapi-typescript](https://openapi-ts.dev/), могут генерировать типы API и функции запросов. Разместите сгенерированный код, например, в `shared/api/openapi`. Обязательно включите `README.md` для документирования того, что это за типы и как их генерировать. ## Интеграция с библиотеками состояния сервера[​](#server-state-libraries "Прямая ссылка на этот заголовок") При использовании библиотек состояния сервера, таких как [TanStack Query (React Query)](https://tanstack.com/query/latest) или [Pinia Colada](https://pinia-colada.esm.dev/) вам может потребоваться совместное использование типов или ключей кеша между срезами. Используйте общий слой `shared` для таких вещей, как: * Типы данных API * Ключи кеша * Общие параметры запросов и мутаций Подробнее о том, как работать с server state библиотеками, читайте в статье [React Query](/documentation/ru/docs/guides/tech/with-react-query.md) --- # Авторизация В общих чертах авторизация состоит из следующих этапов: 1. Получить учетные данные от пользователя 2. Отправить их на бэкенд 3. Сохранить токен для отправки авторизованных запросов. ## Как получить учетные данные пользователя[​](#как-получить-учетные-данные-пользователя "Прямая ссылка на этот заголовок") Мы предполагаем, что ваше приложение само собирает эти данные. Если у вас авторизация через OAuth, вы можете просто создать страницу логина со ссылкой на страницу провайдера OAuth и перейти к [шагу 3](#how-to-store-the-token-for-authenticated-requests). ### Отдельная страница для логина[​](#отдельная-страница-для-логина "Прямая ссылка на этот заголовок") Обычно на сайтах есть отдельные страницы для логина, где вы вводите свое имя пользователя и пароль. Эти страницы довольно просты, поэтому не требуют декомпозиции. Более того, формы логина и регистрации внешне очень похожи, поэтому их можно даже сгруппировать на одной странице. Создайте слайс для вашей страницы логина/регистрации на слое Pages: * 📂 pages * 📂 login * 📂 ui * 📄 LoginPage.tsx (или аналог в вашем фреймворке) * 📄 RegisterPage.tsx * 📄 index.ts * остальные страницы… Здесь мы создали два компонента и экспортировали их обоих в индексе слайса. Эти компоненты будут содержать формы, которые содержат понятные пользователю элементы для введения их учетных данных. ### Диалог для логина[​](#диалог-для-логина "Прямая ссылка на этот заголовок") Если в вашем приложении есть диалоговое окно для входа в систему, которое можно использовать на любой странице, вы можете создать для этого диалогового окна виджет. Таким образом, вы все равно сможете не сильно декомпозировать саму форму, но при этом переиспользовать этот диалог на любой странице. * 📂 widgets * 📂 login-dialog * 📂 ui * 📄 LoginDialog.tsx * 📄 index.ts * остальные виджеты… Остальная часть этого руководства написана для первого подхода, где логин делается на отдельной странице, но те же принципы применимы и к виджету диалога. ### Клиентская валидация[​](#клиентская-валидация "Прямая ссылка на этот заголовок") Иногда, особенно при регистрации, имеет смысл выполнить проверку на стороне клиента, чтобы быстро сообщить пользователю, что они допустили ошибку. Проверка может происходить в сегменте `model` на странице логина. Используйте библиотеку проверки по схемам, например, [Zod](https://zod.dev) для JS/TS, и предоставьте эту схему сегменту `ui`: pages/login/model/registration-schema.ts ``` import { z } from "zod"; export const registrationData = z.object({ email: z.string().email(), password: z.string().min(6), confirmPassword: z.string(), }).refine((data) => data.password === data.confirmPassword, { message: "Passwords do not match", path: ["confirmPassword"], }); ``` Затем в сегменте `ui` вы можете использовать эту схему для проверки ввода пользователя: pages/login/ui/RegisterPage.tsx ``` import { registrationData } from "../model/registration-schema"; function validate(formData: FormData) { const data = Object.fromEntries(formData.entries()); try { registrationData.parse(data); } catch (error) { // TODO: Показать пользователю сообщение об ошибке } } export function RegisterPage() { return (
validate(new FormData(e.target))}>
) } ``` ## Как отправить учетные данные на бэкенд[​](#как-отправить-учетные-данные-на-бэкенд "Прямая ссылка на этот заголовок") Создайте функцию, которая отправляет запрос к эндпоинту логина на бэкенде. Эту функцию можно вызвать либо непосредственно в коде компонента через библиотеку мутаций (например, TanStack Query), либо как побочный эффект в стейт-менеджере. ### Где хранить функцию запроса[​](#где-хранить-функцию-запроса "Прямая ссылка на этот заголовок") Есть два места, куда можно положить эту функцию: в `shared/api` или в сегмент `api` на странице. #### В `shared/api`[​](#в-sharedapi "Прямая ссылка на этот заголовок") Этот подход хорошо сочетается с тем, чтобы размещать в `shared/api` все функции запросов, и группировать их по эндпоинту, например. Структура файлов в таком случае может выглядеть так: * 📂 shared * 📂 api * 📂 endpoints * 📄 login.ts * остальные функции запросов… * 📄 client.ts * 📄 index.ts Файл `📄 client.ts` содержит обёртку над примитивом, выполняющим запросы (например, `fetch()`). Эта обёртка знает про base URL вашего бэкенда, проставляет необходимые заголовки, сериализует данные, и т.д. shared/api/endpoints/login.ts ``` import { POST } from "../client"; export function login({ email, password }: { email: string, password: string }) { return POST("/login", { email, password }); } ``` shared/api/index.ts ``` export { login } from "./endpoints/login"; ``` #### В сегменте `api` страницы[​](#в-сегменте-api-страницы "Прямая ссылка на этот заголовок") Если вы не храните все свои запросы в одном месте, возможно, вам подойдет разместить эту функцию запроса в сегменте `api` на странице логина. * 📂 pages * 📂 login * 📂 api * 📄 login.ts * 📂 ui * 📄 LoginPage.tsx * 📄 index.ts * остальные страницы… pages/login/api/login.ts ``` import { POST } from "shared/api"; export function login({ email, password }: { email: string, password: string }) { return POST("/login", { email, password }); } ``` Эту функцию даже необязательно реэкспортировать из индекса страницы, потому что, скорее всего, она будет использоваться только внутри этой страницы. ### Двухфакторная аутентификация[​](#двухфакторная-аутентификация "Прямая ссылка на этот заголовок") Если ваше приложение поддерживает двухфакторную аутентификацию (2FA), возможно, вам придется перенаправить пользователя на другую страницу, где они смогут ввести одноразовый пароль. Обычно, ваш запрос `POST /login` возвращает объект пользователя с флагом, указывающим, что у пользователя включен 2FA. Если этот флаг установлен, перенаправьте пользователя на страницу 2FA. Поскольку эта страница очень связана с логином, вы также можете положить её в тот же слайс, `login`, на слое Pages. Вам также понадобится еще одна функция запроса, похожая на `login()`, которую мы создали выше. Поместите их вместе либо в Shared, либо в сегмент `api` на странице `login`. ## Как хранить токен для авторизованных запросов[​](#how-to-store-the-token-for-authenticated-requests "Прямая ссылка на этот заголовок") Независимо от используемой вами схемы авторизации, будь то простой логин и пароль, OAuth или двухфакторная аутентификация, в конце вы получите токен. Этот токен следует хранить, чтобы последующие запросы могли идентифицировать себя. Идеальным хранилищем токенов для веб-приложения являются **cookies** — они не требуют ручного сохранения или обработки токенов. Таким образом, хранение cookies практически не требует усилий со стороны архитектуры фронтенда. Если ваш фронтенд-фреймворк имеет серверную часть (например, [Remix](https://remix.run)), то серверную инфраструктуру cookies следует хранить в `shared/api`. В [разделе туториала «Аутентификация»](/documentation/ru/docs/get-started/tutorial.md#authentication) есть пример того, как это сделать в Remix. Однако, иногда хранить токен в cookies — не вариант. В этом случае вам придется хранить токен самим. Помимо этого, вам также может потребоваться написать логику для обновления этого токена по истечении срока его действия. В рамках FSD есть несколько мест, где вы можете хранить токен, а также несколько способов сделать его доступным для остальной части приложения. ### В Shared[​](#в-shared "Прямая ссылка на этот заголовок") Этот подход хорошо работает, когда API-клиент определен в `shared/api`, поскольку токен свободно доступен ему для других функций-запросов, которые требуют авторизацию. Вы можете сделать так, чтобы клиент имел свой стейт, либо с помощью реактивного хранилища, либо просто с помощью переменной на уровне модуля. Затем вы можете обновлять этот стейт в ваших функциях `login()`/`logout()`. Автоматическое обновление токена может быть реализовано как middleware в API-клиенте — то, что выполняется каждый раз, когда вы делаете какой-либо запрос. Например, можно сделать так: * Авторизоваться и сохранить токен доступа, а также токен обновления. * Сделать любой запрос, требующий авторизации * Если запрос падает с кодом состояния, указывающим на истечение срока действия токена, а в хранилище есть токен, сделать запрос на обновление, сохранить новые токены и повторить исходный запрос. Одним из недостатков этого подхода является то, что логика хранения и обновления токена не имеет выделенного места. Это может подойти каким-то приложениям или командам, но если логика управления токенами более сложна, может захотеться разделить обязанности по отправке запросов и управлению токенами. В этом случае можно положить запросы и API-клиент в `shared/api`, а хранилище токенов и логику обновления — в `shared/auth`. Еще одним недостатком этого подхода является то, что если ваш сервер возвращает объект c информацией о вашем текущем пользователе вместе с токеном, вам будет некуда её положить, и придется запросить её снова из специального эндпоинта, например `/me` или `/users/current`. ### В Entities[​](#в-entities "Прямая ссылка на этот заголовок") У проектов на FSD часто есть сущность пользователя и/или сущность текущего пользователя. Это даже может быть одна сущность. примечание **Текущий пользователь** также иногда называется "viewer" или "me". Это делается для того, чтобы различать одного авторизованного пользователя с разрешениями и приватной информацией и всех остальных пользователей с публичной информацией. Чтобы хранить токен в сущности User, создайте реактивное хранилище в сегменте `model`. Это хранилище может содержать одновременно и токен, и объект с информацией о пользователе. Поскольку API-клиент обычно размещается в `shared/api` или распределяется между сущностями, главной проблемой этого подхода является обеспечение доступа к токену для других запросов, без нарушения [правил импортов для слоёв](/documentation/ru/docs/reference/layers.md#import-rule-on-layers): > Модуль (файл) в слайсе может импортировать другие слайсы только в том случае, если они расположены на слоях строго ниже. Есть несколько решений этой проблемы: 1. **Передавать токен вручную каждый раз, когда делаете запрос**
Это самое простое решение, но оно быстро становится неудобным, и если у вас нет строгой типизации, об этом легко забыть. Это решение также несовместимо с паттерном middleware для API-клиента в Shared. 2. **Открыть доступ к токену для всего приложения через контекст или глобальное хранилище вроде `localStorage`**
Ключ, по которому можно будет получить токен, будет храниться в `shared/api`, чтобы API-клиент мог его использовать. Реактивное хранилище токена будет экспортировано из сущности User, а провайдер контекста (если требуется) будет настроен на слое App. Это дает больше свободы для дизайна API-клиента, но такой подход создаёт неявную зависимость 3. **Вставлять токен в API-клиент каждый раз, когда токен меняется**
Если ваше хранилище реактивное, то можно подписаться на изменения и обновлять токен в API-клиенте каждый раз, когда хранилище в сущности User меняется. Это похоже на прошлое решение тем, что они оба создают неявную зависимость, но это решение более императивное ("push"), тогда как предыдущее — более декларативное ("pull"). Решив проблему доступности токена, хранящегося в модели сущности User, вы сможете описать дополнительную бизнес-логику, связанную с управлением токенами. Например, сегмент `model` может содержать логику, которая делает токен недействительным через определенный период времени или обновляет токен по истечении срока его действия. Чтобы совершать запросы на бэкенд для выполнения этих задач, используйте сегмент `api` сущности User или `shared/api`. ### В Pages/Widgets (не рекомендуется)[​](#в-pageswidgets-не-рекомендуется "Прямая ссылка на этот заголовок") Не рекомендуется хранить состояние, актуальное для всего приложения, как например токен доступа, в страницах или виджетах. Не стоит размещать хранилище токенов в сегменте `model` на странице логина. Вместо этого выберите одно из первых двух решений: Shared или Entities. ## Логаут и аннулирование токена[​](#логаут-и-аннулирование-токена "Прямая ссылка на этот заголовок") Обычно в приложениях не делают целую отдельную страницу для логаута, но функционал логаута, тем не менее, очень важен. В этот функционал входит авторизованный запрос на бэкенд и обновление хранилища токенов. Если вы храните все ваши запросы в `shared/api`, оставьте там функцию для запроса на логаут, рядом с функцией для логина. Если нет, разместите функцию-запрос на логаут рядом с кнопкой, которая её вызывает. Например, если у вас есть виджет хэдера, который есть на каждой странице и содержит ссылку для логаута, поместите этот запрос в сегмент `api` этого виджета. Обновление хранилища токенов также должно будет запускаться с места кнопки логаута, как, например, виджет заголовка. Вы можете объединить запрос и обновление хранилища в сегменте `model` этого виджета. ### Автоматический логаут[​](#автоматический-логаут "Прямая ссылка на этот заголовок") Не забудьте предусмотреть ситуации сбоя запроса на логаут или сбоя запроса на обновление токена. В обоих случаях вам следует очистить хранилище токенов. Если вы храните свой токен в Entities, этот код можно поместить в сегмент `model`, поскольку это чистая бизнес-логика. Если вы храните токен в Shared, размещение этой логики в `shared/api` может раздуть сегмент и размыть его предназначение. Если вы замечаете, что ваш сегмент `api` содержит две несвязанные вещи, рассмотрите возможность выделения логики управления токенами в другой сегмент, например, `shared/auth`. --- # Автокомплит WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/170) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Про декомпозицию по слоям ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Дискуссия) Про применение методологии для селекта с подгружаемыми справочниками](https://github.com/feature-sliced/documentation/discussions/65#discussioncomment-480807) --- # Browser API WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/197) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Про работу с Browser API: localStorage, audioApi, bluetoothAPI и т.п. > > Подробнее про идею можно спросить [@alex\_novi](https://t.me/alex_novich) --- # CMS WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/172) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Фичи бывают разные[​](#features-may-be-different "Прямая ссылка на этот заголовок") В некоторых проектах весь функционал сосредоточен в данных с сервера > ## Как корректней работать с CMS-разметкой[​](#how-to-work-more-correctly-with-cms-markup "Прямая ссылка на этот заголовок") > > --- # Обратная связь WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/187) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Errors, Alerts, Notifications, ... --- # i18n WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/171) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Куда положить? Как с этим работать?[​](#where-to-place-it-how-to-work-with-this "Прямая ссылка на этот заголовок") * * * --- # Метрика WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/181) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Про способы инициализировать метрики в приложении --- # Монорепозитории WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/221) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Про применимость для монорепозиториев, про bff, про микроаппы ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Дискуссия) Про монорепозитории и подключаемые модули-пакеты](https://github.com/feature-sliced/documentation/discussions/50) * [(Тред) Про применение для монорепозитория](https://t.me/feature_sliced/2412) --- # Лейауты страниц Это руководство рассматривает абстракцию *лейаута страницы* — когда несколько страниц имеют одинаковую структуру, отличаясь только основным содержимым. к сведению Вашего вопроса нет в этом руководстве? Напишите свой вопрос, оставив отзыв к этой статье (синяя кнопка справа), и мы рассмотрим возможность расширения этого руководства! ## Простой лейаут[​](#простой-лейаут "Прямая ссылка на этот заголовок") Самый простой лейаут можно увидеть прямо на этой странице. Он имеет хэдер с навигацией по сайту, два сайдбара и футер с внешними ссылками. Здесь нет сложной бизнес-логики, и единственные динамические части — это сайдбары и переключатели справа в хэдере. Такой лейаут можно разместить целиком в `shared/ui` или в `app/layouts`, с заполнением контента сайдбаров через пропы: shared/ui/layout/Layout.tsx ``` import { Link, Outlet } from "react-router-dom"; import { useThemeSwitcher } from "./useThemeSwitcher"; export function Layout({ siblingPages, headings }) { const [theme, toggleTheme] = useThemeSwitcher(); return (
{/* Здесь будет основное содержимое страницы */}
  • GitHub
  • Twitter
); } ``` shared/ui/layout/useThemeSwitcher.ts ``` export function useThemeSwitcher() { const [theme, setTheme] = useState("light"); function toggleTheme() { setTheme(theme === "light" ? "dark" : "light"); } useEffect(() => { document.body.classList.remove("light", "dark"); document.body.classList.add(theme); }, [theme]); return [theme, toggleTheme] as const; } ``` Код сайдбаров оставлен читателю в качестве упражнения 😉. ## Использование виджетов в лейауте[​](#использование-виджетов-в-лейауте "Прямая ссылка на этот заголовок") Иногда есть необходимость включить в лейаут определенную бизнес-логику, особенно если вы используете глубоко вложенные маршруты с роутером типа [React Router](https://reactrouter.com/). Тогда вы не можете хранить лейаут в Shared или в Widgets из-за [правила импорта для слоёв](/documentation/ru/docs/reference/layers.md#import-rule-on-layers): > Модуль в слайсе может импортировать другие слайсы только в том случае, если они расположены на слоях строго ниже. Прежде чем обсуждать решения, нам нужно обсудить, действительно ли это проблема. Вам *действительно нужен* этот лейаут, и если да, *действительно ли* он должен быть виджетом? Если блок бизнес-логики, про который идёт речь, используется на 2-3 страницах, и лейаут просто является небольшой обёрткой для этого виджета, рассмотрите один из этих двух вариантов: 1. **Напишите ваш лейаут прямо в коде роутера на уровне App**
Это отлично подходит для роутеров, поддерживающих вложенность, потому что вы можете группировать определенные маршруты и применять нужный лейаут только к ним. 2. **Просто скопируйте его**
Желание абстрагировать код часто переоценено. Это особенно верно для лейаутов, потому что они редко меняются. В какой-то момент, если одна из этих страниц потребует изменений, вы можете просто внести изменения, не затрагивая другие страницы. Если вы беспокоитесь, что кто-то может забыть обновить другие страницы, всегда можно оставить комментарий, описывающий отношения между страницами. Если ни один из вышеперечисленных вариантов не подходит, есть два решения для включения виджета в лейаут: 1. **Используйте render props или слоты**
Большинство фреймворков позволяют передавать часть UI внешне. В React это называется [render props](https://www.patterns.dev/react/render-props-pattern/), в Vue — [слоты](https://ru.vuejs.org/guide/components/slots). 2. **Переместите лейаут на уровень App**
Вы также можете хранить свой лейаут на уровне App, например, в `app/layouts`, и комбинировать любые виджеты, которые вам нужны. ## Дополнительные материалы[​](#дополнительные-материалы "Прямая ссылка на этот заголовок") * Пример создания лейаута с аутентификацией с помощью React и Remix (аналогичен React Router) можно найти в [туториале](/documentation/ru/docs/get-started/tutorial.md). --- # Desktop/Touch платформы WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/198) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Про применение методологии для desktop/touch --- # SSR WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/173) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Про реализацию SSR с применением методологии --- # Темизация WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/207) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Куда положить работу с темой и палитрой?[​](#where-should-i-put-my-work-with-the-theme-and-palette "Прямая ссылка на этот заголовок") > ## Рассуждения про расположение theme, i18n логики[​](#discussion-about-the-location-of-the-theme-i18n-logic "Прямая ссылка на этот заголовок") > --- # Типы В этом руководстве рассматриваются типы данных из типизированных языков, таких как TypeScript, и где они вписываются в FSD. к сведению Вашего вопроса нет в этом руководстве? Напишите свой вопрос, оставив отзыв к этой статье (синяя кнопка справа), и мы рассмотрим возможность расширения этого руководства! ## Типы-утилиты[​](#типы-утилиты "Прямая ссылка на этот заголовок") Типы-утилиты — это типы, которые сами по себе не имеют особого смысла и обычно используются с другими типами. Например: ``` type ArrayValues = T[number]; ``` Источник: Чтобы добавить типы-утилиты в ваш проект, установите библиотеку, например [`type-fest`](https://github.com/sindresorhus/type-fest), или создайте свою собственную библиотеку в `shared/lib`. Обязательно четко укажите, какие новые типы *можно* добавлять в эту библиотеку, а какие — *нельзя*. Например, назовите ее `shared/lib/utility-types` и добавьте внутрь файл README, описывающий, что такое типы-утилиты в понимании вашей команды. Не переоценивайте потенциал переиспользования типов-утилит. То, что их *можно* использовать повторно, не означает, что так и будет, и поэтому не каждый тип-утилита должен быть в Shared. Некоторые типы-утилиты должны лежать прямо там, где они нужны: * 📂 pages * 📂 home * 📂 api * 📄 ArrayValues.ts (тип-утилита) * 📄 getMemoryUsageMetrics.ts (код, который будет использовать эту утилиту) warning Не поддавайтесь искушению создать папку `shared/types` или добавить сегмент `types` в ваши слайсы. Категория "типы" похожа на категорию "компоненты" или "хуки" в том, что она описывает содержимое, а не то, для чего оно нужно. Сегменты должны описывать цель кода, а не его суть. ## Бизнес-сущности и их ссылки друг на друга[​](#бизнес-сущности-и-их-ссылки-друг-на-друга "Прямая ссылка на этот заголовок") Одними из наиболее важных типов в приложении являются типы бизнес-сущностей, т. е. реальных вещей, с которыми работает ваше приложение. Например, в приложении сервиса онлайн-музыки у вас могут быть бизнес-сущности *Песня* (song), *Альбом* (album) и т. д. Бизнес-сущности часто приходят с бэкенда, поэтому первым шагом является типизация ответов бэкенда. Удобно иметь функцию запроса к каждому эндпоинту и типизировать результат вызова этой функции. Для дополнительной безопасности типов вы можете пропустить результат через библиотеку проверки по схемам, например [Zod](https://zod.dev). Например, если вы храните все свои запросы в Shared, вы можете сделать так: shared/api/songs.ts ``` import type { Artist } from "./artists"; interface Song { id: number; title: string; artists: Array; } export function listSongs() { return fetch('/api/songs').then((res) => res.json() as Promise>); } ``` Вы могли заметить, что тип `Song` ссылается на другую сущность, `Artist`. Это преимущество хранения ваших запросов в Shared — реальные типы часто ссылаются друг на друга. Если бы мы положили эту функцию в `entities/song/api`, мы бы не смогли просто импортировать `Artist` из `entities/artist`, потому что FSD ограничивает кросс-импорт между слайсами через [правило импорта для слоёв](/documentation/ru/docs/reference/layers.md#import-rule-on-layers): > Модуль в слайсе может импортировать другие слайсы только в том случае, если они расположены на слоях строго ниже. Есть два способа решения этой проблемы: 1. **Параметризируйте типы** Вы можете сделать так, чтоб ваши типы принимали типовые аргументы в качестве слотов для соединения с другими сущностями, и даже накладывать ограничения на эти слоты. Например: entities/song/model/song.ts ``` interface Song { id: number; title: string; artists: Array; } ``` Это хорошо работает для некоторых типов, и иногда хуже работает для других. Простой тип, такой как `Cart = { items: Array }`, можно легко заставить работать с любым типом продукта. Более связанные типы, такие как `Country` и `City`, может быть не так легко разделить. 2. **Кросс-импортируйте (но только правильно)** Чтоб сделать кросс-импорт между сущностями в FSD, вы можете использовать отдельный публичный API специально для каждого слайса, который будет кросс-импортировать. Например, если у нас есть сущности `song` (песня), `artist` (исполнитель), и `playlist` (плейлист), и последние две должны ссылаться на `song`, мы можем создать два специальных публичных API для них обоих в сущности `song` через `@x`-нотацию: * 📂 entities * 📂 song * 📂 @x * 📄 artist.ts (публичный API, из которого будет импортировать сущность `artist`) * 📄 playlist.ts (публичный API, из которого будет импортировать сущность `playlist`) * 📄 index.ts (обыкновенный публичный API) Содержимое файла `📄 entities/song/@x/artist.ts` похоже на `📄 entities/song/index.ts`: entities/song/@x/artist.ts ``` export type { Song } from "../model/song.ts"; ``` Затем `📄 entities/artist/model/artist.ts` может импортировать `Song` следующим образом: entities/artist/model/artist.ts ``` import type { Song } from "entities/song/@x/artist"; export interface Artist { name: string; songs: Array; } ``` С помощью явных связей между сущностями мы получаем точный контроль взаимозависимостей и при этом поддерживаем достаточный уровень разделения доменов. ## Объекты передачи данных (DTO) и мапперы[​](#data-transfer-objects-and-mappers "Прямая ссылка на этот заголовок") Объекты передачи данных, или DTO (от англ. *data transfer object*), — это термин, описывающий форму данных, которые поступают из бэкенда. Иногда DTO можно использовать как есть, но иногда их формат неудобен для фронтенда. Тут приходят на помощь мапперы — это функции, которые преобразуют DTO в более удобную форму. ### Куда положить DTO[​](#куда-положить-dto "Прямая ссылка на этот заголовок") Если ваши типы бэкенда находятся в отдельном пакете (например, если вы делите код между фронтендом и бэкендом), просто импортируйте ваши DTO оттуда, и готово! Если вы не делите код между бэкендом и фронтендом, вам нужно хранить DTO где-то в вашем фронтенд-коде, и мы рассмотрим этот случай ниже. Если вы храните функции запросов в `shared/api`, то именно там должны быть DTO, прямо рядом с функцией, которая их использует: shared/api/songs.ts ``` import type { ArtistDTO } from "./artists"; interface SongDTO { id: number; title: string; artist_ids: Array; } export function listSongs() { return fetch('/api/songs').then((res) => res.json() as Promise>); } ``` Как упоминалось в предыдущем разделе, хранение ваших запросов и DTO в Shared имеет преимущество того, что вы можете ссылаться на другие DTO. ### Куда положить мапперы[​](#куда-положить-мапперы "Прямая ссылка на этот заголовок") Мапперы — это функции, которые принимают DTO для преобразования, и, следовательно, они должны находиться рядом с определением DTO. На практике это означает, что если ваши запросы и DTO определены в `shared/api`, то и мапперы должны быть там же: shared/api/songs.ts ``` import type { ArtistDTO } from "./artists"; interface SongDTO { id: number; title: string; disc_no: number; artist_ids: Array; } interface Song { id: string; title: string; /** The full title of the song, including the disc number. */ fullTitle: string; artistIds: Array; } function adaptSongDTO(dto: SongDTO): Song { return { id: String(dto.id), title: dto.title, fullTitle: `${dto.disc_no} / ${dto.title}`, artistIds: dto.artist_ids.map(String), }; } export function listSongs() { return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO)); } ``` Если ваши запросы и хранилища определены в слайсах сущностей, то весь этот код должен быть там, с учётом ограничения кросс-импортов между сущностями: entities/song/api/dto.ts ``` import type { ArtistDTO } from "entities/artist/@x/song"; export interface SongDTO { id: number; title: string; disc_no: number; artist_ids: Array; } ``` entities/song/api/mapper.ts ``` import type { SongDTO } from "./dto"; export interface Song { id: string; title: string; /** Полное название песни, включая номер диска. */ fullTitle: string; artistIds: Array; } export function adaptSongDTO(dto: SongDTO): Song { return { id: String(dto.id), title: dto.title, fullTitle: `${dto.disc_no} / ${dto.title}`, artistIds: dto.artist_ids.map(String), }; } ``` entities/song/api/listSongs.ts ``` import { adaptSongDTO } from "./mapper"; export function listSongs() { return fetch('/api/songs').then(async (res) => (await res.json()).map(adaptSongDTO)); } ``` entities/song/model/songs.ts ``` import { createSlice, createEntityAdapter } from "@reduxjs/toolkit"; import { listSongs } from "../api/listSongs"; export const fetchSongs = createAsyncThunk('songs/fetchSongs', listSongs); const songAdapter = createEntityAdapter(); const songsSlice = createSlice({ name: "songs", initialState: songAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSongs.fulfilled, (state, action) => { songAdapter.upsertMany(state, action.payload); }) }, }); ``` ### Что делать с вложенными DTO[​](#что-делать-с-вложенными-dto "Прямая ссылка на этот заголовок") Самый проблемный момент — это когда ответ от бэкенда содержит несколько сущностей. Например, если песня включает в себя не только ID авторов, но и сами объекты данных об авторах целиком. В этом случае сущности не могут не знать друг о друге (если только мы не хотим выбрасывать данные или проводить серьезную беседу с командой бэкенда). Вместо того, чтобы придумывать решения для неявных связей между срезами (например, общий middleware, который будет диспатчить действия другим слайсам), предпочитайте явный кросс-импорт через `@x`-нотацию. Вот как мы можем это реализовать с Redux Toolkit: entities/song/model/songs.ts ``` import { createSlice, createEntityAdapter, createAsyncThunk, createSelector, } from '@reduxjs/toolkit' import { normalize, schema } from 'normalizr' import { getSong } from "../api/getSong"; // Объявляем схемы сущностей в normalizr export const artistEntity = new schema.Entity('artists') export const songEntity = new schema.Entity('songs', { artists: [artistEntity], }) const songAdapter = createEntityAdapter() export const fetchSong = createAsyncThunk( 'songs/fetchSong', async (id: string) => { const data = await getSong(id) // Нормализуем данные, чтобы редьюсеры могли загружать предсказуемый объект, например: // `action.payload = { songs: {}, artists: {} }` const normalized = normalize(data, songEntity) return normalized.entities } ) export const slice = createSlice({ name: 'songs', initialState: songAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSong.fulfilled, (state, action) => { songAdapter.upsertMany(state, action.payload.songs) }) }, }) const reducer = slice.reducer export default reducer ``` entities/song/@x/artist.ts ``` export { fetchSong } from "../model/songs"; ``` entities/artist/model/artists.ts ``` import { createSlice, createEntityAdapter } from '@reduxjs/toolkit' import { fetchSong } from 'entities/song/@x/artist' const artistAdapter = createEntityAdapter() export const slice = createSlice({ name: 'users', initialState: artistAdapter.getInitialState(), reducers: {}, extraReducers: (builder) => { builder.addCase(fetchSong.fulfilled, (state, action) => { // И здесь обрабатываем тот же ответ с бэкенда, добавляя исполнителей artistAdapter.upsertMany(state, action.payload.artists) }) }, }) const reducer = slice.reducer export default reducer ``` Это немного ограничивает преимущества изоляции слайсов, но чётко обозначает связь между этими двумя сущностями, которую мы не контролируем. Если эти сущности когда-либо будут рефакториться, их нужно будет рефакторить вместе. ## Глобальные типы и Redux[​](#глобальные-типы-и-redux "Прямая ссылка на этот заголовок") Глобальные типы — это типы, которые будут использоваться во всем приложении. Существует два вида глобальных типов, в зависимости от того, что им нужно знать: 1. Универсальные типы, которые не имеют никакой специфики приложения 2. Типы, которым нужно знать обо всем приложении Первый случай легко решить — поместите свои типы в Shared, в соответствующий сегмент. Например, если у вас есть интерфейс глобальной переменной для аналитики, вы можете поместить его в `shared/analytics`. warning Избегайте создания папки `shared/types`. Она группирует несвязанные вещи только на основе свойства «быть типом», и это свойство обычно бесполезно при поиске кода в проекте. Второй случай часто встречается в проектах с Redux без RTK. Ваш окончательный тип хранилища доступен только после того, как вы соедините все редьюсеры, но этот тип хранилища нужен селекторам, которые вы используете в приложении. Например, вот типичное определение хранилища в Redux: app/store/index.ts ``` import { combineReducers, rootReducer } from "redux"; import { songReducer } from "entities/song"; import { artistReducer } from "entities/artist"; const rootReducer = combineReducers(songReducer, artistReducer); const store = createStore(rootReducer); type RootState = ReturnType; type AppDispatch = typeof store.dispatch; ``` Было бы неплохо иметь типизированные хуки `useAppDispatch` и `useAppSelector` в `shared/store`, но они не могут импортировать `RootState` и `AppDispatch` из слоя App из-за [правила импорта для слоёв](/documentation/ru/docs/reference/layers.md#import-rule-on-layers): > Модуль в слайсе может импортировать другие слайсы только в том случае, если они расположены на слоях строго ниже. Рекомендуемое решение в этом случае — создать неявную зависимость между слоями Shared и App. Эти два типа, `RootState` и `AppDispatch`, вряд ли изменятся, и они будут знакомы разработчикам на Redux, поэтому неявная связь вряд ли станет проблемой. В TypeScript это можно сделать, объявив типы как глобальные, например так: app/store/index.ts ``` /* то же содержимое, что и в блоке кода до этого… */ declare type RootState = ReturnType; declare type AppDispatch = typeof store.dispatch; ``` shared/store/index.ts ``` import { useDispatch, useSelector, type TypedUseSelectorHook } from "react-redux"; export const useAppDispatch = useDispatch.withTypes() export const useAppSelector: TypedUseSelectorHook = useSelector; ``` ## Схемы валидации типов и Zod[​](#схемы-валидации-типов-и-zod "Прямая ссылка на этот заголовок") Если вы хотите проверить, что ваши данные соответствуют определенной форме или ограничениям, вы можете создать схему валидации. В TypeScript популярной библиотекой для этой задачи является [Zod](https://zod.dev). Схемы валидации также должны быть размещены рядом с кодом, который их использует, насколько это возможно. Схемы валидации похожи на мапперы (как обсуждалось в разделе [Объекты передачи данных (DTO) и мапперы](#data-transfer-objects-and-mappers)) в том смысле, что они принимают объект передачи данных и парсят его, выдавая ошибку, если парсинг не удался. Один из наиболее распространенных случаев валидации — это данные, поступающие с бэкенда. Обычно вы хотите пометить запрос как неудавшийся, если данные не соответствуют схеме, поэтому имеет смысл поместить схему в том же месте, что и функция запроса, что обычно является сегментом `api`. Если ваши данные поступают через пользовательский ввод, например, через форму, валидация должна происходить во время ввода данных. Вы можете разместить свою схему в сегменте `ui`, рядом с компонентом формы, или в сегменте `model`, если сегмент `ui` слишком перегружен. ## Типизация пропов компонентов и контекста[​](#типизация-пропов-компонентов-и-контекста "Прямая ссылка на этот заголовок") В целом, лучше хранить интерфейс пропов или контекста в том же файле, что и компонент или контекст, который их использует. Если у вас фреймворк с однофайловыми компонентами, например, Vue или Svelte, и вы не можете определить интерфейс пропов в том же файле, или вы хотите переиспользовать этот интерфейс между несколькими компонентами, создайте отдельный файл в той же папке, обычно в сегменте `ui`. Вот пример с JSX (React или Solid): pages/home/ui/RecentActions.tsx ``` interface RecentActionsProps { actions: Array<{ id: string; text: string }>; } export function RecentActions({ actions }: RecentActionsProps) { /* … */ } ``` И вот пример с интерфейсом, хранящимся в отдельном файле, для Vue: pages/home/ui/RecentActionsProps.ts ``` export interface RecentActionsProps { actions: Array<{ id: string; text: string }>; } ``` pages/home/ui/RecentActions.vue ``` ``` ## Декларационные файлы окружения (`*.d.ts`)[​](#декларационные-файлы-окружения-dts "Прямая ссылка на этот заголовок") Некоторые пакеты, например, [Vite](https://vitejs.dev) или [ts-reset](https://www.totaltypescript.com/ts-reset), требуют декларационные файлы окружения для работы в вашем приложении. Обычно они небольшие и несложные, поэтому часто не требуют какой-либо архитектуры, их можно просто поместить в папку `src/`. Чтобы `src` был более организованным, вы можете хранить их на слое App, в `app/ambient/`. Другие пакеты просто не имеют типов, и вам может понадобиться объявить их как нетипизированные или даже написать собственные типы для них. Хорошим местом для этих типов будет `shared/lib`, в папке типа `shared/lib/untyped-packages`. Создайте там файл `%LIBRARY_NAME%.d.ts` и объявите типы, которые вам нужны: shared/lib/untyped-packages/use-react-screenshot.d.ts ``` // У этой библиотеки нет типов, и мы не хотели заморачиваться с написанием своих. declare module "use-react-screenshot"; ``` ## Автогенерация типов[​](#автогенерация-типов "Прямая ссылка на этот заголовок") Часто бывает полезно генерировать типы из внешних источников, например, генерировать типы бэкенда из схемы OpenAPI. В этом случае создайте специальное место в вашем коде для этих типов, например, `shared/api/openapi`. Идеально, если вы также включите README в эту папку, который описывает, что это за файлы, как их перегенерировать и т. д. --- # White Labels WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/215) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Figma, brand uikit, templates, адаптируемость к брендам ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Тред) Про применение для white-labels (брендированных) проектов](https://t.me/feature_sliced/1543) * [(Презентация) Про white-labels приложения и дизайн](https://yadi.sk/i/5IdhzsWrpO3v4Q) --- # Кросс-импорты WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/220) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* > Кросс-импорты появляются тогда, когда слой/абстракция начинает брать слишком много ответственности, чем должна. Именно поэтому методология выделяет новые слои, которые позволяют расцепить эти кросс-импорты ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Тред) Про предполагаемую неизбежность кросс-импортов](https://t.me/feature_sliced/4515) * [(Тред) Про резолвинг кросс-импортов в сущностях](https://t.me/feature_sliced/3678) * [(Тред) Про кросс-импорты и ответственность](https://t.me/feature_sliced/3287) * [(Тред) Про импорты между сегментами](https://t.me/feature_sliced/4021) * [(Тред) Про кросс-импорты внутри shared](https://t.me/feature_sliced/3618) --- # Десегментация WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/148) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Ситуация[​](#situation "Прямая ссылка на этот заголовок") Очень часто на проектах встречается ситуация, когда модули, относящиеся к конкретному домену из предметной области, излишне десегментированы и раскиданы по проекту ``` ├── components/ | ├── DeliveryCard | ├── DeliveryChoice | ├── RegionSelect | ├── UserAvatar ├── actions/ | ├── delivery.js | ├── region.js | ├── user.js ├── epics/ | ├── delivery.js | ├── region.js | ├── user.js ├── constants/ | ├── delivery.js | ├── region.js | ├── user.js ├── helpers/ | ├── delivery.js | ├── region.js | ├── user.js ├── entities/ | ├── delivery/ | | ├── getters.js | | ├── selectors.js | ├── region/ | ├── user/ ``` ## Проблема[​](#problem "Прямая ссылка на этот заголовок") Проблема проявляется как минимум в нарушении принципа **High Cohesion** и излишнего растягивания **оси изменений** ## Если проигнорировать[​](#if-you-ignore-it "Прямая ссылка на этот заголовок") * При необходимости затронуть логику, например, доставки - нам придется держать в голове, что она лежит в нескольких местах и затронуть в коде именно несколько мест - что излишне растягивает нашу **Ось изменений** * Если нам надо изучить логику по пользователю, нам придется пройтись по всему-всему проекту, чтобы изучить в деталях **actions, epics, constants, entities, components** - вместо того, чтобы это лежало в одном месте * Неявные связи и неконтролируемость растущей предметной области * При таком подходе очень часто замыливается глаз и можно не заметить, как мы "создаем константы ради констант", создавая свалку в соответствующей директории проекта ## Решение[​](#solution "Прямая ссылка на этот заголовок") Располагать все модули, относящиеся к конкретному домену/юзкейсу - непосредственно рядом Чтобы при изучении конкретного модуля - все его составляющие лежали рядом, а не были раскиданы по проекту > Это также повышает discoverability и явность кодовой базы и связей между модулями ``` - ├── components/ - | ├── DeliveryCard - | ├── DeliveryChoice - | ├── RegionSelect - | ├── UserAvatar - ├── actions/ - | ├── delivery.js - | ├── region.js - | ├── user.js - ├── epics/{...} - ├── constants/{...} - ├── helpers/{...} ├── entities/ | ├── delivery/ + | | ├── ui/ # ~ components/ + | | | ├── card.js + | | | ├── choice.js + | | ├── model/ + | | | ├── actions.js + | | | ├── constants.js + | | | ├── epics.js + | | | ├── getters.js + | | | ├── selectors.js + | | ├── lib/ # ~ helpers | ├── region/ | ├── user/ ``` ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Статья) Про Low Coupling и High Cohesion наглядно](https://enterprisecraftsmanship.com/posts/cohesion-coupling-difference/) * [(Статья) Low Coupling и High Cohesion. Закон Деметры](https://medium.com/german-gorelkin/low-coupling-high-cohesion-d36369fb1be9) --- # Роутинг WIP Статья находится в процессе написания Чтобы ускорить ее появление, можно: * 📢 Поделиться обратной связью [в тикете (комментарии/эмодзи-реакция)](https://github.com/feature-sliced/documentation/issues/169) * 💬 Собрать в тикет накопленный по теме [материал из чата](https://t.me/feature_sliced) * ⚒️ Посодействовать [любым другим способом](https://github.com/feature-sliced/documentation/blob/master/CONTRIBUTING.md)
*🍰 Stay tuned!* ## Ситуация[​](#situation "Прямая ссылка на этот заголовок") Урлы к страницам хардкодятся в слоях ниже pages entities/post/card ``` ... ``` ## Проблема[​](#problem "Прямая ссылка на этот заголовок") Урлы не сконцентрированы в слое страниц, где им и место по скоупу ответственности ## Если проигнорировать[​](#if-you-ignore-it "Прямая ссылка на этот заголовок") Тогда при изменении урлов, придется держать в голове, что эти урлы (и логика урлов/редиректов) могут быть во всех слоях кроме pages А также это значит, что теперь даже простая карточка товара берет часть ответственности от страниц, что размазывает логику по проекту ## Решение[​](#solution "Прямая ссылка на этот заголовок") Определять работу с урлами/редиректами от уровня страниц и выше В слои ниже передавать через композицию/пропсы/фабрики ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [(Тред) Что если "зашивать" роутинг в entities/features/widgets](https://t.me/feature_sliced/4389) * [(Тред) Почему размазывае логику роутов только в pages](https://t.me/feature_sliced/3756) --- # Миграция с кастомной архитектуры Это руководство описывает подход, который может быть полезен при миграции с кастомной самодельной архитектуры на Feature-Sliced Design. Вот структура папок типичной кастомной архитектуры. Мы будем использовать ее в качестве примера в этом руководстве. Нажмите на синюю стрелку, чтобы открыть папку. 📁 src * 📁 actions * 📁 product * 📁 order * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 routes * 📁 products.jsx * 📄 products.\[id].jsx * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles * 📄 App.jsx * 📄 index.js ## Перед началом[​](#before-you-start "Прямая ссылка на этот заголовок") Самый важный вопрос, который нужно задать своей команде при рассмотрении перехода на Feature-Sliced Design, — *действительно ли вам это нужно?* Мы любим Feature-Sliced Design, но даже мы признаем, что некоторые проекты прекрасно обойдутся и без него. Вот несколько причин, по которым стоит рассмотреть переход: 1. Новые члены команды жалуются, что сложно достичь продуктивного уровня 2. Внесение изменений в одну часть кода **часто** приводит к тому, что ломается другая несвязанная часть 3. Добавление новой функциональности затруднено из-за огромного количества вещей, о которых нужно думать **Избегайте перехода на FSD против воли ваших коллег**, даже если вы являетесь тимлидом.
Сначала убедите своих коллег в том, что преимущества перевешивают стоимость миграции и стоимость изучения новой архитектуры вместо установленной. Также имейте в виду, что любые изменения в архитектуре незаметны для руководства в моменте. Убедитесь, что они поддерживают переход, прежде чем начинать, и объясните им, как этот переход может быть полезен для проекта. подсказка Если вам нужна помощь в убеждении менеджера проекта в том, что FSD вам полезен, вот несколько идей: 1. Миграция на FSD может происходить постепенно, поэтому она не остановит разработку новых функций 2. Хорошая архитектура может значительно сократить время, которое потребуется новым разработчикам для достижения производительности 3. FSD — это документированная архитектура, поэтому команде не нужно постоянно тратить время на поддержание собственной документации *** Если вы всё-таки приняли решение начать миграцию, то первое, что вам следует сделать, — настроить алиас для `📁 src`. Это будет полезно позже, чтоб ссылаться на папки верхнего уровня. Далее в тексте мы будем считать `@` псевдонимом для `./src`. ## Шаг 1. Разделите код по страницам[​](#divide-code-by-pages "Прямая ссылка на этот заголовок") Большинство кастомных архитектур уже имеют разделение по страницам, независимо от размера логики. Если у вас уже есть `📁 pages`, вы можете пропустить этот шаг. Если у вас есть только `📁 routes`, создайте `📁 pages` и попробуйте переместить как можно больше кода компонентов из `📁 routes`. Идеально, если у вас будет маленький файл роута и больший файл страницы. При перемещении кода создайте папку для каждой страницы и добавьте в нее индекс-файл: примечание Пока что ваши страницы могут импортировать друг из друга, это нормально. Позже будет отдельный шаг для устранения этих зависимостей, но сейчас сосредоточьтесь на установлении явного разделения по страницам. Файл роута: src/routes/products.\[id].js ``` export { ProductPage as default } from "@/pages/product" ``` Индекс-файл страницы: src/pages/product/index.js ``` export { ProductPage } from "./ProductPage.jsx" ``` Файл с компонентом страницы: src/pages/product/ProductPage.jsx ``` export function ProductPage(props) { return
; } ``` ## Шаг 2. Отделите все остальное от страниц[​](#separate-everything-else-from-pages "Прямая ссылка на этот заголовок") Создайте папку `📁 src/shared` и переместите туда все, что не импортируется из `📁 pages` или `📁 routes`. Создайте папку `📁 src/app` и переместите туда все, что импортирует страницы или роуты, включая сами роуты. Помните, что у слоя Shared нет слайсов, поэтому сегменты могут импортировать друг из друга. В итоге у вас должна получиться структура файлов, похожая на эту: 📁 src * 📁 app * 📁 routes * 📄 products.jsx * 📄 products.\[id].jsx * 📄 App.jsx * 📄 index.js * 📁 pages * 📁 product * 📁 ui * 📄 ProductPage.jsx * 📄 index.js * 📁 catalog * 📁 shared * 📁 actions * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles ## Шаг 3. Устраните кросс-импорты между страницами[​](#tackle-cross-imports-between-pages "Прямая ссылка на этот заголовок") Найдите все случаи, когда одна страница импортирует что-то из другой, и сделайте одно из двух: 1. Скопируйте код, который импортируется, в зависимую страницу, чтобы убрать зависимость 2. Переместите код в соответствующий сегмент в Shared: * если это часть UI-кита, переместите в `📁 shared/ui`; * если это константа конфигурации, переместите в `📁 shared/config`; * если это взаимодействие с бэкендом, переместите в `📁 shared/api`. примечание **Копирование само по себе не является архитектурной проблемой**, на самом деле иногда даже правильнее продублировать что-то, чем абстрагировать в новый переиспользуемый модуль. Дело в том, что иногда общие части страниц начинают расходиться, и в этих случаях вам не нужно, чтобы эти зависимости мешались. Однако существует смысл в принципе DRY ("don't repeat yourself" — "не повторяйтесь"), поэтому убедитесь, что вы не копируете бизнес-логику. В противном случае вам придется держать в голове, что баги нужно исправлять в нескольких местах одновременно. ## Шаг 4. Разберите слой Shared[​](#unpack-shared-layer "Прямая ссылка на этот заголовок") На данном этапе у вас может быть много всего в слое Shared, и в целом, следует избегать таких ситуаций. Причина этому в том, что слой Shared может быть зависимостью для любого другого слоя в вашем коде, поэтому внесение изменений в этот код автоматически более чревато непредвиденными последствиями. Найдите все объекты, которые используются только на одной странице, и переместите их в слайс этой страницы. И да, *это относится и к экшнам (actions), редьюсерам (reducers) и селекторам (selectors)*. Нет никакой пользы в группировке всех экшнов вместе, но есть польза в том, чтобы поместить актуальные экшны рядом с их местом использования. В итоге у вас должна получиться структура файлов, похожая на эту: 📁 src * 📁 app (unchanged) * 📁 pages * 📁 product * 📁 actions * 📁 reducers * 📁 selectors * 📁 ui * 📄 Component.jsx * 📄 Container.jsx * 📄 ProductPage.jsx * 📄 index.js * 📁 catalog * 📁 shared (only objects that are reused) * 📁 actions * 📁 api * 📁 components * 📁 containers * 📁 constants * 📁 i18n * 📁 modules * 📁 helpers * 📁 utils * 📁 reducers * 📁 selectors * 📁 styles ## Шаг 5. Распределите код по техническому назначению[​](#organize-by-technical-purpose "Прямая ссылка на этот заголовок") В FSD разделение по техническому назначению происходит с помощью *сегментов*. Существует несколько часто встречающихся сегментов: * `ui` — всё, что связано с отображением интерфейса: компоненты UI, форматирование дат, стили и т. д. * `api` — взаимодействие с бэкендом: функции запросов, типы данных, мапперы и т. д. * `model` — модель данных: схемы, интерфейсы, хранилища и бизнес-логика. * `lib` — библиотечный код, который нужен другим модулям на этом слайсе. * `config` — файлы конфигурации и фиче-флаги. Вы можете создавать свои собственные сегменты, если это необходимо. Убедитесь, что не создаете сегменты, которые группируют код по тому, чем он является, например, `components`, `actions`, `types`, `utils`. Вместо этого группируйте код по тому, для чего он предназначен. Перераспределите код ваших страниц по сегментам. У вас уже должен быть сегмент `ui`, теперь пришло время создать другие сегменты, например, `model` для ваших экшнов, редьюсеров и селекторов, или `api` для ваших thunk-ов и мутаций. Также перераспределите слой Shared, чтобы удалить следующие папки: * `📁 components`, `📁 containers` — большинство из их содержимого должно стать `📁 shared/ui`; * `📁 helpers`, `📁 utils` — если остались какие-то повторно используемые хелперы, сгруппируйте их по назначению, например, даты или преобразования типов, и переместите эти группы в `📁 shared/lib`; * `📁 constants` — так же сгруппируйте по назначению и переместите в `📁 shared/config`. ## Шаги по желанию[​](#optional-steps "Прямая ссылка на этот заголовок") ### Шаг 6. Создайте сущности/фичи ёмкостью из Redux-слайсов, которые используются на нескольких страницах[​](#form-entities-features-from-redux "Прямая ссылка на этот заголовок") Обычно эти переиспользуемые Redux-слайсы будут описывать что-то, что имеет отношение к бизнесу, например, продукты или пользователи, поэтому их можно переместить в слой Entities, одна сущность на одну папку. Если Redux-слайс скорее связан с действием, которое ваши пользователи хотят совершить в вашем приложении, например, комментарии, то его можно переместить в слой Features. Сущности и фичи должны быть независимы друг от друга. Если ваша бизнес-область содержит встроенные связи между сущностями, обратитесь к [руководству по бизнес-сущностям](/documentation/ru/docs/guides/examples/types.md#business-entities-and-their-cross-references) за советом по организации этих связей. API-функции, связанные с этими слайсами, могут остаться в `📁 shared/api`. ### Шаг 7. Проведите рефакторинг modules[​](#refactor-your-modules "Прямая ссылка на этот заголовок") Папка `📁 modules` обычно используется для бизнес-логики, поэтому она уже довольно похожа по своей природе на слой Features из FSD. Некоторые модули могут также описывать большие части пользовательского интерфейса, например, шапку приложения. В этом случае их можно переместить в слой Widgets. ### Шаг 8. Сформируйте чистый фундамент UI в `shared/ui`[​](#form-clean-ui-foundation "Прямая ссылка на этот заголовок") `📁 shared/ui`, в идеале, должен содержать набор UI-элементов, в которых нет бизнес-логики. Они также должны быть очень переиспользуемыми. Проведите рефакторинг UI-компонентов, которые раньше находились в `📁 components` и `📁 containers`, чтобы отделить бизнес-логику. Переместите эту бизнес-логику в верхние слои. Если она не используется в слишком многих местах, вы даже можете рассмотреть копирование как вариант. ## See also[​](#see-also "Прямая ссылка на этот заголовок") * [(Доклад) Ilya Klimov — Крысиные бега бесконечного рефакторинга: как не дать техническому долгу убить мотивацию и продукт](https://youtu.be/aOiJ3k2UvO4) --- # Миграция с v1 ## Зачем v2?[​](#why-v2 "Прямая ссылка на этот заголовок") Изначальная концепция **feature-slices** [была заявлена](https://t.me/feature_slices) еще в 2018 году. С тех пор прошло много трансформаций методологии, но при этом **[сохранялись базовые принципы](https://feature-sliced.github.io/featureslices.dev/v1.0.html)**: * Использование *стандартизированной* структуры фронтенд-проектов * Разбиение приложения в первую очередь - согласно *бизнес-логике* * Использование *изолированных фичей*, для предотвращения неявных сайд-эффектов и циклических зависимостей * Использование *Public API* с запретом лезть "во внутренности" модуля При этом, в прежней версии методологии все равно **оставались слабые места**, которые * Где-то приводили к бойлерплейту * Где-то к чрезмерному усложнению кодовой базы и неочевидным правилам между абстракциями * Где-то к неявным архитектурным решениям, что мешало поддержке проекта и онбордингу новых людей Новая версия методологии ([v2](https://github.com/feature-sliced/documentation)) призвана **устранить эти недостатки, сохраняя при этом и имеющиеся достоинства** подхода. С 2018 года [развивалась](https://github.com/kof/feature-driven-architecture/issues) и другая подобная методология - [**feature-driven**](https://github.com/feature-sliced/documentation/tree/rc/feature-driven), о которой заявил впервые [Oleg Isonen](https://github.com/kof). В результате слияния двух подходов, **были улучшены и доработаны существующие практики** - в сторону большей гибкости, понятности и эффективности при применении. > По итогу это повлияло даже на наименование методологии - *"feature-slice**d**"* ## Почему имеет смысл мигрировать проект на v2?[​](#why-does-it-make-sense-to-migrate-the-project-to-v2 "Прямая ссылка на этот заголовок") > `WIP:` Текущая версия методологии находится на стадии разработки и некоторые детали *могут измениться* #### 🔍 Более прозрачная и простая архитектура[​](#-more-transparent-and-simple-architecture "Прямая ссылка на этот заголовок") Методология (v2) предлагает **более интуитивно понятные и более распространенные среди разработчиков абстракции и способы разделения логики.** Все это крайне положительно влияет на привлечение новых людей, а также изучение текущего состояния проекта, и распределение бизнес-логики приложения. #### 📦 Более гибкая и честная модульность[​](#-more-flexible-and-honest-modularity "Прямая ссылка на этот заголовок") Методология (v2) позволяет **распределять логику более гибким способом:** * С возможностью рефакторить с нуля изолированные части * С возможностью опираться на одни и те же абстракции, но без лишних переплетений зависимостей * С более простыми требованиями для расположения нового модуля *(layer => slice => segment)* #### 🚀 Больше спецификации, планов, комьюнити[​](#-more-specifications-plans-community "Прямая ссылка на этот заголовок") На данный момент `core-team` ведет активную работу именно над последней (v2) версией методологии А значит именно для нее: * будет больше описанных кейсов / проблем * будет больше гайдов по применению * будет больше реальных примеров * будет в целом больше документации для онбординга новых людей и изучения концепций методологии * будет развиваться в дальнейшем тулкит для соблюдения концепций и конвенций по архитектуре > Само собой, будет поддержка пользователей и по первой версии - но для нас первоочередная все же последняя версия > > В будущем же, при следующих мажорных обновлениях - у вас сохранится доступ и к текущей версии (v2) методологии, **без рисков для ваших команд и проектов** ## Changelog[​](#changelog "Прямая ссылка на этот заголовок") ### `BREAKING` Layers[​](#breaking-layers "Прямая ссылка на этот заголовок") Теперь методология предполагает явное выделение слоев на верхнем уровне * `/app` > `/processes` > **`/pages`** > **`/features`** > `/entities` > `/shared` * *Т.е. не все теперь трактуется как фичи/страницы* * Такой подход позволяет [явно задать правила для слоев](https://t.me/atomicdesign/18708): * Чем **выше расположен слой** модуля - тем большим **контекстом** он располагает *(иными словами - каждый модуль слоя - может импортировать только модули нижележащих слоев, но не выше)* * Чем **ниже расположен слой** модуля - тем больше **опасности и ответственности**, чтобы внести в него изменения *(потому что, как правило - более переиспользуемыми являются именно нижележащие слои)* ### `BREAKING` Shared[​](#breaking-shared "Прямая ссылка на этот заголовок") Инфраструктурные абстракции `/ui`, `/lib`, `/api`, которые раньше лежали в src-корне проекта, теперь обособлены отдельной директорией `/src/shared` * `shared/ui` - Все так же общий uikit приложения (опционален) * *При этом никто не запрещает использовать здесь `Atomic Design` как раньше* * `shared/lib` - Набор вспомогательных библиотек для реализации логики * *По-прежнему - без свалки хелперов* * `shared/api` - Общий энтрипоинт для обращения к API * *Может прописываться и локально в каждой фиче/странице - но не рекомендуется* * Как и раньше - в `shared` не должно быть явной привязки к бизнес-логике * *При необходимости - нужно выносить эту связь на уровень `entities` или еще выше* ### `NEW` Entities, Processes[​](#new-entities-processes "Прямая ссылка на этот заголовок") В v2 **добавлены и другие новые абстракции**, для устранения проблем усложнения логики и сильной связности. * `/entities` - слой **бизнес-сущностей**, содержащий в себе слайсы, относящиеся напрямую к бизнес-моделям или синтетическим сущностям, необходимым только на фронтенде * *Примеры: `user`, `i18n`, `order`, `blog`* * `/processes` - слой **бизнес-процессов**, пронизывающих приложение * **Слой опционален**, обычно рекомендуется использовать, когда *логика разрастается и начинает размываться в нескольких страницах* * *Примеры: `payment`, `auth`, `quick-tour`* ### `BREAKING` Abstractions & Naming[​](#breaking-abstractions--naming "Прямая ссылка на этот заголовок") Теперь определены конкретные абстракции и [четкие рекомендации для их нейминга](/documentation/ru/docs/about/understanding/naming.md) #### Layers[​](#layers "Прямая ссылка на этот заголовок") * `/app` — **слой инициализации приложения** * *Прежние варианты: `app`, `core`, `init`, `src/index` (и такое бывает)* * `/processes` — [**слой бизнес-процессов**](https://github.com/feature-sliced/documentation/discussions/20) * *Прежние варианты: `processes`, `flows`, `workflows`* * `/pages` — **слой страниц приложения** * *Прежние варианты: `pages`, `screens`, `views`, `layouts`, `components`, `containers`* * `/features` — [**слой частей функциональности**](https://github.com/feature-sliced/documentation/discussions/23) * *Прежние варианты: `features`, `components`, `containers`* * `/entities` — [**слой бизнес-сущностей**](https://github.com/feature-sliced/documentation/discussions/18#discussioncomment-422649) * *Прежние варианты: `entities`, `models`, `shared`* * `/shared` — [**слой переиспользуемого инфраструктурного кода**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-453020) 🔥 * *Прежние варианты: `shared`, `common`, `lib`* #### Segments[​](#segments "Прямая ссылка на этот заголовок") * `/ui` — [**UI-сегмент**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-453132) 🔥 * *Прежние варианты: `ui`, `components`, `view`* * `/model` — [**БЛ-сегмент**](https://github.com/feature-sliced/documentation/discussions/31#discussioncomment-472645) 🔥 * *Прежние варианты: `model`, `store`, `state`, `services`, `controller`* * `/lib` — сегмент **вспомогательного кода** * *Прежние варианты: `lib`, `libs`, `utils`, `helpers`* * `/api` — [**API-сегмент**](https://github.com/feature-sliced/documentation/discussions/66) * *Прежние варианты: `api`, `service`, `requests`, `queries`* * `/config` — **сегмент конфигурации приложения** * *Прежние варианты: `config`, `env`, `get-env`* ### `REFINED` Low coupling[​](#refined-low-coupling "Прямая ссылка на этот заголовок") Теперь гораздо проще [соблюдать принцип низкой связности](/documentation/ru/docs/reference/slices-segments.md#zero-coupling-high-cohesion) между модулями, благодаря новым слоям. *При этом по-прежнему рекомендуется максимально избегать случаев, где крайне трудно "расцепить" модули* ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [Заметки с доклада "React SPB Meetup #1"](https://t.me/feature_slices) * [React Berlin Talk - Oleg Isonen "Feature Driven Architecture"](https://www.youtube.com/watch?v=BWAeYuWFHhs) * [Сравнение с v1 (community-chat)](https://t.me/feature_sliced/493) * [Новые идеи v2 с пояснениями (atomicdesign-chat)](https://t.me/atomicdesign/18708) * [Обсуждение абстракций и нейминга для новой версии методологии (v2)](https://github.com/feature-sliced/documentation/discussions/31) --- # Миграция с v2.0 на v2.1 Основным изменением в v2.1 является новая ментальная модель разложения интерфейса — сначала страницы. В версии FSD 2.0 рекомендовалось найти сущности и фичи в вашем интерфейсе, рассматривая даже малейшие части представления сущностей и интерактивность как кандидаты на декомпозицию. Затем вы бы могли строить виджеты и страницы из сущностей и фич. В этой модели декомпозиции большая часть логики находилась в сущностях и фичах, а страницы были просто композиционными слоями, которые сами по себе не имели большого значения. В версии FSD 2.1 мы рекомендуем начинать со страниц, и возможно даже на них и остановиться. Большинство людей уже знают, как разделить приложение на страницы, и страницы также часто являются отправной точкой при попытке найти компонент в кодовой базе. В новой модели декомпозиции вы храните большую часть интерфейса и логики в каждой отдельной странице, а повторно используемый фундамент — в Shared. Если возникнет необходимость переиспользования бизнес-логики на нескольких страницах, вы можете переместить её на слой ниже. Другим нововведением в Feature-Sliced Design 2.1 является стандартизация кросс-импортов между сущностями с помощью `@x`-нотации. ## Как мигрировать[​](#how-to-migrate "Прямая ссылка на этот заголовок") В версии 2.1 нет ломающих изменений, что означает, что проект, написанный с использованием FSD v2.0, также является валидным проектом в FSD v2.1. Однако мы считаем, что новая ментальная модель более полезна для команд и особенно для обучения новых разработчиков, поэтому рекомендуем внести небольшие изменения в вашу декомпозицию. ### Соедините слайсы[​](#соедините-слайсы "Прямая ссылка на этот заголовок") Простой способ начать — запустить на проекте наш линтер, [Steiger](https://github.com/feature-sliced/steiger). Steiger построен с новой ментальной моделью, и наиболее полезные правила будут: * [`insignificant-slice`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice) — если сущность или фича используется только на одной странице, это правило предложит целиком переместить код этой сущности или фичи прямо в эту страницу. * [`excessive-slicing`](https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/excessive-slicing) — если у слоя слишком много слайсов, это обычно означает, что декомпозиция слишком мелкая. Это правило предложит объединить или сгруппировать некоторые слайсы, чтобы помочь в навигации по проекту. ``` npx steiger src ``` Это поможет вам определить, какие слайсы используются только один раз, чтобы вы могли ещё раз подумать, действительно ли они необходимы. Помните, что слой формирует своего рода глобальное пространство имен для всех слайсов внутри него. Точно так же, как вы не захотите загрязнять глобальное пространство имен переменными, которые используются только один раз, вы должны относиться к месту в пространстве имен слоя как к ценному месту, которое следует использовать сдержанно. ### Стандартизируйте кросс-импорты[​](#стандартизируйте-кросс-импорты "Прямая ссылка на этот заголовок") Если у вас были кросс-импорты в вашем проекте до этого (мы не осуждаем!), вы теперь можете воспользоваться новой нотацией для кросс-импортов в Feature-Sliced Design — `@x`-нотацией. Она выглядит так: entities/B/some/file.ts ``` import type { EntityA } from "entities/A/@x/B"; ``` Чтоб узнать больше об этом, обратитесь к разделу [Публичный API для кросс-импортов](/documentation/ru/docs/reference/public-api.md#public-api-for-cross-imports) в разделе справочника. --- # Использование с Electron Electron-приложения имеют особую архитектуру, состоящую из нескольких процессов с разными ответственностями. Применение FSD в таком контексте требует адаптации структуры под специфику Electron. ``` └── src ├── app # Общий слой app │ ├── main # Main процесс │ │ └── index.ts # Точка входа main процесса │ ├── preload # Preload скрипт и Context Bridge │ │ └── index.ts # Точка входа preload │ └── renderer # Renderer процесс │ └── index.html # Точка входа renderer процесса ├── main │ ├── features │ │ └── user │ │ └── ipc │ │ ├── get-user.ts │ │ └── send-user.ts │ ├── entities │ └── shared ├── renderer │ ├── pages │ │ ├── settings │ │ │ ├── ipc │ │ │ │ ├── get-user.ts │ │ │ │ └── save-user.ts │ │ │ ├── ui │ │ │ │ └── user.tsx │ │ │ └── index.ts │ │ └── home │ │ ├── ui │ │ │ └── home.tsx │ │ └── index.ts │ ├── widgets │ ├── features │ ├── entities │ └── shared └── shared # Общий код между main и renderer └── ipc # Описание IPC (наименование event'ов, контракты) ``` ## Правила для публичного API[​](#правила-для-публичного-api "Прямая ссылка на этот заголовок") Каждый процесс должен иметь свой публичный API, как пример, нельзя импортировать модули из `main` в `renderer`. Общедоступным между процессами кодом является только папка `src/shared`. Она же необходима для описания контрактов по взаимодействию процессов. ## Дополнительные изменения в стандартной структуре[​](#дополнительные-изменения-в-стандартной-структуре "Прямая ссылка на этот заголовок") Предлагается использовать новый сегмент `ipc`, в котором происходит взаимодействие между процессами. Слои `pages` и `widgets`, исходя из названия, не должны присутствовать в `src/main`, вы можете использовать `features`, `entities` и `shared`. Слой `app` в `src` содержит точки входа для `main` и `renderer`, а также IPC. Сегментам в слое `app` нежелательно иметь точек пересечения ## Пример взаимодействия[​](#пример-взаимодействия "Прямая ссылка на этот заголовок") src/shared/ipc/channels.ts ``` export const CHANNELS = { GET_USER_DATA: 'GET_USER_DATA', SAVE_USER: 'SAVE_USER', } as const; export type TChannelKeys = keyof typeof CHANNELS; ``` src/shared/ipc/events.ts ``` import { CHANNELS } from './channels'; export interface IEvents { [CHANNELS.GET_USER_DATA]: { args: void, response?: { name: string; email: string; }; }; [CHANNELS.SAVE_USER]: { args: { name: string; }; response: void; }; } ``` src/shared/ipc/preload.ts ``` import { CHANNELS } from './channels'; import type { IEvents } from './events'; type TOptionalArgs = T extends void ? [] : [args: T]; export type TElectronAPI = { [K in keyof typeof CHANNELS]: (...args: TOptionalArgs) => IEvents[typeof CHANNELS[K]]['response']; }; ``` src/app/preload/index.ts ``` import { contextBridge, ipcRenderer } from 'electron'; import { CHANNELS, type TElectronAPI } from 'shared/ipc'; const API: TElectronAPI = { [CHANNELS.GET_USER_DATA]: () => ipcRenderer.sendSync(CHANNELS.GET_USER_DATA), [CHANNELS.SAVE_USER]: args => ipcRenderer.invoke(CHANNELS.SAVE_USER, args), } as const; contextBridge.exposeInMainWorld('electron', API); ``` src/main/features/user/ipc/send-user.ts ``` import { ipcMain } from 'electron'; import { CHANNELS } from 'shared/ipc'; export const sendUser = () => { ipcMain.on(CHANNELS.GET_USER_DATA, ev => { ev.returnValue = { name: 'John Doe', email: 'john.doe@example.com', }; }); }; ``` src/renderer/pages/user-settings/ipc/get-user.ts ``` import { CHANNELS } from 'shared/ipc'; export const getUser = () => { const user = window.electron[CHANNELS.GET_USER_DATA](); return user ?? { name: 'John Donte', email: 'john.donte@example.com' }; }; ``` ## См. также[​](#см-также "Прямая ссылка на этот заголовок") * [Документация по моделям процессов](https://www.electronjs.org/docs/latest/tutorial/process-model) * [Документация по изоляции контекстов](https://www.electronjs.org/docs/latest/tutorial/context-isolation) * [Документация по IPC](https://www.electronjs.org/docs/latest/tutorial/ipc) * [Пример](https://github.com/feature-sliced/examples/tree/master/examples/electron) --- # Использование с Next.js FSD совместим с Next.js как в варианте App Router, так и в варианте Pages Router, если устранить главный конфликт — папки `app` и `pages`. ## App Router[​](#app-router "Прямая ссылка на этот заголовок") ### Конфликт между FSD и Next.js в слое `app`[​](#conflict-between-fsd-and-nextjs-in-the-app-layer "Прямая ссылка на этот заголовок") Next.js предлагает использовать папку `app` для определения маршрутов приложения. Он ожидает, что файлы в папке `app` будут соответствовать маршрутам. Этот механизм маршрутизации **не соответствует** концепции FSD, потому что невозможно сохранить плоскую структуру слайсов. Чтоб решить эту проблему, перенесите Next.js-овскую папку `app` в корень проекта, а затем импортируйте FSD-страницы из `src`, где располагаются слои FSD, в Next.js-овскую папку `app`. Вам также нужно будет добавить в корень проекта папку `pages`, иначе Next.js будет пытаться использовать `src/pages` в качестве Pages Router, даже если вы используете App Router, что приведёт к ошибкам при сборке проекта. Имеет смысл положить внутрь этой корневой папки `pages` файл `README.md` с описанием, почему эта папка нужна, даже когда она пустая. ``` ├── app # Папка app (Next.js) │ ├── api │ │ └── get-example │ │ └── route.ts │ └── example │ └── page.tsx ├── pages # Пустая папка pages (Next.js) │ └── README.md └── src ├── app │ └── api-routes # API-маршруты ├── pages │ └── example │ ├── index.ts │ └── ui │ └── example.tsx ├── widgets ├── features ├── entities └── shared ``` Пример ре-экспорта страницы из `src/pages` в Next.js-овском `app`: app/example/page.tsx ``` export { ExamplePage as default, metadata } from '@/pages/example'; ``` ### Middleware[​](#middleware "Прямая ссылка на этот заголовок") Если вы используете middleware в проекте, оно обязательно должно располагаться в корне проекта рядом с Next.js-овскими папками `app` и `pages`. ### Instrumentation[​](#instrumentation "Прямая ссылка на этот заголовок") Файл `instrumentation.js` позволяет отслеживать производительность и поведение вашего приложения. Если вы его используете, то он обязательно должен находиться в корне проекта по аналогии с `middleware.js` ## Pages Router[​](#pages-router "Прямая ссылка на этот заголовок") ### Конфликт между FSD и Next.js в слое `pages`[​](#conflict-between-fsd-and-nextjs-in-the-pages-layer "Прямая ссылка на этот заголовок") Роуты страниц должны помещаться в папку `pages` в корне проекта по аналогии с папкой `app` для App Router. Структура внутри `src`, где располагаются папки слоёв, остаётся без изменений. ``` ├── pages # Папка pages (Next.js) │ ├── _app.tsx │ ├── api │ │ └── example.ts # Ре-экспорт API-маршрутов │ └── example │ └── index.tsx └── src ├── app │ ├── custom-app │ │ └── custom-app.tsx # Кастомный компонент App │ └── api-routes │ └── get-example-data.ts # API-маршрут ├── pages │ └── example │ ├── index.ts │ └── ui │ └── example.tsx ├── widgets ├── features ├── entities └── shared ``` Пример ре-экспорта страницы из `src/pages` в Next.js-овском `pages`: pages/example/index.tsx ``` export { Example as default } from '@/pages/example'; ``` ### Кастомный компонент `_app`[​](#custom-_app-component "Прямая ссылка на этот заголовок") Вы можете поместить ваш кастомный компонент App либо в `src/app/_app` либо в `src/app/custom-app`: src/app/custom-app/custom-app.tsx ``` import type { AppProps } from 'next/app'; export const MyApp = ({ Component, pageProps }: AppProps) => { return ( <>

My Custom App component

); }; ``` pages/\_app.tsx ``` export { App as default } from '@/app/custom-app'; ``` ## Route Handlers (API-маршруты)[​](#route-handlers-api-routes "Прямая ссылка на этот заголовок") Используйте сегмент `api-routes` в слое `app` для работы c Route Handlers. Будьте внимательны при написании бэкенд-кода в структуре FSD — FSD в первую очередь предназначен для фронтенда, и именно это люди будут ожидать в нём найти. Если вам нужно много эндпоинтов, попробуйте выделить их в отдельный пакет в монорепозитории. * App Router * Pages Router src/app/api-routes/get-example-data.ts ``` import { getExamplesList } from '@/shared/db'; export const getExampleData = () => { try { const examplesList = getExamplesList(); return Response.json({ examplesList }); } catch { return Response.json(null, { status: 500, statusText: 'Ouch, something went wrong', }); } }; ``` app/api/example/route.ts ``` export { getExampleData as GET } from '@/app/api-routes'; ``` src/app/api-routes/get-example-data.ts ``` import type { NextApiRequest, NextApiResponse } from 'next'; const config = { api: { bodyParser: { sizeLimit: '1mb', }, }, maxDuration: 5, }; const handler = (req: NextApiRequest, res: NextApiResponse) => { res.status(200).json({ message: 'Hello from FSD' }); }; export const getExampleData = { config, handler } as const; ``` src/app/api-routes/index.ts ``` export { getExampleData } from './get-example-data'; ``` app/api/example.ts ``` import { getExampleData } from '@/app/api-routes'; export const config = getExampleData.config; export default getExampleData.handler; ``` ## Дополнительные рекомендации[​](#additional-recommendations "Прямая ссылка на этот заголовок") * Используйте сегмент `db` в слое `shared` для описания запросов к БД и их дальнейшего использования в вышестоящих слоях. * Логику кэширования и ревалидации запросов лучше держать там же, где и сами запросы. ## См. также[​](#see-also "Прямая ссылка на этот заголовок") * [Структура проекта Next.js](https://nextjs.org/docs/app/getting-started/project-structure) * [Компоновка страниц Next.js](https://nextjs.org/docs/app/getting-started/layouts-and-pages) --- # Использование с NuxtJS В NuxtJS проекте возможно реализовать FSD, однако возникают конфликты из-за различий между требованиями к структуре проекта NuxtJS и принципами FSD: * Изначально, NuxtJS предлагает файловую структуру проекта без папки `src`, то есть в корне проекта. * Файловый роутинг находится в папке `pages`, а в FSD эта папка отведена под плоскую структуру слайсов. ## Добавление алиаса для `src` директории[​](#добавление-алиаса-для-src-директории "Прямая ссылка на этот заголовок") Добавьте обьект `alias` в ваш конфиг: nuxt.config.ts ``` export default defineNuxtConfig({ devtools: { enabled: true }, // Не относятся к FSD, включёны при старте проекта alias: { "@": '../src' }, }) ``` ## Выбор способа настройки роутера[​](#выбор-способа-настройки-роутера "Прямая ссылка на этот заголовок") В NuxtJS есть два способа настройки роутинга - с помощью конфига и с помощью файловой структуры. В случае с файловым роутингом вы будете создавать index.vue файлы в папках внутри директории app/routes, а в случае конфига - настраивать роуты в `router.options.ts` файле. ### Роутинг с помощью конфига[​](#роутинг-с-помощью-конфига "Прямая ссылка на этот заголовок") В слое `app` создайте файл `router.options.ts`, и экспортируйте из него обьект конфига: app/router.options.ts ``` import type { RouterConfig } from '@nuxt/schema'; export default { routes: (_routes) => [], }; ``` Чтобы добавить страницу `Home` в проект, вам нужно сделать следующие шаги: * Добавить слайс страницы внутри слоя `pages` * Добавить соответствующий роут в конфиг `app/router.config.ts` Для того чтобы создать слайс страницы, воспользуемся [CLI](https://github.com/feature-sliced/cli): ``` fsd pages home ``` Создайте файл `home-page.vue` внутри сегмента ui, откройте к нему доступ с помощью Public API src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page'; ``` Таким образом, файловая структура будет выглядеть так: ``` |── src │ ├── app │ │ ├── router.config.ts │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.vue │ │ │ ├── index.ts ``` Наконец, добавим роут в конфиг: app/router.config.ts ``` import type { RouterConfig } from '@nuxt/schema' export default { routes: (_routes) => [ { name: 'home', path: '/', component: () => import('@/pages/home.vue').then(r => r.default || r) } ], } ``` ### Файловый роутинг[​](#файловый-роутинг "Прямая ссылка на этот заголовок") В первую очередь, создайте `src` директорию в корне проекта, а также создайте внутри этой директории слои app и pages и папку routes внутри слоя app. Таким образом, ваша файловая структура должна выглядеть так: ``` ├── src │ ├── app │ │ ├── routes │ ├── pages # Папка pages, закреплённая за FSD ``` Для того чтобы NuxtJS использовал папку routes внутри слоя `app` для файлового роутинга, вам нужно изменить `nuxt.config.ts` следующим образом: nuxt.config.ts ``` export default defineNuxtConfig({ devtools: { enabled: true }, // Не относятся к FSD, включёны при старте проекта alias: { "@": '../src' }, dir: { pages: './src/app/routes' } }) ``` Теперь, вы можете создавать роуты для страниц внутри `app` и подключать к ним страницы из `pages`. Например, чтобы добавить страницу `Home` в проект, вам нужно сделать следующие шаги: * Добавить слайс страницы внутри слоя `pages` * Добавить соответствующий роут внутрь слоя `app` * Совместить страницу из слайса с роутом Для того чтобы создать слайс страницы, воспользуемся [CLI](https://github.com/feature-sliced/cli): ``` fsd pages home ``` Создайте файл `home-page.vue` внутри сегмента ui, откройте к нему доступ с помощью Public API src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page'; ``` Создайте роут для этой страницы внутри слоя `app`: ``` ├── src │ ├── app │ │ ├── routes │ │ │ ├── index.vue │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.vue │ │ │ ├── index.ts ``` Добавьте внутрь `index.vue` файла компонент вашей страницы: src/app/routes/index.vue ``` ``` ## Что делать с `layouts`?[​](#что-делать-с-layouts "Прямая ссылка на этот заголовок") Вы можете разместить layouts внутри слоя `app`, для этого нужно изменить конфиг следующим образом: nuxt.config.ts ``` export default defineNuxtConfig({ devtools: { enabled: true }, // Не относятся к FSD, включёны при старте проекта alias: { "@": '../src' }, dir: { pages: './src/app/routes', layouts: './src/app/layouts' } }) ``` ## См. также[​](#см-также "Прямая ссылка на этот заголовок") * [Документация по изменению конфига директорий в NuxtJS](https://nuxt.com/docs/api/nuxt-config#dir) * [Документация по изменению конфига роутера в NuxtJS](https://nuxt.com/docs/guide/recipes/custom-routing#router-config) * [Документация по изменению алиасов в NuxtJS](https://nuxt.com/docs/api/nuxt-config#alias) --- # Использование с React Query ## Проблема «куда положить ключи»[​](#проблема-куда-положить-ключи "Прямая ссылка на этот заголовок") ### Решение - разбить по сущностям[​](#решение---разбить-по-сущностям "Прямая ссылка на этот заголовок") Если в проекте уже присутствует разделение на сущности, и каждый запрос соответствует одной сущности, наиболее чистым будет разделение по сущностям. В таком случае, предлагаем использовать следующую структуру: ``` └── src/ # ├── app/ # | ... # ├── pages/ # | ... # ├── entities/ # | ├── {entity}/ # | ... └── api/ # | ├── `{entity}.query` # Фабрика запросов, где определены ключи и функции | ├── `get-{entity}` # Функция получения сущности | ├── `create-{entity}` # Функция создания сущности | ├── `update-{entity}` # Функция обновления объекта | ├── `delete-{entity}` # Функция удаления объекта | ... # | # ├── features/ # | ... # ├── widgets/ # | ... # └── shared/ # ... # ``` Если среди сущностей есть связи (например, у сущности Страна есть поле-список сущностей Город), то можно воспользоваться [публичным API для кросс-импортов](/documentation/ru/docs/reference/public-api.md#public-api-for-cross-imports) или рассмотреть альтернативное решение ниже. ### Альтернативное решение — хранить запросы в общем доступе.[​](#альтернативное-решение--хранить-запросы-в-общем-доступе "Прямая ссылка на этот заголовок") В случаях, когда не подходит разделение по сущностям, можно рассмотреть следующую структуру: ``` └── src/ # ... # └── shared/ # ├── api/ # ... ├── `queries` # Query-factories | ├── `document.ts` # | ├── `background-jobs.ts` # | ... # └── index.ts # ``` Затем в `@/shared/api/index.ts`: @/shared/api/index.ts ``` export { documentQueries } from "./queries/document"; ``` ## Проблема «Куда мутации?»[​](#проблема-куда-мутации "Прямая ссылка на этот заголовок") Мутации не рекомендуется смешивать с запросами. Возможны два варианта: ### 1. Определить кастомный хук в сегменте api рядом с местом использования[​](#1-определить-кастомный-хук-в-сегменте-api-рядом-с-местом-использования "Прямая ссылка на этот заголовок") @/features/update-post/api/use-update-title.ts ``` export const useUpdateTitle = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ id, newTitle }) => apiClient .patch(`/posts/${id}`, { title: newTitle }) .then((data) => console.log(data)), onSuccess: (newPost) => { queryClient.setQueryData(postsQueries.ids(id), newPost); }, }); }; ``` ### 2. Определить функцию мутации в другом месте (Shared или Entities) и использовать `useMutation` напрямую в компоненте[​](#2-определить-функцию-мутации-в-другом-месте-shared-или-entities-и-использовать-usemutation-напрямую-в-компоненте "Прямая ссылка на этот заголовок") ``` const { mutateAsync, isPending } = useMutation({ mutationFn: postApi.createPost, }); ``` @/pages/post-create/ui/post-create-page.tsx ``` export const CreatePost = () => { const { classes } = useStyles(); const [title, setTitle] = useState(""); const { mutate, isPending } = useMutation({ mutationFn: postApi.createPost, }); const handleChange = (e: ChangeEvent) => setTitle(e.target.value); const handleSubmit = (e: FormEvent) => { e.preventDefault(); mutate({ title, userId: DEFAULT_USER_ID }); }; return (
Create ); }; ``` ## Организация запросов[​](#организация-запросов "Прямая ссылка на этот заголовок") ### Фабрика запросов[​](#фабрика-запросов "Прямая ссылка на этот заголовок") В этом гайде рассмотрим, как использовать фабрику запросов (объект, где значениями ключа - являются функции, возвращающие список ключей запроса) ``` const keyFactory = { all: () => ["entity"], lists: () => [...postQueries.all(), "list"], }; ``` к сведению `queryOptions` - встроенная утилита react-query\@v5 (опционально) ``` queryOptions({ queryKey, ...options, }); ``` Для большей типобезопасности и дальнейшей совместимости со следующими версиями react-query, а также упрощенного доступа к функциям и ключам запросов - можно использовать встроенную в функцию queryOptions из “@tanstack/react-query” [(Подробнее здесь)](https://tkdodo.eu/blog/the-query-options-api#queryoptions). ### 1. Создание Фабрики запросов[​](#1-создание-фабрики-запросов "Прямая ссылка на этот заголовок") @/entities/post/api/post.queries.ts ``` import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { getPosts } from "./get-posts"; import { getDetailPost } from "./get-detail-post"; import { PostDetailQuery } from "./query/post.query"; export const postQueries = { all: () => ["posts"], lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) => queryOptions({ queryKey: [...postQueries.lists(), page, limit], queryFn: () => getPosts(page, limit), placeholderData: keepPreviousData, }), details: () => [...postQueries.all(), "detail"], detail: (query?: PostDetailQuery) => queryOptions({ queryKey: [...postQueries.details(), query?.id], queryFn: () => getDetailPost({ id: query?.id }), staleTime: 5000, }), }; ``` ### 2. Применение Фабрики запросов в коде приложения[​](#2-применение-фабрики-запросов-в-коде-приложения "Прямая ссылка на этот заголовок") ``` import { useParams } from "react-router-dom"; import { postApi } from "@/entities/post"; import { useQuery } from "@tanstack/react-query"; type Params = { postId: string; }; export const PostPage = () => { const { postId } = useParams(); const id = parseInt(postId || ""); const { data: post, error, isLoading, isError, } = useQuery(postApi.postQueries.detail({ id })); if (isLoading) { return
Loading...
; } if (isError || !post) { return <>{error?.message}; } return (

Post id: {post.id}

{post.title}

{post.body}

Owner: {post.userId}
); }; ``` ### Преимущества использования Фабрики запросов[​](#преимущества-использования-фабрики-запросов "Прямая ссылка на этот заголовок") * **Структурирование запросов:** Фабрика позволяет организовать все запросы к API в одном месте, что делает код более читаемым и поддерживаемым. * **Удобный доступ к запросам и ключам:** Фабрика предоставляет удобные методы для доступа к различным типам запросов и их ключам. * **Возможность рефетчинга запросов:** Фабрика обеспечивает возможность легкой рефетчинга без необходимости изменения ключей запросов в разных частях приложения. ## Пагинация[​](#пагинация "Прямая ссылка на этот заголовок") В этом разделе рассмотрим пример функции `getPosts`, которая выполняет запрос к API для получения сущностей постов с применением пагинации. ### 1. Создание функции `getPosts`[​](#1-создание-функции-getposts "Прямая ссылка на этот заголовок") Функция getPosts находится в файле `get-posts.ts`, который находится в сегменте API. @/pages/post-feed/api/get-posts.ts ``` import { apiClient } from "@/shared/api/base"; import { PostWithPaginationDto } from "./dto/post-with-pagination.dto"; import { PostQuery } from "./query/post.query"; import { mapPost } from "./mapper/map-post"; import { PostWithPagination } from "../model/post-with-pagination"; const calculatePostPage = (totalCount: number, limit: number) => Math.floor(totalCount / limit); export const getPosts = async ( page: number, limit: number, ): Promise => { const skip = page * limit; const query: PostQuery = { skip, limit }; const result = await apiClient.get("/posts", query); return { posts: result.posts.map((post) => mapPost(post)), limit: result.limit, skip: result.skip, total: result.total, totalPages: calculatePostPage(result.total, limit), }; }; ``` ### 2. Фабрика запросов для пагинации[​](#2-фабрика-запросов-для-пагинации "Прямая ссылка на этот заголовок") Фабрика запросов `postQueries` определяет различные варианты запросов для работы с постами, включая запрос списка постов с заранее определенной страницей и лимитом. ``` import { keepPreviousData, queryOptions } from "@tanstack/react-query"; import { getPosts } from "./get-posts"; export const postQueries = { all: () => ["posts"], lists: () => [...postQueries.all(), "list"], list: (page: number, limit: number) => queryOptions({ queryKey: [...postQueries.lists(), page, limit], queryFn: () => getPosts(page, limit), placeholderData: keepPreviousData, }), }; ``` ### 3. Использование в коде приложения[​](#3-использование-в-коде-приложения "Прямая ссылка на этот заголовок") @/pages/home/ui/index.tsx ``` export const HomePage = () => { const itemsOnScreen = DEFAULT_ITEMS_ON_SCREEN; const [page, setPage] = usePageParam(DEFAULT_PAGE); const { data, isFetching, isLoading } = useQuery( postApi.postQueries.list(page, itemsOnScreen), ); return ( <> setPage(page)} page={page} count={data?.totalPages} variant="outlined" color="primary" /> ); }; ``` примечание Пример упрощен, полная версия доступна на [GitHub](https://github.com/ruslan4432013/fsd-react-query-example) ## `QueryProvider` для управления запросами[​](#queryprovider-для-управления-запросами "Прямая ссылка на этот заголовок") В этом гайде рассмотрим, как организовать `QueryProvider`. ### 1. Создание `QueryProvider`[​](#1-создание-queryprovider "Прямая ссылка на этот заголовок") Файл `query-provider.tsx` расположен по пути `@/app/providers/query-provider.tsx`. @/app/providers/query-provider.tsx ``` import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactNode } from "react"; type Props = { children: ReactNode; client: QueryClient; }; export const QueryProvider = ({ client, children }: Props) => { return ( {children} ); }; ``` ### 2. Создание `QueryClient`[​](#2-создание-queryclient "Прямая ссылка на этот заголовок") `QueryClient` представляет собой экземпляр, используемый для управления запросами к API. Файл `query-client.ts` расположен по пути `@/shared/api/query-client.ts`. `QueryClient` создается с определенными настройками для кэширования запросов. @/shared/api/query-client.ts ``` import { QueryClient } from "@tanstack/react-query"; export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000, }, }, }); ``` ## Кодогенерация[​](#кодогенерация "Прямая ссылка на этот заголовок") Существуют инструменты для автоматической генерации кода, которые менее гибкие, по сравнению с теми, что можно настроить, как описано выше. Если ваш Swagger-файл хорошо структурирован, и вы используете одно из таких инструментов, то возможно имеет смысл сгенерировать весь код в каталоге @/shared/api. ## Дополнительный совет по организации RQ[​](#дополнительный-совет-по-организации-rq "Прямая ссылка на этот заголовок") ### API-Клиент[​](#api-клиент "Прямая ссылка на этот заголовок") Используя собственный класс клиента API в общем слое shared, можно стандартизировать настройку и работу с API в проекте. Это позволяет управлять логированием, заголовками и форматом обмена данными (например, JSON или XML) из одного места. Такой подход облегчает поддержку и развитие проекта, поскольку упрощает изменения и обновления взаимодействия с API. @/shared/api/api-client.ts ``` import { API_URL } from "@/shared/config"; export class ApiClient { private baseUrl: string; constructor(url: string) { this.baseUrl = url; } async handleResponse(response: Response): Promise { if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } try { return await response.json(); } catch (error) { throw new Error("Error parsing JSON response"); } } public async get( endpoint: string, queryParams?: Record, ): Promise { const url = new URL(endpoint, this.baseUrl); if (queryParams) { Object.entries(queryParams).forEach(([key, value]) => { url.searchParams.append(key, value.toString()); }); } const response = await fetch(url.toString(), { method: "GET", headers: { "Content-Type": "application/json", }, }); return this.handleResponse(response); } public async post>( endpoint: string, body: TData, ): Promise { const response = await fetch(`${this.baseUrl}${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }); return this.handleResponse(response); } } export const apiClient = new ApiClient(API_URL); ``` ## Полезные ссылки[​](#see-also "Прямая ссылка на этот заголовок") * [(GitHub) Пример проекта](https://github.com/ruslan4432013/fsd-react-query-example) * [(CodeSandbox) Пример проекта](https://codesandbox.io/p/github/ruslan4432013/fsd-react-query-example/main) * [О фабрике запросов](https://tkdodo.eu/blog/the-query-options-api) --- # Использование с SvelteKit В SvelteKit проекте возможно реализовать FSD, однако возникают конфликты из-за различий между требованиями к структуре проекта SvelteKit и принципами FSD: * Изначально, SvelteKit предлагает файловую структуру внутри папки `src/routes`, в то время как в FSD роутинг должен быть частью слоя `app`. * SvelteKit предлагает складывать всё, что не относится к роутингу в папку `src/lib`. ## Настроим конфиг[​](#настроим-конфиг "Прямая ссылка на этот заголовок") svelte.config.ts ``` import adapter from '@sveltejs/adapter-auto'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config}*/ const config = { preprocess: [vitePreprocess()], kit: { adapter: adapter(), files: { routes: 'src/app/routes', // перемещаем роутинг внутрь app слоя lib: 'src', appTemplate: 'src/app/index.html', // Перемещаем входную точку приложения внутрь слоя app assets: 'public' }, alias: { '@/*': 'src/*' // Создаём алиас для директории src } } }; export default config; ``` ## Перемещение файлового роутинга в `src/app`[​](#перемещение-файлового-роутинга-в-srcapp "Прямая ссылка на этот заголовок") Создадим слой app, переместим в него входную точку приложения `index.html` и создадим папку routes. Таким образом, ваша файловая структура должна выглядеть так: ``` ├── src │ ├── app │ │ ├── index.html │ │ ├── routes │ ├── pages # Папка pages, закреплённая за FSD ``` Теперь, вы можете создавать роуты для страниц внутри `app` и подключать к ним страницы из `pages`. Например, чтобы добавить главную страницу в проект, вам нужно сделать следующие шаги: * Добавить слайс страницы внутри слоя `pages` * Добавить соответствующий роут в папку `routes` из слоя `app` * Совместить страницу из слайса с роутом Для того чтобы создать слайс страницы, воспользуемся [CLI](https://github.com/feature-sliced/cli): ``` fsd pages home ``` Создайте файл `home-page.svelte` внутри сегмента ui, откройте к нему доступ с помощью Public API src/pages/home/index.ts ``` export { default as HomePage } from './ui/home-page.svelte'; ``` Создайте роут для этой страницы внутри слоя `app`: ``` ├── src │ ├── app │ │ ├── routes │ │ │ ├── +page.svelte │ │ ├── index.html │ ├── pages │ │ ├── home │ │ │ ├── ui │ │ │ │ ├── home-page.svelte │ │ │ ├── index.ts ``` Добавьте внутрь `+page.svelte` файла компонент вашей страницы: src/app/routes/+page.svelte ``` ``` ## См. также[​](#см-также "Прямая ссылка на этот заголовок") * [Документация по изменению конфига директорий в SvelteKit](https://kit.svelte.dev/docs/configuration#files) --- # Docs for LLMs This page provides links and guidance for LLM crawlers. * Spec: ### Files[​](#files "Прямая ссылка на этот заголовок") * [llms.txt](/documentation/ru/llms.txt) * [llms-full.txt](/documentation/ru/llms-full.txt) ### Notes[​](#notes "Прямая ссылка на этот заголовок") * Files are served from the site root, regardless of the current page path. * In deployments with a non-root base URL (e.g., `/documentation/`), the links above are automatically prefixed. --- # Слои Слои - это первый уровень организационной иерархии в Feature-Sliced Design. Их цель - разделить код на основе того, сколько ответственности ему требуется и от скольких других модулей в приложении он зависит. Каждый слой несет особое семантическое значение, чтобы помочь вам определить, сколько ответственности следует выделить вашему коду. Всего существует **7 слоев**, расположенных от наибольшей ответственности и зависимости к наименьшей: ![Дерево файловой системы с одной корневой папкой под названием src и семью подпапками: app, processes, pages, widgets, features, entities, shared. Папка processes слегка выцвечена.](/documentation/ru/img/layers/folders-graphic-light.svg#light-mode-only) ![Дерево файловой системы с одной корневой папкой под названием src и семью подпапками: app, processes, pages, widgets, features, entities, shared. Папка processes слегка выцвечена.](/documentation/ru/img/layers/folders-graphic-dark.svg#dark-mode-only) 1. App (Эпп) 2. Processes (Процессы, устаревший слой) 3. Pages (Страницы) 4. Widgets (Виджеты) 5. Features (Фичи/функции) 6. Entities (Сущности) 7. Shared (Шэред) Вы не обязаны использовать все слои в своем проекте - добавляйте только те, что приносят пользу вашему проекту. Как правило, в большинстве фронтенд-проектов будут как минимум слои Shared, Pages и App. На практике слои представляют собой папки с названиями в нижнем регистре (например, `📁 shared`, `📁 pages`, `📁 app`). Добавление новых слоев *не рекомендуется*, так как их семантика стандартизирована. ## Правило импорта слоев[​](#правило-импорта-слоев "Прямая ссылка на этот заголовок") Слои состоят из *слайсов* — высокосвязанных групп модулей. Зависимости между слайсами регулируются **правилом импорта слоев**: > *Модуль (файл) в слайсе может импортировать другие слайсы только если они находятся на слоях строго ниже.* Например, папка `📁 ~/features/aaa` является слайсом с именем "aaa". Файл внутри нее, `~/features/aaa/api/request.ts`, не может импортировать код из любого файла в `📁 ~/features/bbb`, но может импортировать код из `📁 ~/entities` и `📁 ~/shared`, а также любой код из `📁 ~/features/aaa`, например, `~/features/aaa/lib/cache.ts`. Слои App и Shared являются **исключениями** из этого правила — они одновременно являются и слоем, и слайсом. Слайсы разделяют код по бизнес-доменам, и эти два слоя являются исключениями, потому что в Shared нет бизнес-доменов, а App объединяет все бизнес-домены. На практике это означает, что слои App и Shared состоят из сегментов, и сегменты могут свободно импортировать друг друга. ## Определения слоёв[​](#определения-слоёв "Прямая ссылка на этот заголовок") В этом разделе описывается семантическое значение каждого слоя, чтобы создать интуитивное представление о том, какой код в нём будет лежать. ### Shared[​](#shared "Прямая ссылка на этот заголовок") Этот слой формирует фундамент для остальной части приложения. Это место для создания связей с внешним миром, например, бэкенды, сторонние библиотеки, среда выполнения приложения (environment). Также это место для ваших собственных библиотек, сконцентрированных на конкретной задаче. Этот слой, как и слой App, *не содержит слайсов*. Слайсы предназначены для разделения слоя на предметные области, но предметные области не существуют в Shared. Это означает, что все файлы в Shared могут ссылаться и импортировать друг друга. Вот сегменты, которые вы обычно можете найти в этом слое: * `📁 api` — API-клиент и, возможно, функции для выполнения запросов к конкретным эндпоинтам бэкенда. * `📁 ui` — UI-кит приложения.
Компоненты на этом слое не должны содержать бизнес-логику, но могут быть тематически связаны с бизнесом. Например, здесь можно разместить логотип компании и макет страницы. Компоненты с UI-логикой также допустимы (например, автозаполнение или строка поиска). * `📁 lib` — коллекция внутренних библиотек.
Эта папка не должна рассматриваться как хелперы или утилиты ([прочитайте здесь, почему эти папки часто превращаются в свалку](https://sergeysova.com/ru/why-utils-and-helpers-is-a-dump/)). Вместо этого каждая библиотека в этой папке должна иметь одну область фокуса, например, даты, цвета, манипуляции с текстом и т.д. Эта область фокуса должна быть задокументирована в файле README. Разработчики в вашей команде должны знать, что можно и что нельзя добавлять в эти библиотеки. * `📁 config` — переменные окружения, глобальные фиче-флаги и другая глобальная конфигурация для вашего приложения. * `📁 routes` — константы маршрутов или шаблоны для сопоставления маршрутов. * `📁 i18n` — код, настраивающий систему переводов, а также глобальные строки перевода. Вы можете добавлять ещё сегментов, но убедитесь, что название этих сегментов описывает цель содержимого, а не его суть. Например, `components`, `hooks` и `types` — плохие имена сегментов, поскольку они не очень полезны при поиске кода. ### Entities[​](#entities "Прямая ссылка на этот заголовок") Слайсы на этом слое представляют концепции из реального мира, с которыми работает проект. Обычно это термины, которые бизнес использует для описания продукта. Например, социальная сеть может работать с бизнес-сущностями, такими как Пользователь, Публикация и Группа. Слайс сущности может содержать хранилище данных (`📁 model`), схемы валидации данных (`📁 model`), функции запросов API, связанные с сущностями (`📁 api`), а также визуальное представление этой сущности в интерфейсе (`📁 ui`). Это визуальное представление не обязательно должно создавать полный блок пользовательского интерфейса — оно в первую очередь предназначено для переиспользования одного и того же внешнего вида на нескольких страницах приложения, и к нему может быть присоединена различная бизнес-логика через пропы или слоты. #### Связи между сущностями[​](#связи-между-сущностями "Прямая ссылка на этот заголовок") Сущности в FSD являются слайсами, и по умолчанию слайсы не могут знать друг о друге. Однако в реальной жизни сущности часто взаимодействуют друг с другом, и иногда одна сущность владеет или содержит другие сущности. Из-за этого бизнес-логику этих взаимодействий лучше всего хранить на более высоких уровнях, таких как Features или Pages. Когда объект данных одной сущности содержит другие объекты данных, обычно хорошей идеей является сделать связь между сущностями явной и обойти изоляцию слайсов, создав API для кросс-ссылок через `@x`-нотацию. Причина в том, что связанные сущности должны рефакториться вместе, поэтому лучше сделать так, чтоб связь было невозможно не заметить. Например: entities/artist/model/artist.ts ``` import type { Song } from "entities/song/@x/artist"; export interface Artist { name: string; songs: Array; } ``` entities/song/@x/artist.ts ``` export type { Song } from "../model/song.ts"; ``` Вы можете узнать больше о `@x`-нотации в разделе [Публичный API для кросс-импортов](/documentation/ru/docs/reference/public-api.md#public-api-for-cross-imports). ### Features[​](#features "Прямая ссылка на этот заголовок") Этот слой предназначен для основных взаимодействий в вашем приложении, действий, которые важны вашим пользователям. Эти взаимодействия часто затрагивают бизнес-сущности, поскольку сущности — это то, о чём ваше приложение. Важный принцип эффективного использования слоя Features: **не все должно быть фичей**. Хорошим показателем того, что что-то должно быть фичей, является тот факт, что оно переиспользуется. Например, если в приложении есть несколько редакторов, и у всех них есть комментарии, то комментарии являются переиспользуемой фичей. Помните, что слайсы — это механизм для быстрого поиска кода, и если фич слишком много, важные фичи теряются. В идеале, когда вы приходите в новый проект, вы узнаёте о его функциональности, просматривая страницы и фичи. Принимая решение о том, что должно быть функцией, оптимизируйте опыт новичка в проекте, который/ая хочет быстро обнаружить большие важные области кода. Слайс фичи может содержать UI для выполнения действия, например, форму (`📁 ui`), вызовы API, необходимые для выполнения действия (`📁 api`), валидацию и внутреннее состояние (`📁 model`), фиче-флаги (`📁 config`). ### Widgets[​](#widgets "Прямая ссылка на этот заголовок") Слой Widgets предназначен для больших самодостаточных блоков интерфейса. Виджеты наиболее полезны, когда они используются на нескольких страницах или когда страница, к которой они принадлежат, имеет несколько больших независимых блоков, и это один из них. Если блок интерфейса составляет бо́льшую часть интересного контента на странице и никогда не используется повторно, он **не должен быть виджетом**, и вместо этого его следует разместить непосредственно на этой странице. подсказка Если вы используете систему вложенного роутинга (например, роутер [Remix](https://remix.run)), может быть полезно использовать слой Widgets так же, как плоская система роутинга использует слой Pages — для создания полных блоков роутинга, включая связанные запросы данных, состояния загрузки и границы ошибок. Таким же образом, вы можете хранить лейауты страниц на этом слое. ### Pages[​](#pages "Прямая ссылка на этот заголовок") Страницы — это то, из чего состоят веб-сайты и приложения (также называются "экраны" или "активности"). Одна страница обычно соответствует одному слайсу, однако, если есть несколько очень похожих страниц, их можно сгруппировать в один слайс, например, формы регистрации и входа. Нет никаких ограничений на количество кода, которое можно разместить в слайсе страницы, по крайней мере, до тех пор, пока вашей команде не станет сложно ориентироваться в ней. Если блок интерфейса со страницы не переиспользуется на других страницах, вполне допустимо оставить его внутри слайса страницы. В слайсе страницы вы обычно найдете интерфейс страницы, а также состояния загрузки и границы ошибок (`📁 ui`). Также вы можете найти там запросы на получение и изменение данных (`📁 api`). Обычно у страницы нет выделенной модели данных, и небольшие части состояния могут храниться в самих компонентах. ### Processes[​](#processes "Прямая ссылка на этот заголовок") предупреждение Этот слой считается устаревшим. Текущая версия спецификации рекомендует избегать его и перемещать его содержимое в `features` и `app`. Выход из ситуаций, когда требуется сложное многостраничное взаимодействие. Этот уровень намеренно оставлен не очень определенным. Большинству приложений этот слой не пригодится, логику на уровне роутера и сервера следует оставить на уровне App. Используйте этот слой только тогда, когда слой App вырастет настолько, что станет неподдерживаемым и потребует разгрузки. ### App[​](#app "Прямая ссылка на этот заголовок") Всё, что касается приложения целиком, как в техническом смысле (например, провайдеры контекста), так и в бизнес-смысле (например, аналитика). Этот слой обычно не содержит слайсов, как и Shared, и вместо этого внутри него сразу находятся сегменты. Вот сегменты, которые вы обычно можете найти в этом слое: * `📁 routes` — конфигурация роутера * `📁 store` — глобальная конфигурация хранилища * `📁 styles` — глобальные стили * `📁 entrypoint` — точка входа в код приложения, специфичная для вашего фреймворка --- # Публичный API Публичный API — это *контракт* между группой модулей, например, слайсом, и кодом, который его использует. Он также действует как ворота, разрешая доступ только к определенным объектам и только через этот публичный API. На практике это обычно реализуется как индексный файл с реэкспортами: pages/auth/index.js ``` export { LoginPage } from "./ui/LoginPage"; export { RegisterPage } from "./ui/RegisterPage"; ``` ## Что делает публичный API хорошим?[​](#что-делает-публичный-api-хорошим "Прямая ссылка на этот заголовок") Хороший публичный API делает использование и интеграцию слайса в другой код удобным и надежным. Этого можно достичь, поставив три цели: 1. Остальная часть приложения должна быть защищена от структурных изменений в слайсе, таких как рефакторинг. 2. Значительные изменения в поведении слайса, которые нарушают предыдущие ожидания, должны вызывать изменения в публичном API. 3. Только необходимые части слайса должны быть доступны. Последняя цель имеет важные практические последствия. Может возникнуть соблазн создать слепые реэкспорты всего, особенно на ранних этапах разработки слайса, потому что любые новые объекты, которые вы экспортируете из своих файлов, также автоматически экспортируются из слайса: Плохая практика, features/comments/index.js ``` // ❌ НИЖЕ ПЛОХОЙ КОД, НЕ ДЕЛАЙТЕ ТАК export * from "./ui/Comment"; // 👎 не пытайтесь повторить дома export * from "./model/comments"; // 💩 это плохая практика ``` Это ухудшает понимаемость слайса беглым взглядом, потому что вы не можете легко определить, каков интерфейс этого слайса. Не зная интерфейс, вам придется глубоко погружаться в код слайса, чтобы понять, как его интегрировать. Еще одна проблема заключается в том, что вы можете случайно раскрыть внутренние модули, что усложнит рефакторинг, если кто-то начнет от них зависеть. ## Публичный API для кросс-импортов[​](#public-api-for-cross-imports "Прямая ссылка на этот заголовок") Кросс-импорты — это ситуация, когда один слайс импортирует из другого слайса на том же слое. Обычно это запрещено [правилом импорта для слоёв](/documentation/ru/docs/reference/layers.md#import-rule-on-layers), но часто есть реальные причины, чтоб сделать кросс-импорт. Например, в реальном мире бизнес-сущности часто ссылаются друг на друга, и лучше отразить эти отношения в коде, а не пытаться избавиться от них. Для этой цели существует особый вид публичного API, также известный как `@x`-нотация. Если у вас есть сущности A и B, и сущность B должна импортировать из сущности A, то сущность A может объявить отдельный публичный API только для сущности B. * `📂 entities` * `📂 A` * `📂 @x` * `📄 B.ts` — специальный публичный API только для кода внутри `entities/B/` * `📄 index.ts` — обычный публичный API Затем код внутри `entities/B/` может импортировать из `entities/A/@x/B`: ``` import type { EntityA } from "entities/A/@x/B"; ``` Нотацию `A/@x/B` следует читать как "пересечение A и B". примечание Старайтесь свести кросс-импорты к минимуму и **используйте эту нотацию только на уровне Entities**, где устранение кросс-импортов часто неразумно. ## Проблемы с индексными файлами[​](#проблемы-с-индексными-файлами "Прямая ссылка на этот заголовок") Индексные файлы, такие как `index.js`, также известные как barrel-файлы (файлы-бочки), являются самым распространенным способом определения публичного API. Их легко создать, но они иногда вызывают проблемы с некоторыми сборщиками и фреймворками. ### Циклические импорты[​](#циклические-импорты "Прямая ссылка на этот заголовок") Циклический импорт — это когда два или более файла импортируют друг друга по кругу. ![Три файла, импортирующие друг друга по кругу](/documentation/ru/img/circular-import-light.svg#light-mode-only)![Три файла, импортирующие друг друга по кругу](/documentation/ru/img/circular-import-dark.svg#dark-mode-only) На изображении выше: три файла, `fileA.js`, `fileB.js` и `fileC.js`, импортирующие друг друга по кругу. Эти ситуации часто трудно обрабатывать сборщикам, и в некоторых случаях они могут даже привести к ошибкам во время выполнения кода, которые может быть трудно отладить. Циклические импорты могут возникать и без индексных файлов, но наличие индексного файла создает явную возможность случайно создать циклический импорт. Это часто происходит, когда у вас есть два объекта, доступных в публичном API слайса, например, `HomePage` и `loadUserStatistics`, и `HomePage` нужно получить доступ к `loadUserStatistics`, но он делает это следующим образом: pages/home/ui/HomePage.jsx ``` import { loadUserStatistics } from "../"; // импортируем из pages/home/index.js export function HomePage() { /* … */ } ``` pages/home/index.js ``` export { HomePage } from "./ui/HomePage"; export { loadUserStatistics } from "./api/loadUserStatistics"; ``` Эта ситуация создает циклический импорт, потому что `index.js` импортирует `ui/HomePage.jsx`, но `ui/HomePage.jsx` тоже импортирует `index.js`. Чтобы предотвратить эту проблему, воспользуйтесь этими принципами. Если у вас есть два файла, и один импортирует из другого: * Если они находятся в одном слайсе, всегда используйте *относительные* импорты и пишите полный путь импорта * Если они находятся в разных слайсах, всегда используйте *абсолютные* импорты, например, через алиас ### Большие бандлы и неработающий tree-shaking в Shared[​](#large-bundles "Прямая ссылка на этот заголовок") Некоторым сборщикам может быть трудно выполнять tree-shaking (удаление неимпортированного кода), когда у вас есть индексный файл, который реэкспортирует все. Обычно это не проблема для публичных API, потому что содержимое модуля обычно довольно тесно связано, поэтому вам редко нужно импортировать одну вещь, но удалить другую. Однако есть два очень распространенных случая, когда обычные правила публичного API в FSD могут привести к проблемам — `shared/ui` и `shared/lib`. Эти две папки являются коллекциями несвязанных вещей, которые часто не нужны все в одном месте. Например, в `shared/ui` могут быть модули для каждого компонента в библиотеке UI: * `📂 shared/ui/` * `📁 button` * `📁 text-field` * `📁 carousel` * `📁 accordion` Эта проблема усугубляется, когда один из этих модулей имеет тяжелую зависимость, такую как подсветка синтаксиса или библиотека drag'n'drop. Вы не хотите подтягивать их на каждую страницу, которая использует что-то из `shared/ui`, например, кнопку. Если ваши бандлы нежелательно растут из-за единого публичного API в `shared/ui` или `shared/lib`, рекомендуется вместо этого иметь отдельный индексный файл для каждого компонента или библиотеки: * `📂 shared/ui/` * `📂 button` * `📄 index.js` * `📂 text-field` * `📄 index.js` Тогда потребители этих компонентов могут импортировать их напрямую, как показано ниже: pages/sign-in/ui/SignInPage.jsx ``` import { Button } from '@/shared/ui/button'; import { TextField } from '@/shared/ui/text-field'; ``` ### Нет реальной защиты от обхода публичного API[​](#нет-реальной-защиты-от-обхода-публичного-api "Прямая ссылка на этот заголовок") Когда вы создаете индексный файл для слайса, ничто не мешает другим не использовать его и импортировать напрямую. Это особенно заметно с автоимпортами, потому что существует несколько мест, откуда объект может быть импортирован, поэтому IDE должна решить за вас. Иногда она может выбрать прямой импорт, нарушая правило публичного API для слайсов. Чтобы автоматически выявлять эти проблемы, мы рекомендуем использовать [Steiger](https://github.com/feature-sliced/steiger), архитектурный линтер с набором правил для Feature-Sliced Design. ### Худшая производительность сборщиков на больших проектах[​](#худшая-производительность-сборщиков-на-больших-проектах "Прямая ссылка на этот заголовок") Наличие большого количества индексных файлов в проекте может замедлить работу сервера разработки, как отметил TkDodo в [своей статье "Please Stop Using Barrel Files"](https://tkdodo.eu/blog/please-stop-using-barrel-files). Есть несколько вещей, которые вы можете сделать, чтобы справиться с этой проблемой: 1. То же самое, что и в разделе "Большие бандлы и неработающий tree-shaking в Shared" — создайте отдельные индексные файлы для каждого компонента/библиотеки в shared/ui и shared/lib вместо одного большого 2. Избегайте наличия индексных файлов в сегментах на слоях, которые имеют слайсы.
Например, если у вас есть индекс для фичи "comments", `📄 features/comments/index.js`, нет смысла иметь еще один индекс для `ui` сегмента этой фичи, `📄 features/comments/ui/index.js`. 3. Если у вас очень большой проект, есть большая вероятность, что ваше приложение можно разделить на несколько больших кусков.
Например, у Google Docs очень разные обязанности для редактора документов и для файлового браузера. Вы можете создать монорепозиторий, где каждый пакет является отдельным корнем FSD со своим набором слоев. Некоторые пакеты могут иметь только слои Shared и Entities, другие могут иметь только Pages и App, а некоторые могут включать свой небольшой Shared, но при этом использовать большой Shared из другого пакета. --- # Слайсы и сегменты ## Слайсы[​](#слайсы "Прямая ссылка на этот заголовок") Слайсы — это второй уровень в организационной иерархии Feature-Sliced Design. Их основное назначение — группировать код по его значению для продукта, бизнеса или просто приложения. Имена слайсов не стандартизированы, поскольку они напрямую определяются бизнес-областью вашего приложения. Например, фотогалерея может иметь слайсы `photo`, `effects`, `gallery-page`. Для социальной сети потребуются другие слайсы, например, `post`, `comments`, `news-feed`. Слои Shared и App не содержат слайсов. Это потому, что Shared не должен содержать никакой бизнес-логики, следовательно, не имеет продуктового значения, а App должен содержать только код, касающийся всего приложения, поэтому разделение не требуется. ### Нулевая сцепленность и высокая связность[​](#zero-coupling-high-cohesion "Прямая ссылка на этот заголовок") Слайсы задуманы как независимые и сильно связанные группы файлов кода. Картинка ниже может помочь визуализировать такие сложные концепции как *связность* (cohesion) и *сцепленность* (coupling): ![](/documentation/ru/img/coupling-cohesion-light.svg#light-mode-only)![](/documentation/ru/img/coupling-cohesion-dark.svg#dark-mode-only) Картинка вдохновлена Идеальный слайс независим от других слайсов на своем уровне (нулевая сцепленность) и содержит бо́льшую часть кода, связанного с его основной целью (высокая связность). Независимость слайсов обеспечивается [правилом импорта для слоёв](/documentation/ru/docs/reference/layers.md#import-rule-on-layers): > *Модуль (файл) в слайсе может импортировать другие слайсы только если они находятся на слоях строго ниже.* ### Правило публичного API для слайсов[​](#правило-публичного-api-для-слайсов "Прямая ссылка на этот заголовок") Внутри слайса код может быть организован как угодно, и это не создаст никаких проблем до тех пор, пока слайс имеет качественный публичный API. В этом суть правила **публичного API для слайсов**: > *Каждый слайс (и сегмент на слоях, не имеющих слайсов) должен содержать определение публичного API.* > > *Модули вне этого слайса/сегмента могут ссылаться только на публичный API, а не на внутреннюю файловую структуру этого слайса/сегмента.* Подробнее о причинах требования публичных API и лучших практиках их создания читайте в [справочнике о публичных API](/documentation/ru/docs/reference/public-api.md). ### Группы слайсов[​](#группы-слайсов "Прямая ссылка на этот заголовок") Тесно связанные слайсы могут быть структурно сгруппированы в папку, но они должны соблюдать те же правила изоляции, что и другие слайсы — **никакого совместного использования кода** в этой папке быть не должно. ![Функции \"compose\", \"like\" и \"delete\" сгруппированы в папку \"post\". В этой папке также есть файл \"some-shared-code.ts\", который зачеркнут, чтобы показать, что это запрещено.](/documentation/ru/assets/images/graphic-nested-slices-b9c44e6cc55ecdbf3e50bf40a61e5a27.svg) ## Сегменты[​](#сегменты "Прямая ссылка на этот заголовок") Сегменты — это третий и последний уровень в организационной иерархии, их цель — группировать код по его техническому назначению. Существует несколько стандартизированных названий сегментов: * `ui` — все, что связано с отображением UI: UI-компоненты, форматтеры дат, стили и т.д. * `api` — взаимодействие с бэкендом: функции запросов, типы данных, мапперы и т.д. * `model` — модель данных: схемы, интерфейсы, хранилища и бизнес-логика. * `lib` — библиотечный код, необходимый другим модулям в этом слайсе. * `config` — конфигурационные файлы и фиче-флаги. На [странице про Слои](/documentation/ru/docs/reference/layers.md#layer-definitions) есть примеры того, как каждый из этих сегментов может использоваться на разных слоях. Вы также можете создавать свои собственные сегменты. Наиболее распространенные места для кастомных сегментов — это слои App и Shared, где слайсы не имеют смысла. Убедитесь, что название этих сегментов описывает, для чего нужно его содержимое, а не чем оно является. Например, `components`, `hooks` и `types` — плохие названия сегментов, потому что они не так полезны, когда вы ищете код. --- ### Явная бизнес-логика Архитектуру легко осваивать, поскольку она состоит из доменных модулей ---