Local-First Frontend: архитектура для быстрой и гибкой разработки

Дисклеймер

Хочу сразу отметить, что эта статья не является призывом к обязательному использованию предложенной архитектуры. Моя цель — поделиться своими наработками, получить конструктивную критику и обсудить возможные улучшения. Буду рад, если мой опыт окажется полезным или вдохновит вас на собственные решения!

Введение

Привет, Хабр! Сегодня я хочу рассказать о своей архитектуре, которую я разработал в процессе проектирования своих фронтенд-приложений. На первый взгляд, она может напомнить популярную (и неоднозначно воспринимаемую) методологию FSD (Feature-Sliced Design), но это не совсем так. В моем подходе используется обратная логика построения виджетов и компонентов, что делает её в своем роде уникальной.

Эта архитектура была протестирована и создавалась в первую очередь для проектов на Vue 3, однако её легко адаптировать под другие фреймворки или даже нативный JavaScript. Если вы ищете гибкое и масштабируемое решение для своих проектов, возможно, мой опыт будет вам интересен. Давайте разберемся подробнее!

Для лучшего погружения в архитектуру я создал небольшой проект-болванку и залил его на GitHub. После прочтения статьи (или даже во время него) вы сможете изучить код, поэкспериментировать с ним и предложить свои улучшения.

Давайте разбираться подробнее!

Структура архитектуры

Пример слоев и модулей в Local-Frist Frontend
Пример слоев и модулей в Local-Frist Frontend

Моя архитектура отличается минимализмом и отсутствием избыточных слоев, которые могут замедлять разработку на небольших клиентских приложениях. Вместо этого я использую стандартные, интуитивно понятные слои, логика которых практически не изменена. Это позволяет сохранить простоту и скорость разработки, не жертвуя при этом гибкостью и масштабируемостью.

.
├── 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-приключениях!

© Habrahabr.ru