Разработка · 7 мин чтения

Vue 3 Composition API для дашбордов: практический разбор

Vue 3 Composition API я держу под рукой на каждом дашборде, который собираю за последний год - от панели с метриками эквайринга T‑Bank до трекинга отправлений СДЭК в личном кабинете магазина. Когда на экране 15-20 виджетов, у каждого своё состояние, свои фильтры и своё обновление по таймеру, Options API расползается: логика одного графика оказывается размазана по data, computed, methods и watch, а рядом лежит логика ещё пяти. Композиционный API собирает всё это в одну функцию, которую видно целиком и которую я переношу в соседний проект копипастом. Ниже - как это выглядит на боевых панелях, с кодом, цифрами и граблями, на которые я наступал.

Почему композиционный API удобнее Options API на дашбордах

Дашборд отличается от обычной страницы тем, что виджеты живут своей жизнью: один тянет продажи WooCommerce за неделю, второй крутит статус доставок, третий раз в минуту дёргает вебхук из n8n. В Options API каждый такой блок вынуждает лезть в четыре разных секции компонента. В Composition API одна фича - одна функция, и переиспользование получается почти бесплатным.

Вот честное сравнение по тем критериям, которые реально болят на больших панелях:

Критерий Options API Composition API
Логика одного виджета Размазана по data / computed / methods / watch Собрана в одной функции-композибле
Переиспользование между панелями Миксины с конфликтами имён Импорт композибла, ноль магии
Читаемость при 15+ виджетах Файл на 600 строк, скролл вслепую setup из 5-6 понятных вызовов
TypeScript Работает, но с бубном Типы выводятся из ref и computed сами
Тестирование логики Только через монтирование компонента Композибл тестируется как обычная функция

На панели для интернет-магазина, где было 18 виджетов, переход с миксинов на композиблы сократил главный компонент дашборда с 640 строк до 90 - вся тяжёлая логика уехала в шесть отдельных файлов в папке composables, и каждый я потом переиспользовал в админке на другом проекте без единой правки.

Реактивное состояние: ref, reactive и computed

Первое, обо что спотыкаются те, кто пришёл с Options API - ref против reactive. Моё правило после десятка панелей простое: примитивы и одиночные значения (число, строка, флаг загрузки) - через ref; связный объект состояния фильтров - через reactive, но без фанатизма. ref требует .value в скрипте, зато его нельзя случайно «разложить» деструктуризацией и потерять реактивность, а с reactive это происходит регулярно.

Производные метрики - только computed, никогда не пересчёт вручную во watch. Сумма по строкам таблицы, средний чек, конверсия - всё это чистые вычисления от исходных данных, и computed кэширует результат, пока источник не изменился. На дашборде с 30-секундным поллингом это экономит десятки лишних перерасчётов в минуту.

import { ref, computed, onMounted, onUnmounted } from 'vue'

export function useDashboardData(endpoint, intervalMs = 30000) {
  const rows = ref([])
  const loading = ref(false)
  const error = ref(null)
  let timer = null

  async function load() {
    loading.value = true
    try {
      const res = await fetch(endpoint)
      rows.value = await res.json()
      error.value = null
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }

  const total = computed(() =>
    rows.value.reduce((sum, r) => sum + r.amount, 0)
  )

  onMounted(() => {
    load()
    timer = setInterval(load, intervalMs)
  })
  onUnmounted(() => clearInterval(timer))

  return { rows, loading, error, total, reload: load }
}

Эта функция - самодостаточный кусок дашборда: она сама грузит данные, сама обновляет их по таймеру и сама подчищает таймер при размонтировании. В компоненте её подключение занимает одну строку.

<script setup>
import { useDashboardData } from '@/composables/useDashboardData'

const { rows, total, loading, reload } = useDashboardData('/api/orders')
</script>

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Композиблы: выносим логику виджетов из компонента

Композибл - обычная функция, которая начинается с use и возвращает реактивное состояние вместе с методами. Смысл в том, что setup перестаёт быть свалкой и превращается в оглавление: видно, из чего собрана панель, ещё до того как ты открыл сами файлы.

Я раскладываю композиблы по зонам ответственности:

  • useDashboardData - загрузка и поллинг одного источника, как выше;
  • useFilters - реактивный объект фильтров (диапазон дат, статус заказа, город доставки) и его сериализация в query-строку;
  • useChart - жизненный цикл инстанса графика, о нём отдельно ниже;
  • useExport - выгрузка текущей выборки в CSV, которую менеджеры просят в 9 случаях из 10.

Отдельная выгода - тестируемость. Композибл useFilters я покрываю юнит-тестами напрямую, без монтирования компонента: вызвал функцию, подёргал возвращённые ref, проверил, что query собралась правильно. На панели заказов это ловило баги с датами до того, как их видел заказчик.

Если пишете композибл, который обращается к жизненному циклу через onMounted или onUnmounted, помните: вызывать его можно только синхронно внутри setup. Спрятать вызов useDashboardData внутрь await или колбэка - и хуки жизненного цикла молча не подключатся, таймер не запустится, а вы будете час гадать, почему виджет не обновляется. Проверено на себе.

Производительность: shallowRef, markRaw и графики

Самая частая просадка на дашбордах - не сеть, а реактивность поверх тяжёлых объектов. Инстанс Chart.js или ApexCharts - это здоровенный объект с сотнями внутренних полей. Если положить его в обычный ref, Vue рекурсивно сделает реактивным каждое из этих полей, и при обновлении данных браузер начинает подтормаживать на ровном месте.

Лечится двумя приёмами. Сам инстанс графика оборачиваю в markRaw, чтобы Vue вообще не трогал его прокси, и держу ссылку в shallowRef - реактивной остаётся только сама ссылка, а не внутренности.

import { shallowRef, markRaw, watch } from 'vue'
import { Chart } from 'chart.js/auto'

const chart = shallowRef(null)

function mountChart(canvas, data) {
  chart.value = markRaw(new Chart(canvas, {
    type: 'line',
    data
  }))
}

watch(() => props.series, (next) => {
  if (!chart.value) return
  chart.value.data.datasets[0].data = next
  chart.value.update()
})

Второе - не перерисовывать график целиком при каждом обновлении. Через watch я меняю данные внутри существующего инстанса и вызываю его update(), а не пересоздаю компонент. На панели с восемью графиками, которые обновлялись раз в 15 секунд, связка shallowRef + markRaw + точечный update() убрала фризы интерфейса, которые до этого длились по 200-400 мс на каждом тике.

Отдельно про большие таблицы: если строк больше пары тысяч, реактивность самого массива уже не главная беда - тормозит DOM. Тут спасает виртуализация списка, а не борьба с ref. Vue 3 сам по себе держит десятки тысяч реактивных полей спокойно, проблема почти всегда в рендере, а не в реактивной системе.

Когда панель нужна быстро, с интеграциями к эквайрингу и доставке и без месяца на архитектуру, я беру разработку дашборда на Vue.js под ключ от 90 000 ₽ - с уже готовым набором композиблов под поллинг, фильтры и графики, которые не надо изобретать заново.

Живые данные: поллинг, WebSocket и интеграции

Дашборд без свежих данных - картинка. Способ обновления выбираю по задаче, а не по моде.

  • Поллинг раз в 15-60 секунд закрывает 80% случаев: метрики продаж, статусы заказов, остатки. Реализуется тем самым setInterval внутри композибла, дёшево и предсказуемо.
  • WebSocket беру, когда данные должны прилетать в реальном времени - например, новые заявки, которые параллельно улетают в Telegram через aiogram-бота. Подключение оборачиваю в useSocket, а входящие сообщения кладу в тот же ref, что и поллинг, чтобы виджет не знал, откуда пришло обновление.
  • Вебхуки через n8n - удобный слой между внешними сервисами и дашбордом. СДЭК дёргает n8n на смену статуса отправления, n8n складывает событие в базу, а панель забирает его поллингом. Не нужно, чтобы фронтенд напрямую держал десяток интеграций.

Ещё один сценарий из практики - встроить готовый Vue-виджет с метриками прямо в лендинг на Tilda через блок с кодом. Composition API тут выигрывает, потому что виджет монтируется точечно в конкретный div, тянет свой композибл с данными и не тащит за собой оформление всей страницы.

Важный момент про отписки: любой источник живых данных нужно гасить в onUnmounted - закрыть сокет, снять интервал. Забытый таймер на панели, которую пользователь открывает и закрывает по десять раз за смену, за час превращается в горсть висящих запросов и утёкшую память. Композибл хорош тем, что очистку я пишу рядом с подпиской, в той же функции, и забыть про неё физически сложнее.

Интерфейс аналитики и отчётов

Дашборд / BI

от 90 000 ₽

Подробнее →

Частые вопросы

Стоит ли переписывать существующий дашборд с Options API на Composition API?

Если панель работает и её почти не трогают - нет, переписывание ради переписывания только добавит багов. А вот если дашборд активно растёт, каждый новый виджет причиняет боль и файл компонента распух за 500 строк, миграцию окупает уже второй-третий новый виджет. Vue 3 поддерживает оба API одновременно, поэтому переводить можно постепенно: новые фичи писать на композиблах, старые не трогать, пока руки не дойдут.

Что выбрать для состояния - ref или reactive?

Я по умолчанию беру ref для всего: примитивов, массивов, объектов. Он предсказуемее, его нельзя случайно потерять при деструктуризации, и он единообразно выглядит в коде. reactive оставляю для случаев, когда действительно удобно работать со связным объектом без .value на каждом поле - например, объект фильтров, который целиком уходит в форму. Мешать оба стиля в одном композибле без причины не стоит.

Как Composition API дружит с TypeScript на дашборде?

Очень хорошо, и это одна из причин, по которой я на нём и сижу. Типы выводятся из ref и computed автоматически, дженерики у ref<Order[]>() работают понятно, а композиблы отдают наружу уже типизированное состояние. В связке с <script setup> типизация пропсов и эмитов получается лаконичной - без громоздких деклараций, которые требовал Options API.

Тормозит ли реактивность Vue 3 при большом объёме данных?

Сама реактивная система тянет десятки тысяч полей спокойно. Тормоза почти всегда в двух местах: тяжёлые внешние объекты вроде инстансов графиков, положенные в глубокий ref вместо shallowRef с markRaw, и рендер огромных таблиц без виртуализации. Уберите эти две причины - и панель с живым обновлением по таймеру работает плавно даже на слабых ноутбуках у менеджеров.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Самозанятый Калинкин Н. А. · работаю с физлицами и юрлицами

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно