← Курс/Vue DevTools: отладка приложений#257 из 257+20 XP

Vue DevTools: отладка приложений

Что такое Vue DevTools

**Vue DevTools** — это расширение для браузера (Chrome, Firefox, Edge), которое добавляет вкладку Vue в панель разработчика. Оно позволяет инспектировать компонентное дерево, просматривать и редактировать реактивное состояние в реальном времени, отслеживать события и работать с Pinia.

Установка: **Chrome Web Store** или **Firefox Add-ons** — поиск "Vue.js devtools".

Инспектор компонентов

Вкладка **Components** показывает дерево компонентов вашего приложения. Для каждого компонента доступны:

  • **Props** — пропсы, переданные от родителя (только чтение)
  • **Setup** — все ref, reactive, computed из <script setup> (можно редактировать!)
  • **Emits** — список событий с историей вызовов
  • **Provide/Inject** — переданные и полученные значения
  • Клик на компонент в дереве DevTools выделяет его на странице, и наоборот — иконка "select" позволяет кликнуть на элемент в браузере и сразу найти его компонент.

    Pinia DevTools

    Если приложение использует Pinia, в DevTools появляется вкладка **Pinia**:

    // stores/counter.ts
    import { defineStore } from 'pinia'
    
    export const useCounterStore = defineStore('counter', () => {
      const count = ref(0)
      const doubled = computed(() => count.value * 2)
    
      function increment() {
        count.value++
      }
    
      return { count, doubled, increment }
    })

    В DevTools видно: текущее значение count, вычисляемое doubled, можно вызвать increment() прямо из интерфейса DevTools без кода. Также есть **timeline** — история всех изменений стора с timestamp.

    Timeline и события

    Вкладка **Timeline** записывает хронологию событий:

  • **Component events** — все $emit с аргументами и источником
  • **Pinia mutations** — каждое изменение стора
  • **Router navigations** — переходы с from/to
  • **Performance** — время рендеринга компонентов
  • Это незаменимо для отладки "почему компонент перерендерился" или "откуда пришло это событие".

    Кастомные инспекторы

    Для библиотек и продвинутых сценариев можно добавить собственную вкладку в DevTools:

    // Только в development режиме
    if (import.meta.env.DEV) {
      const { setupDevtoolsPlugin } = await import('@vue/devtools-api')
    
      setupDevtoolsPlugin({
        id: 'my-plugin',
        label: 'Мой плагин',
        app,
      }, (api) => {
        api.addInspector({
          id: 'my-inspector',
          label: 'Мои данные',
          icon: 'storage',
        })
    
        api.on.getInspectorTree((payload) => {
          if (payload.inspectorId === 'my-inspector') {
            payload.rootNodes = [
              { id: 'root', label: 'Состояние' },
            ]
          }
        })
    
        api.on.getInspectorState((payload) => {
          if (payload.inspectorId === 'my-inspector') {
            payload.state = {
              'Данные': [
                { key: 'cacheSize', value: myCache.size },
              ]
            }
          }
        })
      })
    }

    Советы по отладке

    **Проблема: компонент не обновляется.** Откройте компонент в DevTools и посмотрите на состояние. Если данные в Setup обновляются, но шаблон не — проблема в реактивности (возможно, забыли .value или мутируете объект напрямую вместо замены).

    **Проблема: лишние рендеры.** Установите расширение и включите Timeline → Component events. Фильтруйте по имени компонента и смотрите, что вызывает обновления.

    Отладка производительности:

    // Включить детальные предупреждения Vue
    app.config.performance = true  // в development
    
    // Предупреждения о множественных обновлениях
    app.config.warnHandler = (msg, vm, trace) => {
      console.warn(msg, trace)
    }

    DevTools без расширения

    Начиная с Vue 3.4 доступен **standalone DevTools** — отдельное приложение:

    npm install -g @vue/devtools
    vue-devtools

    В приложении добавьте скрипт подключения — удобно для мобильных устройств и окружений, где нельзя установить расширение.

    Примеры

    Симуляция функциональности Vue DevTools: инспекция компонентного дерева, timeline событий и трекинг изменений состояния

    // ============================================
    // Симуляция Vue DevTools
    // ============================================
    // DevTools подключаются к Vue через специальный хук.
    // Здесь мы воспроизводим его логику, чтобы понять принцип.
    
    // ============================================
    // 1. Реестр компонентов (Component Inspector)
    // ============================================
    
    class ComponentRegistry {
      constructor() {
        this._components = new Map()
        this._nextId = 1
      }
    
      register(name, state, props = {}) {
        const id = 'comp-' + this._nextId++
        this._components.set(id, {
          id,
          name,
          state: { ...state },
          props: { ...props },
          children: [],
          parent: null,
        })
        return id
      }
    
      addChild(parentId, childId) {
        const parent = this._components.get(parentId)
        const child = this._components.get(childId)
        if (parent && child) {
          parent.children.push(childId)
          child.parent = parentId
        }
      }
    
      updateState(id, newState) {
        const comp = this._components.get(id)
        if (comp) {
          comp.state = { ...comp.state, ...newState }
        }
      }
    
      inspect(id) {
        const comp = this._components.get(id)
        if (!comp) return null
        return {
          name: comp.name,
          state: comp.state,
          props: comp.props,
          children: comp.children.map(cid => this._components.get(cid)?.name),
          parent: comp.parent ? this._components.get(comp.parent)?.name : null,
        }
      }
    
      // Рисуем дерево компонентов
      printTree(id = null, indent = 0) {
        // Находим корневые компоненты если id не указан
        if (id === null) {
          const roots = [...this._components.values()].filter(c => !c.parent)
          for (const root of roots) this.printTree(root.id, indent)
          return
        }
    
        const comp = this._components.get(id)
        if (!comp) return
    
        const prefix = '  '.repeat(indent)
        const stateStr = Object.entries(comp.state)
          .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
          .join(', ')
        console.log(`${prefix}<${comp.name}> { ${stateStr} }`)
    
        for (const childId of comp.children) {
          this.printTree(childId, indent + 1)
        }
      }
    }
    
    // ============================================
    // 2. Timeline событий
    // ============================================
    
    class DevToolsTimeline {
      constructor() {
        this._events = []
        this._startTime = Date.now()
      }
    
      record(type, source, payload) {
        const event = {
          id: this._events.length + 1,
          time: Date.now() - this._startTime,
          type,
          source,
          payload: JSON.parse(JSON.stringify(payload)),
        }
        this._events.push(event)
        return event
      }
    
      filter(type) {
        return this._events.filter(e => e.type === type)
      }
    
      print(filterType = null) {
        const events = filterType ? this.filter(filterType) : this._events
        console.log(`\n  Timeline [${events.length} событий]${filterType ? ' (фильтр: ' + filterType + ')' : ''}:`)
        for (const e of events) {
          const payloadStr = JSON.stringify(e.payload)
          console.log(`  +${String(e.time).padStart(4)}ms  [${e.type.padEnd(12)}] ${e.source}: ${payloadStr}`)
        }
      }
    }
    
    // ============================================
    // 3. Трекер мутаций (Pinia DevTools)
    // ============================================
    
    function createTrackedStore(name, initialState) {
      const timeline = new DevToolsTimeline()
      let _state = { ...initialState }
      const subscribers = []
    
      const store = {
        get state() { return { ..._state } },
    
        commit(mutationName, updater) {
          const before = { ..._state }
          updater(_state)
          const after = { ..._state }
    
          // Записываем в timeline как DevTools
          timeline.record('pinia', name, {
            mutation: mutationName,
            before,
            after,
            diff: Object.fromEntries(
              Object.entries(after).filter(([k, v]) => v !== before[k])
            ),
          })
    
          for (const sub of subscribers) sub({ ..._state })
        },
    
        subscribe(fn) {
          subscribers.push(fn)
        },
    
        printTimeline() {
          timeline.print('pinia')
        },
      }
    
      return store
    }
    
    // ============================================
    // Демонстрация
    // ============================================
    
    console.log('=== Vue DevTools Симуляция ===')
    
    // Создаём компонентное дерево
    const registry = new ComponentRegistry()
    
    const appId = registry.register('App', { theme: 'light' })
    const headerIdx = registry.register('Header', { isOpen: false }, { title: 'Мой сайт' })
    const mainId = registry.register('Main', {})
    const counterId = registry.register('Counter', { count: 0 }, { step: 1 })
    const listId = registry.register('ProductList', { items: [], loading: true })
    
    registry.addChild(appId, headerIdx)
    registry.addChild(appId, mainId)
    registry.addChild(mainId, counterId)
    registry.addChild(mainId, listId)
    
    console.log('\n--- Дерево компонентов ---')
    registry.printTree()
    
    console.log('\n--- Инспекция Counter ---')
    console.log(registry.inspect(counterId))
    
    // Обновляем состояние (как при клике в приложении)
    registry.updateState(counterId, { count: 3 })
    registry.updateState(listId, { loading: false, items: ['Товар A', 'Товар B'] })
    
    console.log('\n--- После обновлений ---')
    registry.printTree()
    
    // Pinia store с трекингом
    console.log('\n--- Pinia Store (с Timeline) ---')
    const cartStore = createTrackedStore('cart', {
      items: [],
      total: 0,
      coupon: null,
    })
    
    cartStore.commit('addItem', (s) => {
      s.items.push({ id: 1, name: 'Vue Book', price: 500 })
      s.total = 500
    })
    
    cartStore.commit('addItem', (s) => {
      s.items.push({ id: 2, name: 'JS Course', price: 1200 })
      s.total = 1700
    })
    
    cartStore.commit('applyCoupon', (s) => {
      s.coupon = 'VUEJS20'
      s.total = Math.round(s.total * 0.8)
    })
    
    cartStore.printTimeline()
    
    console.log('\nТекущее состояние стора:', cartStore.state)