Стандарты
Декомпозиция компонентов
Стандарт разделения компонентов на переиспользуемые части для поддержания читаемости и maintainability.
Цель
Обеспечить читаемость, тестируемость и переиспользуемость компонентов через контроль их размера и ответственности.
Правила
1. Максимальный размер компонента
| Метрика | Рекомендация | Жёсткий лимит |
|---|---|---|
| Общий размер файла | ≤ 200 строк | 300 строк |
<template> | ≤ 100 строк | 150 строк |
<script setup> | ≤ 80 строк | 120 строк |
| Tailwind классы на элементе | ≤ 7 утилит | 10 утилит |
Источник: eslint-plugin-vue/max-lines-per-block
2. Принцип единственной ответственности
Каждый компонент должен отвечать за одну задачу:
<!-- ❌ Плохо — компонент делает слишком много -->
<template>
<div>
<!-- Фильтры -->
<div class="filters">...</div>
<!-- Таблица -->
<table>...</table>
<!-- Пагинация -->
<div class="pagination">...</div>
<!-- Модальное окно -->
<div class="modal">...</div>
</div>
</template>
<!-- ✅ Хорошо — декомпозиция по ответственности -->
<template>
<div>
<OrderFilters v-model="filters" />
<OrderTable :data="orders" @select="openDetails" />
<OrderPagination v-model:page="page" :total="total" />
<OrderDetailsModal v-model="showModal" :order="selectedOrder" />
</div>
</template>
3. Когда декомпозировать
Обязательно выносить в отдельный компонент:
| Сигнал | Действие |
|---|---|
| Блок > 50 строк в template | Вынести в компонент |
| Повторяющийся код | Создать переиспользуемый компонент |
| Сложная логика в template | Вынести в composable или computed |
| Много Tailwind классов (> 7) | Вынести в компонент |
| Вложенность > 4 уровней | Декомпозировать |
4. Иерархия компонентов
pages/
└── orders/
└── index.vue # Страница (оркестрация)
├── OrderFilters.vue # Фильтры
├── OrderTable.vue # Таблица
│ └── OrderRow.vue # Строка таблицы
├── OrderPagination.vue
└── OrderDetailsModal.vue
Правило именования: Доменный префикс + назначение (OrderFilters, UserAvatar, ServiceCard).
5. Вынос логики в composables
Если <script setup> > 50 строк бизнес-логики — выносить в composable:
<!-- ❌ Плохо — много логики в компоненте -->
<script setup lang="ts">
const filters = ref({})
const orders = ref([])
const loading = ref(false)
const page = ref(1)
const total = ref(0)
async function fetchOrders() {
loading.value = true
try {
const response = await useApi().get('/orders', {
params: { ...filters.value, page: page.value }
})
orders.value = response.data
total.value = response.meta.total
} finally {
loading.value = false
}
}
watch(filters, () => {
page.value = 1
fetchOrders()
}, { deep: true })
watch(page, fetchOrders)
onMounted(fetchOrders)
// + ещё 50 строк логики...
</script>
<!-- ✅ Хорошо — логика в composable -->
<script setup lang="ts">
const { orders, filters, page, total, loading } = useOrders()
</script>
// composables/useOrders.ts
export function useOrders() {
const filters = ref({})
const orders = ref([])
const loading = ref(false)
const page = ref(1)
const total = ref(0)
async function fetchOrders() {
// ...
}
watch(filters, () => {
page.value = 1
fetchOrders()
}, { deep: true })
watch(page, fetchOrders)
onMounted(fetchOrders)
return { orders, filters, page, total, loading }
}
6. Декомпозиция Tailwind классов
<!-- ❌ Плохо — слишком много классов -->
<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md border border-gray-200 hover:shadow-lg transition-shadow duration-200">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
<UIcon name="i-heroicons-user" class="w-5 h-5 text-blue-600" />
</div>
<span class="text-sm font-medium text-gray-900">{{ user.name }}</span>
</div>
</div>
<!-- ✅ Хорошо — декомпозиция на компоненты -->
<UserCard :user="user" />
<!-- components/UserCard.vue -->
<template>
<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md border border-gray-200 hover:shadow-lg transition-shadow">
<UserAvatar :user="user" />
</div>
</template>
<!-- components/UserAvatar.vue -->
<template>
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
<UIcon name="i-heroicons-user" class="w-5 h-5 text-blue-600" />
</div>
<span class="text-sm font-medium text-gray-900">{{ user.name }}</span>
</div>
</template>
ESLint конфигурация
// eslint.config.mjs
import pluginVue from 'eslint-plugin-vue'
export default [
...pluginVue.configs['flat/recommended'],
{
rules: {
// Лимит строк на блок
'vue/max-lines-per-block': ['warn', {
template: 150,
script: 120,
style: 100,
}],
// Общий лимит файла
'max-lines': ['warn', {
max: 300,
skipBlankLines: true,
skipComments: true,
}],
},
},
]
Чек-лист при code review
- Компонент ≤ 200 строк (или обоснование)
- Одна ответственность
- Нет дублирования кода
- Сложная логика вынесена в composables
- Вложенность template ≤ 4 уровней
- Tailwind классы ≤ 7-10 на элемент
Примеры
Пример: Страница заказов
До декомпозиции: 450 строк в одном файле.
После декомпозиции:
| Компонент | Строк | Ответственность |
|---|---|---|
pages/orders/index.vue | 45 | Оркестрация |
OrderFilters.vue | 60 | Фильтрация |
OrderTable.vue | 80 | Отображение таблицы |
OrderRow.vue | 40 | Строка таблицы |
OrderStatusBadge.vue | 25 | Статус заказа |
OrderDetailsModal.vue | 90 | Модальное окно |
useOrders.ts | 70 | Бизнес-логика |
Итого: 410 строк, но в 7 файлах по ~60 строк каждый.