Дисклеймер
Хочу сразу отметить, что эта статья не является призывом к обязательному использованию предложенной архитектуры. Моя цель — поделиться своими наработками, получить конструктивную критику и обсудить возможные улучшения. Буду рад, если мой опыт окажется полезным или вдохновит вас на собственные решения!
Введение
Привет, Хабр! Сегодня я хочу рассказать о своей архитектуре, которую я разработал в процессе проектирования своих фронтенд-приложений. На первый взгляд, она может напомнить популярную (и неоднозначно воспринимаемую) методологию FSD (Feature-Sliced Design), но это не совсем так. В моем подходе используется обратная логика построения виджетов и компонентов, что делает её в своем роде уникальной.
Эта архитектура была протестирована и создавалась в первую очередь для проектов на Vue 3, однако её легко адаптировать под другие фреймворки или даже нативный JavaScript. Если вы ищете гибкое и масштабируемое решение для своих проектов, возможно, мой опыт будет вам интересен. Давайте разберемся подробнее!
Для лучшего погружения в архитектуру я создал небольшой проект-болванку и залил его на GitHub. После прочтения статьи (или даже во время него) вы сможете изучить код, поэкспериментировать с ним и предложить свои улучшения.
Давайте разбираться подробнее!
Структура архитектуры

Моя архитектура отличается минимализмом и отсутствием избыточных слоев, которые могут замедлять разработку на небольших клиентских приложениях. Вместо этого я использую стандартные, интуитивно понятные слои, логика которых практически не изменена. Это позволяет сохранить простоту и скорость разработки, не жертвуя при этом гибкостью и масштабируемостью.
. ├── public └── src ├── app │ ├── assets │ ├── directives │ ├── layouts │ ├── providers │ │ ├── router │ │ └── stores │ ├── styles │ ├── App.vue │ └── index.ts ├── components ├── pages ├── services │ ├── api │ └── composables ├── types ├── widgets └── main.ts
Слои и их назначение
Слой в данной архитектуре — это директория, которая объединяет модули, связанные общей функциональностью. Например, слой app является сервисным слоем и содержит ключевые элементы, необходимые для корректной работы приложения.
Основными и самыми важными элементами внутри app можно выделить router и store (в нашем случае pinia). Также в сервисный слой стоит выносить то, чем пользуется все приложение, например глобальные стили, кастомные директивы, ассеты и т.д.
Store создается на уровне сервисного слоя и используется по всему проекту, так как нет необходимости дублировать его для отдельных компонентов. Это упрощает управление состоянием и делает код более согласованным.
Правила работы с сервисным слоем:
Импорт из сервисного слоя разрешен из любого места в проекте.
Изменения в сервисном слое допускаются только в исключительных случаях, так как он формируется на ранних этапах разработки и служит основой для всего приложения.
Слой components
Слой components — это место для хранения переиспользуемых компонентов, которые не содержат бизнес-логики. Этот слой идеально подходит для:
UI-китов или оберток над ними: Если вы используете сторонние библиотеки компонентов (например, Vuetify, Element UI), здесь можно создавать кастомные обертки для их адаптации под нужды проекта.
Иконок: Компоненты иконок, которые используются в разных частях приложения.
Простых блоков: Например, модальные окна (
modal), которые являются лишь оберткой для вызова и заполняются логикой в других местах.
Пример структуры слоя components:
└── components ├── icon ├── ui │ ├── button │ └── select └── modal
Компоненты из этого слоя должны быть максимально "глупыми" — они получают данные через пропсы и просто отображают их. Это делает их универсальными и легко переиспользуемыми.
Слой widgets
Слой widgets предназначен для хранения блоков, которые имеют свою логику и используются на страницах. Этот слой идеально подходит для:
Сложных компонентов с бизнес-логикой: Например, таблицы с фильтрами, формы или меню.
Организации кода на страницах: Виджеты помогают разбить страницу на логические блоки, делая код более читаемым и поддерживаемым.
Компонентов, которые могут (но не обязаны) переиспользоваться: Например, фильтры для таблиц могут быть уникальными для одной страницы, но их логика вынесена в виджет для удобства.
Пример структуры слоя widgets:
└── widgets ├── grids ├── forms │ └── filters ├── menu │ ├── header │ └── sidebar └── modals
Как видно из структуры слоя
widgets, модульmodalsиспользует внутри себя компонентmodalиз слояcomponentsв качестве обертки. Это отличный пример того, как виджеты взаимодействуют с компонентами.Компонент
modalиз слояcomponents— это "глупая" обертка, которая отвечает только за отображение модального окна и его базовое поведение (например, открытие, закрытие, анимацию). Вся логика, связанная с конкретным модальным окном, находится внутри виджетаmodals.
Внутри виджетов можно использовать компоненты из слоя components и services для работы с данными.
Слой pages
Слой pages — это модули страниц приложения. Этот слой идеально подходит для:
Организации страниц приложения: Каждая страница представляет собой отдельный модуль, который может включать в себя виджеты, компоненты и бизнес-логику.
Хранения специфичной логики: Логика, которая относится только к одной странице, должна находиться здесь.
Использования виджетов и компонентов: Страницы собираются из блоков, которые находятся в слоях widgets и components.
Пример структуры слоя pages:
└── pages ├── activities ├── reports │ ├── summary-report │ └── risk-report └── workstation
Повторяющаяся логика между страницами выносится в composables, чтобы избежать дублирования кода.
Слой services
Слой services — это место для хранения логики, которая часто используется в приложении, а также для взаимодействия с данными. Этот слой идеально подходит для:
API-запросов: Все запросы к серверу, включая контроллеры, типы и утилиты, должны находиться здесь.
Composables: Переиспользуемые хуки, которые содержат общую логику, например, работу с формами или запросами.
Утилит: Вспомогательные функции, которые используются в разных частях приложения.
Пример структуры слоя services:
└── services ├── api │ ├── controllers │ ├── types │ ├── utils │ └── index.ts └── composables
Этот слой помогает централизовать логику, связанную с данными, и делает её легко переиспользуемой.
Вынесение types
Слой types — это вспомогательный слой для работы с TypeScript. Он идеально подходит для:
Глобальных типов: Типы данных, которые используются в нескольких местах приложения.
Моделей данных: Интерфейсы и типы для данных, которые приходят с сервера или используются в состоянии приложения.
Утилит для TypeScript: Вспомогательные типы, которые упрощают работу с TypeScript (например, Utility Types).
Пример структуры слоя types:
└── types ├── enums ├── models ├── utility └── index.ts
Этот слой помогает избежать дублирования типов и делает код более строго типизированным.
Модули и их структура
Модуль — это логически завершенный блок кода, который разбит на смысловые разделы. Каждый модуль включает следующие элементы:
index.ts— единая точка входа в модуль. Упрощает импорт модуля в других частях приложения.ui— визуальная составляющая компонента, содержащая основную логику. Это "лицо" модуля, которое отвечает за отображение и взаимодействие с пользователем.utils- вспомогательные функции, которые могут быть вынесены в отдельный файл для улучшения читаемости кода. Например, это может быть массив с настройками колонок для таблицы или другие данные, которые занимают много места и мешают быстро ориентироваться в основном коде.views- дочерние модули, которые используются только внутри родительского модуля. Например, если основной модуль представляет собой блок на странице с несколькими табами, то каждый таб может быть реализован как отдельный view.Почему
views, а неwidget?Widget— это самостоятельный модуль, который может использоваться на одной или нескольких страницах. Он имеет свою логику и может быть переиспользован в разных частях приложения.View— это вспомогательный модуль, который существует только в контексте родительского модуля. Он не предназначен для самостоятельного использования, но может эволюционировать вwidget, если потребуется.
Важное правило:
Viewsможет существовать только внутри модуляpageилиwidget. Он не может быть частью компонента или другогоview. Это позволяет сохранить четкую иерархию и избежать путаницы.
some-module ├── ui │ ├── _styles.scss │ └── SomeModule.vue ├── utils │ └── useUtilForSomeModule.ts ├── views │ └── some-module-view └── index.ts
Эволюция
views:При создании
viewsмы предполагаем, что со временем, в результате изменений в бизнес-логике, он может превратиться в самостоятельныйwidget. Другими словами,view— это "птенец", который готовится покинуть "гнездо" родительского модуля и стать полноценным независимым элементом. Это достигается путем переноса папкиviewна уровень выше, где он становится самостоятельным модулем.
Такой подход к созданию модулей позволяет гибко адаптировать архитектуру под changing requirements и масштабировать приложение без необходимости кардинальных переделок.
Визуализация
Проект, с которого были взяты следующие скриншоты располагается на GitHub по ссылке: https://github.com/neluckoff/local-first-frontend.

Основная логика страницы настроек построена на том, что все её разделы разнесены по views. Это связано с тем, что каждый раздел может иметь свою уникальную логику, и если таких разделов много, код страницы быстро становится сложным для восприятия. Чтобы избежать этого, я выношу каждый раздел в отдельный view, ведь для widget этот кусок кода пока маловат.

Модальное (диалоговое) окно — это отличный пример взаимодействия между компонентом и виджетом. В моей архитектуре это работает следующим образом:
Компонент — это базовая обертка для модального окна. Он отвечает за отображение, анимацию, закрытие и другие базовые функции. Внутри компонента используются слоты, которые позволяют заполнить его содержимым.
Виджет — это уже полноценное модальное окно с конкретной логикой. Например, виджет AlertModal может содержать текст сообщения, кнопки "ОК" и "Отмена", а также логику их обработки. Виджет использует компонент Modal как обертку и заполняет его слоты своим содержимым
Заключение
В этой статье я поделился своим подходом к организации Frontend-приложений, который помогает мне сохранять код чистым, поддерживаемым и гибким. Основная идея архитектуры — минимум лишних слоев, максимум переиспользования и четкое разделение ответственности между компонентами, виджетами и страницами.
Конечно, этот подход — не истина в последней инстанции. У каждого проекта свои требования, и то, что работает для меня, может не подойти вам. Но я надеюсь, что мои наработки вдохновят вас на эксперименты или помогут найти новые решения для ваших задач.
Если у вас есть вопросы, замечания или идеи по улучшению архитектуры, буду рад обсудить их в комментариях. Также вы можете изучить пример реализации на GitHub и предложить свои правки. Ведь лучший код — это тот, который постоянно обновляется благодаря обратной связи и совместной работе.
Спасибо за внимание, и удачи в ваших Frontend-приключениях!
