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

Выстраивание системы модульных и переиспользуемых компонентов в Vue — это фундаментальный аспект масштабируемой архитектуры. Такая система позволяет создавать автономные, изолированные и легко интегрируемые элементы интерфейса, которые можно применять в разных частях приложения или даже переиспользовать между проектами. Грамотно спроектированная система компонентов повышает читаемость, ускоряет разработку, облегчает сопровождение и тестирование.

Ниже рассмотрены подходы, рекомендации и технические приёмы, применяемые при построении системы модульных компонентов в крупных Vue-проектах.

1. Разделение по уровням абстракции

UI-компоненты (atom)

  • Элементарные элементы интерфейса: кнопки, поля, иконки

  • Без бизнес-логики

  • Примеры: UiButton, UiInput, UiSelect, UiCheckbox

Composable-компоненты (molecule)

  • Комбинируют UI-элементы, добавляют немного логики

  • Примеры: DateRangePicker, SearchInputWithIcon, ImageUploader

Фичи (feature components)

  • Инкапсулированная бизнес-логика, обработка событий, валидации

  • Примеры: LoginForm, ProductFilters, ChangePasswordForm

Виджеты (widget components)

  • Строятся из фич, composables и UI

  • Примеры: UserDashboardWidget, CartSummaryWidget

Страницы (page components)

  • Контейнеры для сборки виджетов и бизнес-модулей

  • Используются в маршрутах

  • Примеры: AccountPage.vue, ProductDetailPage.vue

Такой подход реализует слоистую архитектуру и повышает модульность.

2. Единая структура проекта

src/
├── components/
 ├── ui/ # Переиспользуемые базовые UI-компоненты
 ├── form/
 └── layout/
├── features/
 ├── auth/
 ├── catalog/
 └── user/
├── widgets/
├── pages/
├── composables/
├── stores/
├── assets/
├── styles/

Каждый слой инкапсулирует свою зону ответственности, и слои не пересекаются горизонтально (например, features не обращаются напрямую к widgets).

3. Общие принципы проектирования компонентов

Изоляция

  • Компонент не должен полагаться на глобальные переменные, хардкод или сторонние DOM-элементы

  • Входные данные передаются через props, выход — через emits

Прогнозируемость

  • Чёткие API: описанные props, emits, slots

  • Нет скрытых эффектов

Минимальные зависимости

  • Логика, зависящая от окружения, выносится в composables или store

Слоты и кастомизация

  • Использование default, header, footer, prepend, append, item слотов

  • Позволяет переопределять отдельные участки шаблона

4. Типизация через TypeScript

Пример описания props:

defineProps<{
modelValue: string
label?: string
disabled?: boolean
}>();

Использование defineEmits:

const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'blur'): void
}>();

Типизация повышает предсказуемость использования компонентов и интеграции.

5. Автоматическая регистрация компонентов

Использование unplugin-vue-components:

Components({
dirs: \['src/components/ui'\],
deep: true,
dts: true,
resolvers: \[\],
})

Или ручная регистрация:

import UiButton from '@/components/ui/UiButton.vue';
app.component('UiButton', UiButton);

Позволяет не импортировать десятки компонентов вручную.

6. Унифицированный API компонентов

Для повторного использования компоненты должны иметь единый и предсказуемый API.

Примеры:

  • Все поля формы используют v-model (modelValue, update:modelValue)

  • Все кнопки принимают variant, size, loading, disabled

  • Все списки принимают items, itemKey, itemSlot

Это позволяет заменять компоненты без необходимости переделывать вызов.

7. Рендер-функции и renderless-подход

Renderless-компоненты возвращают только логику, а шаблон реализует родитель:

&lt;!-- TooltipLogic.vue --&gt;
&lt;template&gt;
&lt;slot :show="show" :hide="hide" /&gt;
&lt;/template&gt;
&lt;!-- Использование --&gt;
&lt;TooltipLogic v-slot="{ show, hide }"&gt;
&lt;button @mouseover="show" @mouseleave="hide"&gt;Навести&lt;/button&gt;
&lt;/TooltipLogic&gt;

Позволяет вынести повторяемую логику без навязывания HTML-структуры.

8. Использование composables/ для переиспользуемой логики

Примеры:

  • useToggle() — переключатель булевых состояний

  • useModal() — управление состоянием модальных окон

  • usePagination() — управление страницами и лимитами

  • useDebounce() — задержка вызовов

export function useToggle(defaultValue = false) {
const state = ref(defaultValue);
const toggle = () => (state.value = !state.value);
return { state, toggle };
}

Композиционные функции упрощают тестирование и повторное использование логики между компонентами.

9. Инкапсуляция состояний через Store (Pinia)

Состояние выносится в Store:

export const useCartStore = defineStore('cart', {
state: () => ({ items: \[\] }),
actions: {
add(item) {
this.items.push(item);
}
}
});

Компоненты подписываются на store через useCartStore() и становятся независимыми от глобальных данных.

10. Документирование компонентов

Каждый компонент сопровождается:

  • JSDoc-комментариями или README.md

  • Примером использования

  • Storybook-примером (stories)

/\*\*
\* UiInput  стандартное текстовое поле
\* @prop {string} modelValue  значение поля
\* @prop {string} label  подпись
\*/

Это важно для переиспользования внутри команды и между проектами.

11. Стилизация и темы

  • Использование CSS-переменных (--primary-color) и :root токенов

  • Возможность передачи класса через class, variant или style пропсы

  • Разделение базовых стилей (@/styles/variables.scss) и тем

12. Согласованные тесты

  • Unit-тесты для логики

  • Snapshot-тесты для шаблонов

  • Проверка событий, пропсов, слотов

  • E2E-тесты на интеграцию с другими компонентами

test('emit value on input', () => {
const wrapper = mount(UiInput, {
props: {
modelValue: '',
'onUpdate:modelValue': vi.fn()
}
});
wrapper.find('input').setValue('abc');
expect(wrapper.emitted()\['update:modelValue'\]\[0\]).toEqual(\['abc'\]);
});

13. Вариативность через пропсы

Компоненты могут быть настраиваемыми:

&lt;UiButton variant="primary" size="lg" icon="plus"&gt;
Добавить
&lt;/UiButton&gt;

Это избавляет от необходимости дублировать однотипные компоненты с незначительными отличиями.

14. Инкапсуляция зависимостей

Компоненты не должны напрямую зависеть от:

  • window, document

  • внешних API

  • глобальных стилей

Всё это выносится в провайдеры, абстракции или обёртки.

15. Возможность упаковки как библиотека

Хорошо структурированные UI-компоненты легко выносить в собственный NPM-пакет:

  • У всех компонентов есть index.ts

  • Поддержка TypeScript типов

  • Стили инкапсулированы

  • Не содержат сторонних зависимостей

  • Используется Rollup или Vite для сборки