Со временем многие Vue-компоненты превращаются в мини-приложения. Они загружают данные, делают запросы, хранят бизнес-логику, управляют состоянием и одновременно отвечают за отображение.
Проблема становится заметна, когда один и тот же интерфейс нужно использовать в другом месте. Вместо переиспользования приходится копировать код или добавлять десятки пропсов и флагов.
В этой статье разберем подход, который помогает избежать подобных проблем — превращение компонента в чистый UI-слой.
Как выглядит проблемный компонент
Представим список заказов.
<script setup lang="ts">
const orders = ref([])
const loading = ref(false)
async function loadOrders() {
loading.value = true
orders.value = await api.orders.list()
loading.value = false
}
async function cancelOrder(id: number) {
await api.orders.cancel(id)
await loadOrders()
}
onMounted(loadOrders)
</script>
<template>
<div v-if="loading">
Загрузка...
</div>
<div
v-for="order in orders"
:key="order.id"
>
<span>{{ order.name }}</span>
<button
@click="cancelOrder(order.id)"
>
Отменить
</button>
</div>
</template>
На первый взгляд всё хорошо.
Но компонент теперь отвечает сразу за несколько вещей:
- загрузку данных;
- отображение списка;
- отмену заказа;
- повторную загрузку данных;
- состояние загрузки.
Из-за этого его сложно использовать повторно.
Выделяем ответственность
Первый шаг — понять, что компоненту на самом деле нужно для отображения.
Ему необходимы:
- список заказов;
- признак загрузки;
- событие отмены заказа.
Всё остальное можно вынести наружу.
Создаем UI-компонент
Теперь компонент занимается исключительно отображением.
<script setup lang="ts">
defineProps<{
orders: Order[]
loading: boolean
}>()
defineEmits<{
cancel: [number]
}>()
</script>
<template>
<div v-if="loading">
Загрузка...
</div>
<div
v-for="order in orders"
:key="order.id"
>
<span>{{ order.name }}</span>
<button
@click="$emit('cancel', order.id)"
>
Отменить
</button>
</div>
</template>
Компонент больше ничего не знает про API.
Он просто отображает данные и сообщает о действиях пользователя.
Переносим логику в контейнер
Теперь создадим отдельный компонент с логикой.
<script setup lang="ts">
import OrderList from './OrderList.vue'
const orders = ref([])
const loading = ref(false)
async function loadOrders() {
loading.value = true
orders.value =
await api.orders.list()
loading.value = false
}
async function cancelOrder(
id: number
) {
await api.orders.cancel(id)
await loadOrders()
}
onMounted(loadOrders)
</script>
<template>
<OrderList
:orders="orders"
:loading="loading"
@cancel="cancelOrder"
/>
</template>
Теперь каждая часть занимается своей задачей.
Что мы получили
После разделения становится проще:
Тестировать
UI-компонент можно протестировать без моков API.
Достаточно передать данные через пропсы.
Переиспользовать
Один и тот же список можно использовать:
- на странице заказов;
- в модальном окне;
- в административной панели;
- в мобильной версии.
Компоненту всё равно, откуда пришли данные.
Поддерживать
Изменения в бизнес-логике не затрагивают отображение.
Изменения в интерфейсе не затрагивают работу API.
Следующий шаг — виджеты
Со временем контейнеры тоже начинают разрастаться.
Например:
OrderList
└── OrderListWidget
├── загрузка данных
├── фильтрация
├── сортировка
└── обработка действий
В такой схеме UI-компоненты остаются максимально простыми.
Они получают данные через пропсы и генерируют события.
Вся бизнес-логика живет в виджетах, composable-функциях или сервисах.
Как понять, что компонент пора разделять
Обычно это заметно по нескольким признакам:
- внутри есть запросы к API;
- используется несколько composable с бизнес-логикой;
- компонент сложно протестировать;
- компонент нельзя использовать без подключения половины приложения;
- количество строк быстро растет.
Если компонент отвечает на вопрос «как показать?», он относится к UI-слою.
Если отвечает на вопрос «что делать?», скорее всего, это уже уровень логики.
Итог
Чистый UI-компонент не знает ничего о сервере, хранилищах данных или бизнес-процессах.
Его задача максимально проста:
- получить данные через пропсы;
- отобразить их;
- сообщить о действиях пользователя через события.
Такой подход делает код более предсказуемым, упрощает тестирование и позволяет переиспользовать интерфейс без копирования логики между проектами и страницами.


Добавить комментарий