Стандарты

Декомпозиция компонентов

Стандарт разделения компонентов на переиспользуемые части для поддержания читаемости и 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.vue45Оркестрация
OrderFilters.vue60Фильтрация
OrderTable.vue80Отображение таблицы
OrderRow.vue40Строка таблицы
OrderStatusBadge.vue25Статус заказа
OrderDetailsModal.vue90Модальное окно
useOrders.ts70Бизнес-логика

Итого: 410 строк, но в 7 файлах по ~60 строк каждый.

Связанные документы

Источники