Как выстраивать систему модульных и переиспользуемых компонентов?
Выстраивание системы модульных и переиспользуемых компонентов в 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-компоненты возвращают только логику, а шаблон реализует родитель:
<!-- TooltipLogic.vue -->
<template>
<slot :show="show" :hide="hide" />
</template>
<!-- Использование -->
<TooltipLogic v-slot="{ show, hide }">
<button @mouseover="show" @mouseleave="hide">Навести</button>
</TooltipLogic>
Позволяет вынести повторяемую логику без навязывания 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. Вариативность через пропсы
Компоненты могут быть настраиваемыми:
<UiButton variant="primary" size="lg" icon="plus">
Добавить
</UiButton>
Это избавляет от необходимости дублировать однотипные компоненты с незначительными отличиями.
14. Инкапсуляция зависимостей
Компоненты не должны напрямую зависеть от:
-
window, document
-
внешних API
-
глобальных стилей
Всё это выносится в провайдеры, абстракции или обёртки.
15. Возможность упаковки как библиотека
Хорошо структурированные UI-компоненты легко выносить в собственный NPM-пакет:
-
У всех компонентов есть index.ts
-
Поддержка TypeScript типов
-
Стили инкапсулированы
-
Не содержат сторонних зависимостей
-
Используется Rollup или Vite для сборки