Быстрый старт
В этом разделе мы рассмотрим все основные паттерны ED.
Этой статьи будет достаточно, чтобы уже получить пользу от использования этого паттерна.
Дальнейшие статьи в разделе гайды будут раскрывать отдельные аспекты работы с ED.
Какие проекты можно делать на ED
ED предназначен для работы на основных web фреймворках: vue, react, svelte, solid.
В простых модификациях он будет полезен даже в самых базовых проектах.
Единственное, для разработки библиотек ED не предназначен. Но вы всегда можете взять часть паттернов, если они вам будут полезны.
Модификации ED
Архитектура представлена в 4-х модификациях
- ED small - для маленьких проектов (до 12 человеко/месяцев разработки)
- ED medium - для средних проектов (команда до 5-6 человек фронтов)
- ED enterprise - для крупных проектов (больше одной команды)
- ED monorepo - для команды, которая работает над несколькими проектами
В текущем гайде будут описаны первые 2 модификации. 3-4 в будущем будут описаны в других статьях
Из чего состоит ED.
Для внешнего наблюдателя это просто папочки и файлики. На самом же деле это абстракции и архитектурные границы.
Подробнее об этом мы поговорим далее. Но пока вам никто не мешает об этом думать, как о папочках и файликах
Первое, с чего начинается ED, это с выделения слоёв.
Выделение основных слоёв
Как и в большей части архитектур, в ED на верхнем уровне код организован по слоям.
Всего есть 4 слоя верхнего уровня.
Директорияsrc/
Директорияapp/
- …
Директорияfeatures/
- …
Директорияservices/
- …
Директорияshared/
- …
Отличия от FSD
- В ED
sharedв отличие от fsdsharedможет содержать бизнес-логику. servicesочень похожи наentities, но тут нет ограничений на семантику “объекта из бизнеса”featuresэто крупные куски функциональности приложения. По сути это слоиpageswidgetsfeaturesentitiesв одном.- Отдельного слоя
pagesнет. Код страниц находится вfeatures. Если страницы содержат несколько фич, то композиция находится вapp - Слайсы у нас называются модулями, а сегменты группами. А ещё у нас разрешены подмодули.
App
Слой точка входа. Здесь происходит запуск проекта, глобальная конфигурация, связь фич в единое приложение
Внутренняя структура app не стандартизирована и сильно зависит от приложения.
Слой, в котором должна лежать самая часто меняющаяся логика
Примеры кода, который тут может быть:
app.tsxкорневой компонент приложенияroot-layout.tsxобщий лейаут всего приложенияroot-header.tsxзаголовок всего приложения. Может делегировать задачи компонентам изfeaturesglobal.cssглобальные стилиrouter.tsxинициализацияreact-routerproviders.tsxкомпонент, который использует глобальные react провайдеры
export function Providers({ children }: { children: React.ReactNode }) { return ( <StoreProvider store={store}> <QueryProvider client={queryClient}>{children}</QueryProvider> </StoreProvider> );}- В
next.jsapp остаётся самим собой, только старайтесь большую часть логики уносить вfeatures
Features
Основной слой. Здесь должна находиться большая часть кода приложения.
Каждая папка слоя features это реализация крупного независимого куска функциональности
Пример features для планировщика задач
Директорияsrc/
Директорияapp/
- …
Директорияfeatures/
Директорияauth/
- …
Директорияtask-list/
- …
Директорияsidebar/
- …
Директорияsettings/
- …
Ограничение взаимодействия с app
Это крайне важное ограничение даёт нам следующее:
- В
appнаходятся часто меняющиеся штуки. Зависимость на часто меняющийся код делает фичи ненадёжными sdp - Импорт из app почти всегда - циклическая зависимость. Циклические зависимости всегда плохо, и для компилятора, и для мозга. adp
Структура фичи
Каждая фича по своей сути это модуль!
Так как модули в ED бывают большие их структура подробно описана ниже.
Сейчас важно понимать, что в фиче может быть всё: И компоненты отображения, и логика, и инфраструктура, и страница.
Главный критерий, все эти вещи должны иметь высокую смысловую связность
Взаимодействие фич
Тут есть различие между “маленькой” модификацией и “средней”
Средняя модификация
Директорияsrc/
Директорияapp/
- …
Директорияfeatures/
Директорияauth/
- …
Директорияtask-list/ // из task-list нельзя импортировать auth
- …
Это ограничение очень важно в средних и крупных проектах, чтобы можно было рассматривать фичи как независимые блоки.
- Таким образом сильно возрастает понятность. Так как можно рассматривать фичу в отрыве от других
- Увеличивается надёжность. Так как изменение в одной фиче с меньшей вероятностью сломает другую
- Защита от циклических зависимостей.
В реальности, полностью изолированные фичи бывают редко. Поэтому для взаимодействия фич, с запретом на прямой импорт используются паттерны слабой связанности или по-другому dependency inversion (Инверсия зависимостей)
Звучит сложно, но по факту это: связь через общий стейт, слоты, рендер-пропсы, события, контекст, и dependency injection
Подробнее о DI
Это самая сложная часть ED. По этой причине в “маленькой” модификации это ограничение накладывать нецелесообразно
Маленькая модификация
В маленькой модификации прямой импорт разрешён. Но это не отменяет, что этих импортов должно быть как можно меньше!
Чем меньше связей между фичами будет, тем лучше. В идеале их вообще не должно быть
Services
Слой переиспользуемых бизнес модулей. Могут хранить не только логику, но и представление.
Как и фича, каждый сервис это самодостаточный модуль. Но в отличие от фичи он не реализует функционал приложения, а помогает фичам выполнять свою работу.
Чаще всего модули в services нужны, если есть большое количество переиспользуемой между фичами логики.
Пример services для планировщика задач
Директорияsrc/
Директорияapp/
- …
Директорияfeatures/
Директорияauth/
- …
Директорияtask-list/
- …
Директорияmanage-settings/
- …
Директорияservices/
Директорияsession/
- …
Директорияsettings/
- …
В данном случае в session скорее всего находится хранилище сессии, которое используется в большей части других фич.
А в settings хранятся настройки, которые редактируются в фиче manage-settings и используются в task-list
Ограничение взаимодействия с app и features
Как фичи не могут импортировать app, так и сервисы не могут импортировать features и app
Сделано это по тем же самым причинам. Но для сервисов это ещё важнее. Они чаще всего переиспользуются в нескольких местах.
Зависимость на более часто меняющиеся фичи сделала бы сервисы неустойчивыми, и подвергла бы опасности сразу несколько других фич.
Взаимодействие сервисов
Тут нет однозначного решения. Базово я разрешаю взаимодействие сервисов друг с другом.
Так как чаще всего dependency inversion на таком уровне вызывает очень много сложностей.
Но если появляются проблемы, вводится такое же ограничение как и для фич.
Shared
Слой ядро приложения. Здесь расположены вещи, которые используются в приложении повсеместно
Старайтесь располагать в shared только следующее:
- Глобальные бизнес типы. Типы редко меняются и не вызывают багов
- Глобальную инфраструктуру. Инстансы сторов, нотификации, интернационализацию, тему Обычно такая инфраструктура используется широко, а меняется редко
- Глобальные константы, связывающие приложение. Например, константы роутинга и результат чтения .env
Важные исключения:
По своей сути uikit и api не должны находиться в shared
Это крайне часто меняющиеся модули. Но все попытки унести их из shared не удались.
Поэтому знайте, что uikit и api вечные источники багов. И относитесь к ним соответственно
Структура shared жёстко не стандартизирована, но всегда похожа
Пример стандартного shared
всё ниже перечисленное опционально и может не понадобиться в вашем случае
Директорияsrc/
Директорияshared/
Директорияdomain/ // глобальные бизнес типы. Нужные для работы инфраструктуры и всего приложения
- ids.ts
- events.ts
- session.ts
- user.ts
Директорияui/ // uikit приложения
Директорияkit/
- button.tsx
Директорияtable/ // переиспользуемый компонент таблицы
- …
Директорияapi/ // обычно автоматически генерируемый api instance
- api-instance.ts // инстанс axios
- generated.ts
Директорияmodel/ // работа с глобальными данными
- routes.ts // константы для роутинга
- config.ts // получение доступа к .env
- store.ts // инстанс redux
Директорияlib/ // Глобальная инфраструктура и хелперы
Директорияnotifications/
- …
Директорияi18n/
- …
Директорияreact/
- use-mutation-observer.ts
- date.ts
domain, model, ui, lib
Весь код shared разделён на стандартные группы:
domain: Самые важные бизнес типы и правила. Эта группа должна быть изолированной и ни на кого не зависетьmodel: Работа с глобальным состояниемui: Глобальные компонентыlib: модули глобальной инфраструктуры
Ограничение взаимодействия с app, features и services
shared это слой, в котором находится корневая логика. Он не может напрямую работать ни с одним другим слоем.
При этом его могут и будут импортировать все другие слои.
Это правило крайне важно и не должно нарушаться. shared - опасный слой. С ним всегда нужно быть на чеку)
Схема слоёв
”Маленькая” модификация

“Средняя” модификация

Структура модуля
features и services - в ED содержат модули. Но по вашему желанию вы можете создавать модули и в app и shared
Также ED поддерживает концепцию sub-modules, таким образом Модули в ED могут быть очень большими.
По сути модули в ED можно рассматривать как мини-приложения, у которых есть своя архитектура
Этапы эволюции модулей
Мы против оверхеда, поэтому у модулей есть этапы эволюции, от простого к сложному.
Всегда при создании модулей начинайте с самых простых этапов, а потом проводите рефакторинг, если это требуется.
По нашему опыту это самый эффективный подход! Излишняя архитектура хуже, чем её недостаток
Этап 1: Single file module
Да-да, самые простые модули могут состоять только из одного файла!
Вот пример фичи todo-list, на самых первых этапах
todo-list, на самых первых этапахimport { useState } from "react";
export function TodoListPage() { const [todos, setTodos] = useState([]); const [input, setInput] = useState("");
const addTodo = (e) => { e.preventDefault(); if (!input.trim()) return; setTodos([...todos, { id: Date.now(), text: input, done: false }]); setInput(""); };
const toggleTodo = (id) => { setTodos( todos.map((todo) => todo.id === id ? { ...todo, done: !todo.done } : todo ) ); };
const deleteTodo = (id) => { setTodos(todos.filter((todo) => todo.id !== id)); };
return ( <div className="max-w-md mx-auto p-4"> <h1 className="text-2xl font-bold mb-4">Todo</h1>
<form onSubmit={addTodo} className="flex mb-4"> <input value={input} onChange={(e) => setInput(e.target.value)} className="flex-1 border p-2 rounded-l" placeholder="Add todo..." /> <button type="submit" className="bg-blue-500 text-white px-4 rounded-r hover:bg-blue-600" > Add </button> </form>
<ul> {todos.map((todo) => ( <li key={todo.id} className="flex items-center mb-2"> <input type="checkbox" checked={todo.done} onChange={() => toggleTodo(todo.id)} className="mr-2" /> <span className={`flex-1 ${ todo.done ? "line-through text-gray-400" : "" }`} > {todo.text} </span> <button onClick={() => deleteTodo(todo.id)} className="text-red-500 hover:text-red-700" > × </button> </li> ))} </ul>
{todos.length > 0 && ( <div className="mt-4 text-sm text-gray-500"> {todos.filter((t) => !t.done).length} items left </div> )} </div> );}Чаще всего модули очень быстро перерастают этот этап. Но иногда нет!
С файлом до 400 строк работать приемлемо.
При этом скорость разработки и рефакторинга такого модуля выше, чем модуля из 20 файлов по 20 строк
Не пренебрегайте этим подходом, особенно на этапе прототипирования.
Этап 2: Flat module
Вот ваш файл стал больше 400 строк, и стало неудобно. Что выделяем components/hooks/ui/model?
Нет ещё рано!
Нет ничего хуже папки, в которой один файл.
Второй этап эволюции, это создание плоской структуры разнородных модулей
Вот пример фичи todo-list на втором этапе
Директорияtodo-list/
- todo-list-page.tsx
- use-todo-list.tsx
- api.tsx
- use-intersection-observer.tsx
- index.ts
Здесь мы просто разделили код на функции. А потом вынесли их в отдельные файлы.
Файлы называем в соответствии с содержимым. Избегаем названия hooks components
Это число выведено эмпирически и связано с концепцией кошелёка миллера
Public api
На этом этапе уже не понятно, какой код предназначен для внешнего использования, а какой должен оставаться внутри.
Поэтому мы добавляем public-api.
Это index.ts файл, в котором вы реэкспортируем (иногда со сменой названия) доступные извне элементы.
export { TodoListPage } from "./todo-list-page";Для тех у кого next.js
Если в index файлах нет сайд эффектов, то в vite tree-shaking работает корректно
С next.js же встречались проблемы. Поэтому там часто используется другой подход с public-api.
Все приватные файлы начинаются с _, а публичные без. Таким образом не обязательно создавать только один public файл
Директорияtodo-list/
- todo-list-page.tsx
- _use-todo-list.tsx
- _api.tsx
- _use-intersection-observer.tsx
Explicit dependencies (экспериментально)
При взаимодействии модулей друг с другом, часто крайне сложно проследить все зависимости модуля от других модулей.
Поэтому все зависимости, которые используются внутри, можно реэкспортировать через специальный deps.ts файл.
Особенно это важно для services и features, если разрешены кросс импорты (из фичи фичу, из сервиса сервис)
Пример:
Директорияfeatures/
Директорияsettings/
- …
Директорияauth/
- …
Директорияtodo-list/
- index.ts
- deps.ts
- todo-list-page.tsx
- use-todo-list.tsx
- create-todo-form.tsx
deps.tsx
export { useSession } from "@/features/auth";export { useSettings } from "@/features/settings";todo-list-page.tsx
import { useSession, useSettings } from "./deps";Sub modules
По сути, когда мы сделали такое разделение, мы получили модуль, который состоит из нескольких однофайловых модулей.
Но они не обязаны быть однофайловыми. Любой дочерний модуль может быть на любом этапе эволюции
Директорияtodo-list/
- index.ts
- todo-list-page.tsx
- use-todo-list.tsx
Директорияcreate-todo-form/
- index.ts
- create-todo-form.tsx
- use-create-todo.tsx
Опасности вложенности
Благодаря подмодулям, на втором уровне можно оставаться очень долго.
Но глубокая вложенность не так хороша, как кажется. Глубокие древовидные структуры сложны для понимания. Намного комфортнее читаются однородные списки (слой features как раз пример однородного списка)
Поэтому чаще всего для преодоления ограничения в ~6 элементов подмодулям лучше предпочитать создание групп
Этап 3: Grouped module
Как было сказано выше, если для разнородных папок комфортно < 6 элементов. То для однородных папок это количество резко возрастает (до 20 элементов. Сильно зависиот от однородности)
Поэтому мы можем разделить подмодули на группы, таким образом сильно увеличив допустимый размер модуля
Что такое группа.
Группа это объединение нескольких модулей на основании общего признака:
Примеры групп:
- components
- hooks
- services
- features
- ui
- model
- lib
- api
Кстати, все слои это тоже группы.😉
Стандартные группы
Существуют достаточно удачные группы, которые хорошо себя показали:
Группа: ui
По сути это объединение всех компонентов, реже хуков, которые отвечают целиком и полностью за отображение. И не несут в себе сложной логики
Примеры:
- todo-list-page.tsx
- todo-card.tsx
- todo-form.tsx
- use-render-arrows.tsx
Группа: model
Группа, в которой лежит основная работа с данными.
Если вы на чистом react, здесь лежат хуки, которые манипулируют данными в отрыве от отображения.
Если у вас стейт менеджер, то здесь будет лежать вся логика работы со стейт менеджером
Примеры:
- use-todo-list.ts
- todo-list.slice.ts
- todo-list-store.ts
- todo-item.ts
Группа: lib
Группа, в которой находится инфраструктурный код. Это код чаще всего предоставляет более удобные обёртки над браузерным api и библиотеками. Или просто упрощает рутинные задачи
Примеры:
- use-mutation-observer.ts
- date.ts
Группа: api
Группа для кода работы с api и типами контрактов.
Группа: domain
Если логика в model становится очень сложной.
То код, описывающий самые важные бизнес процессы:
- Расчёт скидки
- Вычисление отпуска
- Получение прогресса
- Расчёт координат при перемещении элемента по карте
Можно вынести в виде чистых функций в группу domain
Также в domain находятся все типы, над которыми эти чистые функции проводят манипуляции
Примеры:
- map-node.ts
- get-intersections.ts
- compute-next-lesson.ts
Группа: view-model
В некоторых кейсах модуль содержит большое количество логики, которая обрабатывает пользовательский ввод
Обычно это происходит, если реализуется dnd или анимации
В таком случае этот код можно вынести в отдельную группу view-model
Пример:
- use-dnd.tsx
- use-animation.tsx
Вложенные группы
Внутри группы вы можете группировать модули и по другим признакам.
Это работает точно так же, как с подмодулями. Только не забывайте, что излишняя глубина это неудобно
Пример:
Директорияui/
Директорияfields/
- file-field.tsx
- text-field.tsx
- select-field.tsx
- date-field.tsx
- create-user-form.tsx
- update-user-form.tsx
Пример модуля с группами
Директорияtodo-list/
- index.ts
- api.ts
Директорияmodel/
- todo-item.ts
- use-create-todo.ts
- use-update-todo.ts
Директорияui/
Директорияpages/
- todo-list-page.tsx
- todo-details-page.tsx
Директорияfields/
- file-field.tsx
- text-field.tsx
- select-field.tsx
- date-field.tsx
- create-todo-form.tsx
- todo-details-form.tsx
- todo-item.module.css
- todo-item.tsx
Группы и подмодули
Появление групп не отменяет подмодули. Иногда чем разделять код на группы, лучше разбить весь код на несколько подмодулей
Директорияtodo-list/
- index.ts
- api.ts
Директорияui/
- file-field.tsx
- text-field.tsx
- select-field.tsx
- date-field.tsx
Директорияtodo-list/
- index.ts
- todo-list-page.tsx
- create-todo-form.tsx
- use-create-todo.ts
- todo-item.tsx
Директорияtodo-details/
- index.ts
- todo-details-page.tsx
- todo-details-form.tsx
- use-update-todo.ts
Когда переходить на следующий этап?
На этом этапе можно разрабатывать модули любого размера.
Но здесь есть важная проблема - связи между подмодулями хаотичны, и в них может быть очень сложно разобраться
Если вы столкнулись с такой проблемой, вам поможет следующий этап:
Этап 4: Module with compose
Это сложный, но при этом крайне мощный паттерн борьбы с хаотическими зависимостями.
Его ближайщий аналог это DIP принцип из SOLID.
Основная суть этого паттерна, чтобы убрать зависимости между подмодулями в model и ui
Для этого используются инструменты слабой связанности.
(для ui слоты и рендер-пропсы, для model события, DI, или простая связь через параметры)
После того как мы получили набор независимых элементов,
всё это объединяется в специальных компонентах медиаторах
Для них я обычно создаю отдельную группу compose
Связи между компонентами в compose разрешены
Пример:
Директорияtodo-list/
- index.ts
- api.ts
Директорияcompose/
- todo-list-page.tsx
- todo-details-page.tsx
- create-todo-form.tsx
- todo-details-form.tsx
Директорияdomain/
- todo-item.ts
Директорияmodel/
- use-todo-list.ts
- use-delete-todo.ts
- use-create-todo.ts
- use-update-todo.ts
Директорияui/
Директорияfields/
- file-field.tsx
- text-field.tsx
- select-field.tsx
- date-field.tsx
- todo-page-layout.tsx
- todo-item.tsx
- common-fields.tsx
- update-button.tsx
- delete-button.tsx
Тогда todo-list-page.tsx выглядел бы как-то так
export function TodoListPage() { const todoList = useTodoList(); const deleteTodo = useDeleteTodo(todoList); const createTodo = useCreateTodo(todoList); const updateTodo = useUpdateTodo(todoList);
return ( <TodoPageLayout createForm={<CreateTodoForm createTodo={createTodo} />} todos={todoList.list.map((item) => ( <TodoItem key={item.id} item={item}> <UpdateButton onClick={updateTodo.bind(item)} /> <DeleteButton onClick={deleteTodo.bind(item)} /> </TodoItem> ))} /> );}Где можно почитать подробнее
Документация ещё активно разрабатывается. Позже об этом паттерне появится отдельная статья. Сейчас вы можете изучить следующие материаллы:
Ограничения этапа 4
Самые главные здесь ограничения технические. Не все инструменты поддерживают слабую связанность. И не во всех ситуациях, это даёт нужную производительность
Но в большей части ситуаций, такой подход помогает сильно уменьшить сложность модуля!
Выводы по эволюции модулей
Может быть вы уже запутались во всех вариантах модулей здесь представленных.
Это нормально, подобная гибкость чуть усложняет вход.
При более глубоком рассмотрении оказывается, что это всё вариации вокруг трёх понятий: модуль группа и public-api.
На самом деле, этот подход одновременно и стандартизирует подходы. И предоставляет гибкость
По мере усложнения ваших модулей, вы можете строить удобный и поддерживаемый код из кирпичиков модулей и групп
При этом не тратить время на постоянный оверхед от неудобных ритуалов и паттернов.
Что дальше?
Пользуясь советами из этого гайда, вы уже сейчас можете начать использовать ED.
Если хотите понять, а как это всё вообще работает. Приходите читать продвинутую часть документации
Сейчас эта часть только начинает развиваться, но со временем там будет появляться всё больше статей, раскрывающих концепции и паттерны