Skip to content

Архитектурные решения

Какие бывают структуры для Vue 3 проектов?

Плоская

При запуске небольшого проекта Vue, такого как Proof of Concept, вы можете предпочесть простую структуру папок, чтобы избежать сложностей:

txt
/src
|-- /components
|   |-- BaseButton.vue
|   |-- BaseCard.vue
|   |-- PokemonList.vue
|   |-- PokemonCard.vue
|-- /composables
|   |-- usePokemon.js
|-- /utils
|   |-- validators.js
|-- /layout
|   |-- DefaultLayout.vue
|   |-- AdminLayout.vue
|-- /plugins
|   |-- translate.js
|-- /views
|   |-- Home.vue
|   |-- PokemonDetail.vue
|-- /router
|   |-- index.js
|-- /store
|   |-- index.js
|-- /assets
|   |-- /images
|   |-- /styles
|-- /tests
|   |-- ...
|-- App.vue
|-- main.js

Атомарный дизайн

Для больших приложений Vue может оказаться полезным использование методологии Atomic Design. Этот подход организует компоненты в иерархию от самого простого к самому сложному:

  • Атомы: Базовые элементы (например, кнопки, иконки).
  • Молекулы: Группы атомов (например, строки поиска).
  • Организмы: Сложные компоненты (например, панели навигации)
  • Шаблоны: Макеты, отображающие структуру компонентов
  • Страницы: Реальные экраны пользовательского интерфейса с реальными данными.

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

image

txt
/src
|-- /components
|   |-- /atoms
|   |   |-- AtomButton.vue
|   |   |-- AtomIcon.vue
|   |-- /molecules
|   |   |-- MoleculeSearchInput.vue
|   |   |-- MoleculePokemonThumbnail.vue
|   |-- /organisms
|   |   |-- OrganismPokemonCard.vue
|   |   |-- OrganismHeader.vue
|   |-- /templates
|   |   |-- TemplatePokemonList.vue
|   |   |-- TemplatePokemonDetail.vue
|-- /pages
|   |-- PageHome.vue
|   |-- PagePokemonDetail.vue
|-- /composables
|   |-- usePokemon.js
|-- /utils
|   |-- validators.js
|-- /layout
|   |-- LayoutDefault.vue
|   |-- LayoutAdmin.vue
|-- /plugins
|   |-- translate.js
|-- /router
|   |-- index.js
|-- /store
|   |-- index.js
|-- /assets
|   |-- /images
|   |-- /styles
|-- /tests
|   |-- ...
|-- App.vue
|-- main.js

Модульный дизайн

По мере масштабирования проекта рассмотрите возможность создания модульной архитектуры. Такая структура инкапсулирует каждую функцию или домен, повышая поддерживоемость и готовя к потенциальной эволюции в сторону микросервисов:

txt
/src
|-- /core
|   |-- /components
|   |   |-- BaseButton.vue
|   |   |-- BaseIcon.vue
|   |-- /models
|   |-- /store
|   |-- /services
|   |-- /views
|   |   |-- DefaultLayout.vue
|   |   |-- AdminLayout.vue
|   |-- /utils
|   |   |-- validators.js
|-- /modules
|   |-- /pokemon
|   |   |-- /components
|   |   |   |-- PokemonThumbnail.vue
|   |   |   |-- PokemonCard.vue
|   |   |   |-- PokemonListTemplate.vue
|   |   |   |-- PokemonDetailTemplate.vue
|   |   |-- /models
|   |   |-- /store
|   |   |   |-- pokemonStore.js
|   |   |-- /services
|   |   |-- /views
|   |   |   |-- PokemonDetailPage.vue
|   |   |-- /tests
|   |   |   |-- pokemonTests.spec.js
|   |-- /search
|   |   |-- /components
|   |   |   |-- SearchInput.vue
|   |   |-- /models
|   |   |-- /store
|   |   |   |-- searchStore.js
|   |   |-- /services
|   |   |-- /views
|   |   |-- /tests
|   |   |   |-- searchTests.spec.js
|-- /assets
|   |-- /images
|   |-- /styles
|-- /scss
|-- App.vue
|-- main.ts
|-- router.ts
|-- store.ts
|-- /tests
|   |-- ...
|-- /plugins
|   |-- translate.js

Feature Sliced Design

Feature-Sliced Design - это способ организации больших и долгосрочных проектов, чтобы ими было легче управлять и развивать. При таком подходе приложение разбивается на различные слои, каждый из которых играет определенную роль:

  • App: Приложение: Глобальные настройки, стили и провайдеры.
  • Pages: Создание полноценных страниц с использованием сущностей, функций и виджетов.
  • Widgets: Объединяют сущности и функции в целостные блоки пользовательского интерфейса, например IssueList или UserProfile.
  • Functions: Обрабатывают пользовательские взаимодействия, которые добавляют ценность, такие как отправка комментариев, добавление в корзину или поиск пользователей.
  • Entities: Представляют основные бизнес-модели, такие как пользователь, продукт и заказ.
  • Shared: Предоставляет многократно используемые утилиты и компоненты, не связанные с конкретной бизнес-логикой, такие как UIKit, библиотеки и API.

image

txt
/src
|-- /app
|   |-- App.vue
|   |-- main.js
|   |-- app.scss
|-- /processes
|-- /pages
|   |-- Home.vue
|   |-- PokemonDetailPage.vue
|-- /widgets
|   |-- UserProfile.vue
|   |-- PokemonStatsWidget.vue
|-- /features
|   |-- pokemon
|   |   |-- CatchPokemon.vue
|   |   |-- PokemonList.vue
|   |-- user
|   |   |-- Login.vue
|   |   |-- Register.vue
|-- /entities
|   |-- user
|   |   |-- userService.js
|   |   |-- userModel.js
|   |-- pokemon
|   |   |-- pokemonService.js
|   |   |-- pokemonModel.js
|-- /shared
|   |-- ui
|   |   |-- BaseButton.vue
|   |   |-- BaseInput.vue
|   |   |-- Loader.vue
|   |-- lib
|   |   |-- api.js
|   |   |-- helpers.js
|-- /assets
|   |-- /images
|   |-- /styles
|-- /router
|   |-- index.js
|-- /store
|   |-- index.js
|-- /tests
|   |-- featureTests.spec.js

Микрофронтенды

Микрофронтенды берут за основу идею микросервисов и применяют ее к фронтенд-части веб-приложений. Это означает, что разные команды могут работать над разными разделами веб-приложения, не мешая друг другу. Каждый раздел, или «микрофронтенд», работает сам по себе и может изменяться независимо. Вот базовый обзор SPA. Обратите внимание, что в этой статье я не буду углубляться в детали того, как на самом деле работают микрофронтенды.

Оболочка приложения: Это основной контроллер, который управляет базовой версткой и маршрутизацией сайта. Он соединяет все микрофронтенды вместе. Декомпозированные пользовательские интерфейсы: Каждый микрофронтенд фокусируется на определенной части приложения. Они могут быть разработаны с использованием разных технологий и обновляться отдельно.

image

Основное преимущество заключается в том, что микрофронтенды позволяют командам обновлять отдельные части приложения, не дожидаясь остальных, что ускоряет разработку. Однако такая настройка может сделать приложение более сложным в управлении и поддержании последовательности.

Полезные ресурсы: Micro Frontends - Extending Microservice Ideas to Frontend DevelopmentMartin Fowler on Microfrontends

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

Сравнительная таблица
ПодходОписаниеПлюсыМинусы
Плоский подходПростая структура, идеально подходящая для небольших проектов или проверки концепции.- Простота реализации
- Минимальная настройка
- Не масштабируется
- Беспорядок по мере роста проекта
Атомный дизайнИерархическая структура, основанная на сложности компонентов.- Масштабируемые
- Организованные
- Многоразовые компоненты
- Накладные расходы на управление слоями
- Сложная настройка
МодулиМодульная структура, инкапсулирующая функции.- Масштабируемость
- Инкапсулированные функции
- Возможное дублирование
- Может стать сложным
Многофункциональный дизайнОрганизует проект по функциональным слоям и фрагментам.- Высокая сплоченность
- Четкое разделение функций
- Начальная сложность
- Требует тщательного планирования
МикроинтерфейсыКаждая часть приложения развертывается отдельно.- Независимые развертывания
- Масштабируемость
- Высокая сложность
- Необходима координация между командами
Подходящая структура для Vue 3 проекта

Самая удобная архитектура для Vue 3 приложения - модульная.

Изначально, приложение разбивается на логические модули, слабо связанные друг с другом. Например, для онлайн магазина это могут быть модуль каталога товаров, модуль отдельного товара, модуль личного кабинета / аккаунта пользователя.

Кроме того, есть модуль оболочки - то, что иногда называет app shell. Шаблон с хедером, футером, боковым меню и главным окном. В этот модуль целесообразно поместить утилиты и ресурсы, используемые по всему приложению - например, api, useI18n, BaseButton и stringHelpers.

В каждом модуле свои components, composables, assets, utils; возможно, api, routes, views, layouts.

Слабая связанность позволяет разрабатывать каждый модуль достаточно независимо, что значительно повышает вероятность успешности проекта.

image

Как переиспользовать одни те же разрабатываемые ресурсы на нескольких проектах?

Иногда есть два или более проекта (админ панель и сайт, например) которые должны использовать одни и те же компоненты, утилиты, сервисы, композабл функции и прочие ресурсы, которые сами в свою очередь находятся в процессе разработки.

Самое удобное в данном случае, выделить все ресурсы в отдельный проект/репозиторий, и использовать Git submodules для подключения его как поддиректории в каждом проекте. VS Code позволяет прозрачно и комфортно работать одновременно и с проектом, и с подключенными сабмодулями.

Если нужна версионность и независимость библиотеки ресурсов, можно оформить её как private NPM пакет.

Когда использовать Provide/Inject?

Use cases

  • Настройки темы или конфигурации: Вы можете предоставить глобальные настройки, темы или параметры конфигурации на корневом уровне вашего приложения Vue.js и внедрить их в любой компонент, которому они нужны.

  • Данные аутентификации: Предоставляйте информацию об аутентификации пользователя, например данные пользователя или токены аутентификации, компонентам, которым они необходимы, не передавая их через пропсы.

  • Интернационализация (i18n): Храните данные перевода на корневом уровне и внедряйте их в компоненты, которым требуется отображать текст на разных языках.

  • Тесно связанные компоненты: Например, компонент AccordianPanel, который всегда появляется только внутри слота компонента Accordian. Вы можете инжектировать общие данные, к которым вы всегда хотите иметь доступ, без необходимости передавать их в качестве пропсов.

Лучшие практики и соображения

При использовании provide и inject необходимо помнить о нескольких лучших практиках:

  • Избегайте чрезмерного использования: Хотя provide и inject могут быть очень мощными, не стоит ими злоупотреблять. Оставьте их для случаев, когда вам действительно нужно поделиться данными между компонентами, не создавая тесной связи между ними.

  • Четкая документация: Документируйте данные, которые вы провайдите и инжектируете, чтобы разработчикам было легко понять, как взаимодействуют компоненты. Их также можно типизировать, - подробности в официальной документации.

  • Рассмотрите возможность Prop Drilling: Prop drilling обычно является антипаттерном, но в некоторых случаях передача данных через пропсы может быть более простым и прозрачным подходом. Оцените, являются ли provide и inject лучшим решением для вашего конкретного сценария.

Как лучше организовать работу с лэйаутами (макетами)?

У приложения может быть несколько лэйаутов - один с боковым меню, например, и другой - без. Какие-то страницы показываются с боковым меню, другие - без.

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

Пример:

vue
// MainLayout
<template>
  <div>
    <AppHeader />
    <router-view />
    <AppFooter />
  </div>
</template>

// SidebarLayout
<template>
  <div>
    <AppHeader />
    <div>
        <AppSidebar />
        <router-view />
    </div>
    <AppFooter />
  </div>
</template>

// App.vue
<template>
  <router-view />
</template>

routes = generalRoutes U accountRoutes

js
export const generalRoutes = {
  path: "/",
  component: MainLayout,
  children: [
    {
      path: "/maintenance",
      name: "maintenance",
      component: MaintenanceView,
    },
  ]
};
// ...

export const accountRoutes = {
  path: "/account",
  component: SidebarLayout,
  children: [
    {
      path: "",
      name: "account",
      component: UserAccountView,
      meta: { requiresAuth: true, backRoute: "/" },
    }
  ]
};
Как сделать один глобальный прелоадер (аналог Suspense)?

Если в приложении одновременно может выполняться несколько асинхронных функций (загрузка с бэкенда, например), а анимационный прелоадер надо показывать один, то можно несложно реализовать его через composable функцию:

AppLoader.vue
vue
<script setup>
import { useAppLoader } from "@/app/composables/useAppLoader";
const { loading } = useAppLoader();
</script>

<template>
  <div class="loader" :class="{ active: loading }">
    <div class="loaderBar" />
  </div>
</template>
useAppLoader.ts
ts
import { computed, reactive, ref } from "vue";

const loaderSet = reactive(new Set<string>());
const loading = computed(() => loaderSet.size > 0);

export function useAppLoader(id) {
  function startLoading() {
    loaderSet.add(id);
  }
  function stopLoading() {
    loaderSet.delete(id);
  }

  return { loading, startLoading, stopLoading };
}
SomeComponent
js
import { useId } from "Vue";
import { useAppLoader } from "@/app/composables/useAppLoader";

const { startLoading, stopLoading } = useAppLoader(useId());

startLoading();
product.value = await api.products.product(props.productId);
stopLoading();

Данный пример использует useId, который появился во Vue 3.5

Для более ранних версий можно использовать любую функцию для генерации уникального id.