Патерн модульних компонентів: альтернативний підхід до архітектури Vue.js

Як я вирішив проблеми управління станом та організації компонентів у великомасштабних Vue.js додатках, йдучи проти конвенцій фреймворку

Проблема

Після 4,5 років роботи з Vue.js на великих enterprise проектах, я постійно стикався з одними й тими ж архітектурними проблемами:

  • Пекло prop drilling: передача даних через множинні рівні компонентів лише для досягнення глибоко вкладених дітей
  • Хаос управління станом: змішування глобальних сторів, локального стану та provide/inject призводило до непередбачуваної поведінки
  • Зв’язаність компонентів: компоненти ставали тісно пов’язаними через спільний стан та складні залежності
  • Когнітивне навантаження: «магія» Vue ускладнювала відлагодження, коли щось йшло не так

Традиційні Vue паттерни як provide/inject, глобальні стори (Pinia/Vuex) та надмірне покладання на Composition API здавалися занадто «магічними» для складної бізнес-логіки. Мені потрібно було більше контролю та передбачуваності.

Рішення: Патерн Модульних Компонентів

Протягом двох років я розробив те, що називаю Патерн Модульних Компонентів — архітектуру, яка розглядає кожен батьківський компонент як самодостатній модуль з власною екосистемою.

Основна Філософія

  1. Явність над Магією: перевага ванільного JavaScript над фреймворковими абстракціями
  2. Ізоляція над Спільністю: компоненти повинні бути самодостатніми модулями
  3. Розділення Логіки-Шаблону: вся бізнес-логіка в JavaScript, шаблони лише для відображення
  4. Багаторівнева Архітектура: чітке розділення між глобальними, загальними та локальними концернами

Огляд Архітектури

src/
├── store/ # Глобальний стан
├── types/ # Глобальні інтерфейси 
├── constants/ # Глобальні константи
├── utils/ # Глобальні утиліти
├── composables/ # Утилітарні хуки (useApi, useModal)
├── components/
│ └── common/ # Багаторазові UI компоненти (лише пропси)
└── features/
 └── UserProfile/ # Модуль компонент
 ├── UserProfile.vue
 ├── components/ # Дочірні компоненти (в межах цього модуля)
 ├── store.ts # Локальний стан
 ├── run.ts # API функції
 ├── types.ts # Локальні типи
 └── utils.ts # Локальні утиліти

Деталі Реалізації

1. Управління Локальним Станом

Замість глобальних сторів або provide/inject, кожен модуль керує власним станом:

// UserProfile/store.ts
import { reactive, ref } from ’vue’
export const state = reactive({
user: null,
loading: false,
error: null
})
export const selectedTab = ref(’profile’)
// Групове скидання коли потрібно
export const resetStore = () => {
state.user = null
state.loading = false
state.error = null
selectedTab.value = ’profile’
}

2. API Рівень з Чистими Функціями

Я використовую два підходи для API функцій, віддаючи перевагу чистим функціям:

Пріоритетний: Чисті функції

// UserProfile/run.ts
export const fetchUser = async (id: string) => {
 const response = await api.get(`/users/${id}`)
 return response.data
}
// У компоненті
const userData = await fetchUser(userId)<br>state.user = userData

Прагматичний: Сайд-ефекти (коли зручно)

// UserProfile/run.ts
import { state } from ’./store’
export const fetchUser = async (id: string) => {
 state.loading = true
 try {
 const response = await api.get(`/users/${id}`)
 state.user = response.data
 } finally {
 state.loading = false
 }
}

3. Common Компоненти через Слоти

Багаторазові UI компоненти ніколи не керують станом — лише пропси та слоти:

<!— components/common/Table/Table.vue —>
<template>
 <table :class="tableClass">
 <slot name="header" />
 <slot />
 <slot name="footer" />
 </table>
</template>
<!— Використання —>
<Table bordered>
 <template #header>
 <TableRow>
 <TableCell>Ім’я</TableCell>
 <TableCell>Email</TableCell>
 </TableRow>
 </template> 
 <TableRow v-for="user in users" :key="user.id">
 <TableCell>{{ user.name }}</TableCell>
 <TableCell>{{ user.email }}</TableCell>
 </TableRow>
</Table>

4. Розділення Логіки-Шаблону

Вся бізнес-логіка залишається в JavaScript, шаблони лише обробляють відображення:

// У script setup
const showNextButton = computed(() => {
 return currentStep.value < totalSteps.value && 
 !loading.value && 
 isFormValid.value
})<br>const showSubmitButton = computed(() => {
 return currentStep.value === totalSteps.value && 
 !hasErrors.value
})
<!— У template —>
<Button v-if="showNextButton" @click="nextStep">
 Далі
</Button>
<Button v-if="showSubmitButton" @click="submit«>
 Відправити
</Button>

5. Очищення Стану

Ручне управління станом вимагає дисциплінованого очищення:

import { onUnmounted } from ’vue’
import { resetStore } from ’./store’<br>onUnmounted(() => {
 resetStore()
})

6. Рідкісна Міжкомпонентна Комунікація

Для рідкісних випадків, що потребують комунікації між віддаленими компонентами:

// composables/useButtonState.ts
const isBlocked = ref(true)<br>export const useButtonState = () => {
 const unblock = () => isBlocked.value = false
 const block = () => isBlocked.value = true 
 return { isBlocked, unblock, block }
}

Стейт Машини для Складної Логіки

Для складних багатоетапних процесів я інтегрую стейт машини (XState) замість ручного управління умовами:

// composables/useStepperMachine.ts
import { createMachine, interpret } from ’xstate’<br>const stepperMachine = createMachine({
 id: ’stepper’,
 initial: ’step1′,
 states: {
 step1: {
 on: { NEXT: { target: ’step2′, cond: ’isStep1Valid’ } }
 },
 step2: {
 on: { 
 NEXT: { target: ’step3′, cond: ’isStep2Valid’ },
 PREV: ’step1′
 }
 },
 // ... більше станів
 }
})

Застосування в Реальному Світі

Я застосовую цей патерн двома способами:

  1. Нові проекти: почати з цієї архітектури з самого початку
  2. Існуючі проекти: створювати нові фічі, використовуючи цей патерн локально, не порушуючи існуючий код

Я успішно впровадив цей підхід у кількох enterprise проектах. Патерн добре масштабується та робить код більш підтримуваним.

Компроміси та Критика

Недоліки:

  • Вища крива навчання для Vue розробників
  • Більше boilerplate коду
  • Йде проти філософії Vue «конвенція над конфігурацією»
  • Ризик витоків пам’яті, якщо забути очищення
  • Складніше використовувати бібліотеки Vue екосистеми
  • Потенційне дублювання коду між модулями

Коли НЕ використовувати:

  • Малі додатки
  • Команди, нові для Vue
  • Проекти, що потребують швидкого прототипування
  • Сильна залежність від плагінів Vue екосистеми

Переваги

Плюси:

  • Передбачувана поведінка та легше відлагодження
  • Краще розділення обов’язків
  • Легший onboarding для React розробників
  • Самодостатні модулі зменшують зв’язаність
  • Явні залежності роблять код легшим для розуміння
  • Добре масштабується для великих команд та складної бізнес-логіки

Висновок

Патерн Модульних Компонентів жертвує зручністю Vue заради контролю та передбачуваності. Він не для всіх, але для великомасштабних додатків, де підтримуваність та явна поведінка важливіші за швидкість розробки, він забезпечує міцну основу.

Цей підхід виник з реальних болючих точок у enterprise розробці. Хоча він йде проти зерна Vue, він пропонує альтернативу для команд, які цінують явний контроль над магією фреймворку.

Що ви думаєте? Чи стикалися ви з подібними архітектурними викликами у Vue.js? Мені цікаво почути ваші думки та критику цього підходу.

Я розробляв та вдосконалював цей патерн протягом 2+ років у кількох великомасштабних проектах.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Так дивно читати цю статтю після прочитання чергової про те, як у Ангуляр середовищі пропагують відмову від модулів, перехід на standalone компоненти :)

Добрий день, в Ангулярі інший підхід

я б до сервер апі функцій не додавав сайд ефекти ще у вигляді виклику vue api, залишив обробку комунікації з сервером на совісті однієї функції, а станом керувати окремо поза нею. я для цього юзаю vue query, або nuxt useAsync, useFetch — це робить життя солодшим.
а так патерн класний і я пробував його на проєктах, але якщо чесно — ручне очищення стейту мене вбивав. і так, все що повʼязано з логікою бажано виносити в чисті true-чисті функції і юзати ще з прототипністю джс-а і як на мене чотко виходить.

Дякую
vue api це приклад

я використовую useFetch який я обгортаю своєю логікою виклика

Я шось подібне використовую у себе, тільки що назвав це Widget Based Architecture 😅 ну і у мене основний інструмент не Vue, а React.

По суті це певне переосмислення Vertical Sliced Design. Якщо не вдаватися в деталі то доволі сильно спрощує підтримку проєкту, додавання нових фіч, внесення змін в існуючі, багфіксінг тощо.

Підписатись на коментарі