Local-First Frontend: архитектура для быстрой и гибкой разработки
Дисклеймер
Хочу сразу отметить, что эта статья не является призывом к обязательному использованию предложенной архитектуры. Моя цель — поделиться своими наработками, получить конструктивную критику и обсудить возможные улучшения. Буду рад, если мой опыт окажется полезным или вдохновит вас на собственные решения!
Введение
Привет, Хабр! Сегодня я хочу рассказать о своей архитектуре, которую я разработал в процессе проектирования своих фронтенд-приложений. На первый взгляд, она может напомнить популярную (и неоднозначно воспринимаемую) методологию 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-приключениях!