Хуки жизненного цикла плагина

Плагин в Vite представляет собой объект с набором хуков, которые вызываются на различных этапах работы dev-сервера, обработки модулей и сборки проекта. Архитектура плагинов Vite основана на модели плагинов Rollup, однако дополняется собственными механизмами, связанными с HMR, dev-сервером и трансформацией модулей в режиме разработки.

Каждый хук отвечает за строго определённый этап:

  • инициализация конфигурации;
  • запуск dev-сервера;
  • разрешение путей модулей;
  • загрузка файлов;
  • трансформация кода;
  • генерация чанков;
  • завершение сборки.

Порядок вызова хуков критически важен. Ошибка в понимании жизненного цикла приводит к конфликтам между плагинами, дублирующимся трансформациям, некорректному HMR и нестабильной сборке.


Общая схема жизненного цикла

Во время работы Vite плагины проходят несколько фаз:

  1. Создание и нормализация конфигурации
  2. Инициализация сервера разработки
  3. Разрешение импортов
  4. Загрузка содержимого модулей
  5. Трансформация исходного кода
  6. Генерация dependency graph
  7. Обработка HMR
  8. Сборка Rollup
  9. Генерация выходных файлов
  10. Завершение процесса

Часть хуков работает только в dev-режиме, часть — только при production build, а некоторые поддерживаются в обоих режимах.


Хук config

Назначение

Хук config вызывается на этапе создания конфигурации Vite до её финальной нормализации.

Используется для:

  • изменения пользовательской конфигурации;
  • добавления alias;
  • изменения define;
  • модификации build options;
  • подключения других настроек.

Сигнатура

config(config, env) {
}

Параметры:

Параметр Описание
config Исходная конфигурация
env Информация о режиме запуска

Пример

export default function myPlugin() {
  return {
    name: 'my-plugin',

    config(config, env) {
      return {
        resolve: {
          alias: {
            '@shared': '/src/shared'
          }
        }
      }
    }
  }
}

Объединение конфигураций

Возвращаемый объект не заменяет конфигурацию полностью. Vite выполняет merge.

config() {
  return {
    define: {
      __DEVTOOLS__: true
    }
  }
}

Использование mode

config(config, { mode }) {
  if (mode === 'production') {
    return {
      build: {
        sourcemap: false
      }
    }
  }
}

Хук configResolved

Назначение

Вызывается после полной обработки конфигурации Vite.

В отличие от config, здесь доступна окончательная конфигурация со всеми merge-операциями.


Основные задачи

  • получение итогового root;
  • чтение aliases;
  • анализ build options;
  • кэширование настроек плагина.

Пример

let resolvedConfig

export default function plugin() {
  return {
    name: 'config-plugin',

    configResolved(config) {
      resolvedConfig = config
    }
  }
}

Доступ к режиму запуска

configResolved(config) {
  console.log(config.command)
}

Возможные значения:

Значение Описание
serve dev-сервер
build production build

Хук configureServer

Назначение

Позволяет получить доступ к dev-серверу Vite.

Работает только в режиме разработки.


Возможности

Через configureServer можно:

  • добавлять middleware;
  • подписываться на websocket;
  • отслеживать HMR;
  • взаимодействовать с module graph;
  • создавать API endpoints.

Пример middleware

configureServer(server) {
  server.middlewares.use((req, res, next) => {
    console.log(req.url)
    next()
  })
}

Добавление API endpoint

configureServer(server) {
  server.middlewares.use('/api/ping', (req, res) => {
    res.setHeader('Content-Type', 'application/json')

    res.end(JSON.stringify({
      success: true
    }))
  })
}

Работа с websocket

configureServer(server) {
  server.ws.on('custom:event', (data) => {
    console.log(data)
  })
}

Отправка событий клиенту

configureServer(server) {
  setInterval(() => {
    server.ws.send({
      type: 'custom',
      event: 'timer',
      data: Date.now()
    })
  }, 1000)
}

Хук configurePreviewServer

Назначение

Аналог configureServer, но работает с preview server.

Используется редко, в основном для кастомизации production preview.


Пример

configurePreviewServer(server) {
  server.middlewares.use((req, res, next) => {
    console.log('preview request')
    next()
  })
}

Хук resolveId

Назначение

Отвечает за разрешение импортов.

Это один из ключевых хуков всей системы плагинов.


Когда вызывается

При каждом:

import ...

или

require(...)

Vite запускает цепочку resolveId.


Пример

resolveId(source) {
  if (source === 'virtual:config') {
    return source
  }
}

Виртуальные модули

Чаще всего resolveId используется для создания виртуальных модулей.

const virtualModuleId = 'virtual:env'
const resolvedVirtualModuleId = '\0virtual:env'

export default function plugin() {
  return {
    name: 'virtual-plugin',

    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    }
  }
}

Почему используется \0

Префикс \0:

  • помечает внутренний виртуальный модуль;
  • исключает стандартный resolver;
  • предотвращает конфликты с файловой системой.

Хук load

Назначение

Позволяет предоставить содержимое модуля вручную.

Обычно используется совместно с resolveId.


Пример виртуального модуля

load(id) {
  if (id === '\0virtual:env') {
    return `
      export const API_URL = 'https://example.com'
    `
  }
}

Генерация модулей

load часто используется для:

  • генерации runtime-конфига;
  • виртуальных CSS;
  • генерации роутов;
  • файловой системы маршрутов;
  • генерации markdown;
  • динамических import maps.

Хук transform

Назначение

Главный хук обработки кода.

Через transform проходят:

  • JavaScript;
  • TypeScript;
  • JSX;
  • TSX;
  • CSS;
  • Vue SFC;
  • Markdown;
  • виртуальные модули.

Сигнатура

transform(code, id) {
}

Простейшая трансформация

transform(code, id) {
  if (id.endsWith('.js')) {
    return code.replace('__DEV__', 'true')
  }
}

Возврат объекта

transform(code) {
  return {
    code: transformedCode,
    map: sourceMap
  }
}

Source Maps

Корректная генерация source maps крайне важна:

return {
  code,
  map: null
}

Фильтрация файлов

Ошибкой считается выполнение тяжёлой обработки для всех модулей.

Правильный подход:

transform(code, id) {
  if (!id.endsWith('.md')) {
    return
  }

  return compileMarkdown(code)
}

Dev и Build режим

transform работает:

  • при каждом запросе модуля в dev;
  • во время production build.

Из-за этого трансформации должны быть:

  • детерминированными;
  • быстрыми;
  • без глобального состояния.

Хук handleHotUpdate

Назначение

Позволяет перехватывать механизм HMR.


Пример

handleHotUpdate(ctx) {
  console.log(ctx.file)
}

Контекст обновления

Поле Описание
file Изменённый файл
modules Затронутые модули
server Экземпляр dev-сервера
timestamp Время изменения

Принудительное обновление

handleHotUpdate(ctx) {
  ctx.server.ws.send({
    type: 'full-reload'
  })
}

Частичное обновление

handleHotUpdate(ctx) {
  return ctx.modules
}

Пример кастомного HMR

handleHotUpdate(ctx) {
  if (ctx.file.endsWith('.txt')) {
    ctx.server.ws.send({
      type: 'custom',
      event: 'text-update'
    })

    return []
  }
}

Хук buildStart

Назначение

Вызывается в начале Rollup build.


Типичные задачи

  • очистка кэша;
  • подготовка ресурсов;
  • анализ входных файлов;
  • инициализация состояния.

Пример

buildStart() {
  console.log('build started')
}

Хук buildEnd

Назначение

Вызывается после завершения build pipeline.


Пример

buildEnd(error) {
  if (error) {
    console.error(error)
  }
}

Хук generateBundle

Назначение

Позволяет модифицировать финальный bundle перед записью файлов.


Сигнатура

generateBundle(options, bundle) {
}

Структура bundle

{
  'index.js': {
    type: 'chunk'
  },

  'style.css': {
    type: 'asset'
  }
}

Добавление файлов

generateBundle() {
  this.emitFile({
    type: 'asset',
    fileName: 'meta.json',
    source: JSON.stringify({
      build: Date.now()
    })
  })
}

Изменение чанков

generateBundle(options, bundle) {
  for (const file in bundle) {
    const chunk = bundle[file]

    if (chunk.type === 'chunk') {
      chunk.code += '\nconsole.log("loaded")'
    }
  }
}

Хук writeBundle

Назначение

Вызывается после записи файлов на диск.


Отличие от generateBundle

Хук Когда вызывается
generateBundle До записи
writeBundle После записи

Пример

writeBundle() {
  console.log('files written')
}

Хук closeBundle

Назначение

Финальный этап жизненного цикла.

Используется для:

  • очистки ресурсов;
  • завершения процессов;
  • закрытия соединений;
  • остановки watcher;
  • удаления временных файлов.

Пример

closeBundle() {
  console.log('bundle closed')
}

Хук transformIndexHtml

Назначение

Специализированный хук обработки HTML.


Пример

transformIndexHtml(html) {
  return html.replace(
    '</head>',
    '<script src="/analytics.js"></script></head>'
  )
}

Возврат объектов

transformIndexHtml() {
  return [
    {
      tag: 'meta',
      attrs: {
        name: 'theme-color',
        content: '#000'
      },
      injectTo: 'head'
    }
  ]
}

Места вставки

Значение Описание
head В конец <head>
head-prepend В начало <head>
body В конец <body>
body-prepend В начало <body>

Порядок выполнения хуков

Последовательность обработки модулей

При импорте файла Vite выполняет:

  1. resolveId
  2. load
  3. transform

Последовательность сборки

Во время build:

  1. config
  2. configResolved
  3. buildStart
  4. resolveId
  5. load
  6. transform
  7. generateBundle
  8. writeBundle
  9. closeBundle

Влияние enforce

Значения

Значение Описание
pre Выполняется раньше
post Выполняется позже

Пример

export default function plugin() {
  return {
    name: 'pre-plugin',
    enforce: 'pre'
  }
}

Приоритет трансформаций

Порядок:

  1. pre
  2. обычные плагины
  3. post

Это особенно важно при:

  • обработке JSX;
  • Markdown;
  • Vue SFC;
  • AST-трансформациях;
  • CSS pipeline.

Асинхронные хуки

Практически все хуки могут быть async.


Пример

async load(id) {
  const result = await fs.promises.readFile(id, 'utf-8')

  return result
}

Контекст плагина

Многие хуки получают доступ к plugin context через this.


Пример

transform(code) {
  this.warn('deprecated api')

  return code
}

Полезные методы

Метод Назначение
this.emitFile() Добавление файлов
this.warn() Предупреждение
this.error() Ошибка
this.resolve() Ручной resolve
this.addWatchFile() Подписка на изменения

this.resolve

Назначение

Позволяет вручную запускать механизм resolve.


Пример

async resolveId(source, importer) {
  const resolved = await this.resolve(source, importer)

  return resolved
}

this.addWatchFile

Назначение

Добавляет файл в watcher.


Пример

load(id) {
  this.addWatchFile('config/theme.json')
}

При изменении файла Vite инициирует обновление.


Условное выполнение хуков

Некоторые плагины работают только в dev или build.


Только dev

apply: 'serve'

Только build

apply: 'build'

Пример

export default function plugin() {
  return {
    name: 'dev-plugin',
    apply: 'serve'
  }
}

Комплексный пример плагина

export default function virtualConfigPlugin() {
  const virtualId = 'virtual:app-config'
  const resolvedVirtualId = '\0virtual:app-config'

  let config

  return {
    name: 'virtual-config-plugin',

    configResolved(resolvedConfig) {
      config = resolvedConfig
    },

    resolveId(id) {
      if (id === virtualId) {
        return resolvedVirtualId
      }
    },

    load(id) {
      if (id === resolvedVirtualId) {
        return `
          export const MODE = '${config.mode}'
          export const BASE = '${config.base}'
        `
      }
    },

    transform(code, id) {
      if (!id.endsWith('.js')) {
        return
      }

      return code.replace('__BUILD_TIME__', Date.now())
    },

    handleHotUpdate(ctx) {
      if (ctx.file.endsWith('.json')) {
        ctx.server.ws.send({
          type: 'full-reload'
        })

        return []
      }
    }
  }
}

Особенности dev-режима

В dev-среде Vite:

  • не выполняет полноценный bundle;
  • трансформирует модули по запросу;
  • использует native ESM;
  • кэширует результат transform;
  • активно использует HMR.

Из-за этого:

  • generateBundle не вызывается;
  • writeBundle отсутствует;
  • closeBundle может не срабатывать так, как ожидается;
  • многие Rollup-хуки игнорируются.

Особенности production build

Во время production build:

  • используется Rollup pipeline;
  • генерируются чанки;
  • запускается tree-shaking;
  • выполняется code splitting;
  • создаются asset files;
  • оптимизируются imports.

В этом режиме жизненный цикл ближе к классическому Rollup.


Совместимость с Rollup

Большинство Rollup-хуков поддерживается напрямую:

Rollup Vite
resolveId поддерживается
load поддерживается
transform поддерживается
generateBundle поддерживается
renderChunk поддерживается
writeBundle поддерживается

Однако Vite добавляет собственные хуки:

  • config
  • configResolved
  • configureServer
  • configurePreviewServer
  • handleHotUpdate
  • transformIndexHtml

Типичные ошибки при работе с хуками

Отсутствие фильтрации файлов

Плохой вариант:

transform(code) {
  return heavyTransform(code)
}

Правильный:

transform(code, id) {
  if (!id.endsWith('.js')) {
    return
  }

  return heavyTransform(code)
}

Мутация shared state

Опасный вариант:

const cache = []

transform(code) {
  cache.push(code)
}

Во время HMR и параллельной обработки это может приводить к трудноуловимым ошибкам.


Бесконечные HMR-циклы

Неверная отправка websocket-событий способна инициировать постоянную перезагрузку страницы.


Игнорирование source maps

Некачественные source maps ломают:

  • debugger;
  • stack trace;
  • devtools;
  • sourcemap chaining.

Тяжёлые synchronous операции

Плохой пример:

transform(code) {
  const huge = fs.readFileSync(...)
}

Такие операции блокируют event loop dev-сервера.


Практика построения сложных плагинов

Крупные плагины обычно разделяют:

  • resolve layer;
  • transform pipeline;
  • cache system;
  • HMR layer;
  • virtual modules;
  • runtime injection;
  • build integration.

Наиболее сложные плагины Vite:

  • обрабатывают AST;
  • генерируют виртуальные модули;
  • внедряют runtime-код;
  • взаимодействуют с module graph;
  • управляют HMR вручную;
  • интегрируются с Rollup output pipeline.

Именно жизненный цикл хуков определяет архитектуру таких систем и позволяет плагинам Vite глубоко вмешиваться практически во все этапы работы сборщика и dev-сервера.