Специфические хуки Vite: configureServer, transformIndexHtml, handleHotUpdate

Плагинная система Vite построена поверх архитектуры Rollup, однако сама среда разработки Vite добавляет собственные механизмы обработки HTTP-запросов, HMR, HTML-документов и dev server. Для взаимодействия с этими механизмами существуют специальные хуки, которых нет в стандартном Rollup API.

Наиболее важными среди них являются:

  • configureServer
  • transformIndexHtml
  • handleHotUpdate

Эти хуки используются преимущественно при разработке dev-инструментов, SSR-инфраструктуры, кастомных middleware, систем виртуальных модулей, HMR-интеграций и HTML-трансформаций.


Хук configureServer

Назначение

Хук configureServer предоставляет доступ к внутреннему dev server Vite. Через него можно:

  • подключать middleware;
  • перехватывать HTTP-запросы;
  • получать доступ к WebSocket HMR-серверу;
  • инициировать кастомные события HMR;
  • читать модульный граф;
  • взаимодействовать с файловым watcher;
  • реализовывать собственный backend внутри Vite.

Этот хук работает только во время vite dev.


Базовая структура

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

    configureServer(server) {
      console.log('Vite server started')
    }
  }
}

Параметр server содержит объект ViteDevServer.


Объект ViteDevServer

Основные свойства

middlewares

Connect-приложение, через которое проходят все HTTP-запросы.

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

ws

WebSocket-сервер HMR.

Позволяет отправлять кастомные события клиенту.

configureServer(server) {
  server.ws.send({
    type: 'custom',
    event: 'my:event',
    data: {
      message: 'Hello'
    }
  })
}

На клиенте:

if (import.meta.hot) {
  import.meta.hot.on('my:event', (data) => {
    console.log(data)
  })
}

moduleGraph

Граф модулей Vite.

Позволяет:

  • искать модули;
  • инвалидировать кэш;
  • анализировать зависимости;
  • вручную запускать обновления.
configureServer(server) {
  const module = server.moduleGraph.getModuleById('/src/main.js')

  console.log(module)
}

watcher

Экземпляр chokidar.

Используется для отслеживания файловой системы.

configureServer(server) {
  server.watcher.on('change', (file) => {
    console.log('changed:', file)
  })
}

config

Финальная конфигурация Vite.

configureServer(server) {
  console.log(server.config.root)
}

Добавление middleware

Простое middleware

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

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

        res.end(JSON.stringify({
          message: 'Hello API'
        }))
      })
    }
  }
}

Теперь запрос:

/api/hello

будет обрабатываться внутри Vite.


Middleware и Connect

Vite использует библиотеку Connect, совместимую с Express middleware.

Сигнатура:

(req, res, next)

Переход к следующему middleware:

next()

Фильтрация запросов

configureServer(server) {
  server.middlewares.use((req, res, next) => {
    if (req.url === '/health') {
      res.end('OK')
      return
    }

    next()
  })
}

Работа с JSON API

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

    res.end(JSON.stringify({
      now: Date.now()
    }))
  })
}

Кастомный SSR middleware

configureServer(server) {
  server.middlewares.use(async (req, res, next) => {
    if (!req.url.startsWith('/app')) {
      return next()
    }

    const html = `
      <html>
        <body>
          <h1>SSR Response</h1>
        </body>
      </html>
    `

    res.setHeader('Content-Type', 'text/html')
    res.end(html)
  })
}

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

configureServer может вернуть функцию.

Она будет вызвана после установки встроенных middleware Vite.

configureServer(server) {
  return () => {
    server.middlewares.use((req, res, next) => {
      next()
    })
  }
}

Это важно, если middleware должно работать после HMR, transform pipeline или static serving.


Работа с WebSocket

Отправка HMR-событий

configureServer(server) {
  setInterval(() => {
    server.ws.send({
      type: 'custom',
      event: 'clock:upd ate',
      data: {
        time: new Date().toISOString()
      }
    })
  }, 1000)
}

Клиент:

if (import.meta.hot) {
  import.meta.hot.on('clock:update', (data) => {
    console.log(data.time)
  })
}

Перезагрузка страницы

configureServer(server) {
  server.watcher.on('change', (file) => {
    if (file.endsWith('.txt')) {
      server.ws.send({
        type: 'full-reload'
      })
    }
  })
}

Инвалидация модулей

configureServer(server) {
  server.watcher.on('change', async (file) => {
    const module = server.moduleGraph.getModuleById(file)

    if (module) {
      server.moduleGraph.invalidateModule(module)
    }
  })
}

Хук transformIndexHtml

Назначение

transformIndexHtml предназначен для трансформации HTML-документа перед отправкой браузеру.

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

  • внедрять скрипты;
  • добавлять meta-теги;
  • модифицировать HTML;
  • внедрять preload;
  • реализовывать SSR HTML pipeline;
  • изменять body/head;
  • интегрировать сторонние инструменты.

Хук работает как в dev, так и в build.


Базовая структура

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

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

Аргументы transformIndexHtml

html

Исходный HTML.

transformIndexHtml(html) {
  console.log(html)

  return html
}

ctx

Контекст трансформации.

transformIndexHtml(html, ctx) {
  console.log(ctx.path)

  return html
}

Структура ctx

ctx.path

Текущий URL.

transformIndexHtml(html, ctx) {
  if (ctx.path === '/admin') {
    return html.replace(
      '</head>',
      '<meta name="admin" content="true"></head>'
    )
  }

  return html
}

ctx.server

Dev server.

Доступен только во время vite dev.

transformIndexHtml(html, ctx) {
  if (ctx.server) {
    console.log('dev mode')
  }

  return html
}

ctx.bundle

Bundle Rollup.

Доступен только при build.


ctx.chunk

Текущий HTML chunk.


Вставка тегов через объект

Вместо строковой трансформации можно возвращать объект.

transformIndexHtml() {
  return {
    html: '',
    tags: [
      {
        tag: 'script',
        attrs: {
          src: '/analytics.js'
        },
        injectTo: 'body'
      }
    ]
  }
}

injectTo

Определяет место вставки:

  • head
  • body
  • head-prepend
  • body-prepend

Добавление meta-тегов

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

Добавление inline script

transformIndexHtml() {
  return {
    html: '',
    tags: [
      {
        tag: 'script',
        children: `
          window.__APP_VERSION__ = '1.0.0'
        `,
        injectTo: 'head'
      }
    ]
  }
}

Добавление preload

transformIndexHtml() {
  return {
    html: '',
    tags: [
      {
        tag: 'link',
        attrs: {
          rel: 'preload',
          href: '/fonts/main.woff2',
          as: 'font',
          crossorigin: true
        },
        injectTo: 'head'
      }
    ]
  }
}

Условная HTML-трансформация

transformIndexHtml(html, ctx) {
  if (ctx.path.startsWith('/admin')) {
    return html.replace(
      '</body>',
      '<script src="/admin.js"></script></body>'
    )
  }

  return html
}

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

По умолчанию:

pre -> normal -> post

Указание порядка:

{
  name: 'my-plugin',

  transformIndexHtml: {
    order: 'pre',

    handler(html) {
      return html
    }
  }
}

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

pre

Выполняется до остальных трансформаций.

post

После всех.


Пример полной HTML-инъекции

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

    transformIndexHtml(html) {
      return {
        html,

        tags: [
          {
            tag: 'script',

            attrs: {
              src: '/runtime.js'
            },

            injectTo: 'head'
          },

          {
            tag: 'meta',

            attrs: {
              name: 'build-time',
              content: new Date().toISOString()
            },

            injectTo: 'head'
          }
        ]
      }
    }
  }
}

Хук handleHotUpdate

Назначение

handleHotUpdate позволяет полностью контролировать механизм HMR Vite.

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

  • фильтровать hot updates;
  • инициировать собственные HMR-события;
  • выполнять частичную перезагрузку;
  • изменять набор обновляемых модулей;
  • запускать дополнительную обработку;
  • строить сложную HMR-логику.

Базовая структура

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

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

Объект ctx

file

Изменённый файл.

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

server

Dev server.

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

modules

Модули, связанные с файлом.

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

read

Функция чтения нового содержимого файла.

handleHotUpdate(async ctx) {
  const content = await ctx.read()

  console.log(content)
}

timestamp

Время обновления.


Полная перезагрузка

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

    return []
  }
}

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


Фильтрация модулей

handleHotUpdate(ctx) {
  return ctx.modules.filter((module) => {
    return module.url.includes('client')
  })
}

Обновляться будут только выбранные модули.


Кастомный HMR

handleHotUpdate(ctx) {
  if (ctx.file.endsWith('.data')) {
    ctx.server.ws.send({
      type: 'custom',

      event: 'dat a:update',

      data: {
        file: ctx.file
      }
    })

    return []
  }
}

Клиент:

if (import.meta.hot) {
  import.meta.hot.on('dat a:update', (payload) => {
    console.log(payload)
  })
}

Чтение содержимого файла

handleHotUpdate(async ctx) {
  const content = await ctx.read()

  if (content.includes('reload')) {
    ctx.server.ws.send({
      type: 'full-reload'
    })

    return []
  }
}

Инвалидация зависимых модулей

handleHotUpdate(ctx) {
  const invalidatedModules = new Se t()

  for (const mod of ctx.modules) {
    ctx.server.moduleGraph.invalidateModule(mod)

    invalidatedModules.add(mod)
  }

  return [...invalidatedModules]
}

Совместное использование configureServer и handleHotUpdate

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

Пример:

export default function liveDataPlugin() {
  let server

  return {
    name: 'live-data-plugin',

    configureServer(_server) {
      server = _server
    },

    handleHotUpdate(ctx) {
      if (ctx.file.endsWith('.json')) {
        server.ws.send({
          type: 'custom',

          event: 'json:update',

          data: {
            file: ctx.file
          }
        })

        return []
      }
    }
  }
}

Практический пример: виртуальный runtime

export default function runtimePlugin() {
  let currentTime = Date.now()

  return {
    name: 'runtime-plugin',

    configureServer(server) {
      setInterval(() => {
        currentTime = Date.now()

        server.ws.send({
          type: 'custom',
          event: 'runtime:update',
          data: {
            currentTime
          }
        })
      }, 1000)
    },

    transformIndexHtml(html) {
      return html.replace(
        '</head>',
        `
          <script>
            window.__START_TIME__ = ${currentTime}
          </script>
        </head>
        `
      )
    }
  }
}

Особенности работы хуков в build и dev

Хук Dev Build
configureServer Да Нет
transformIndexHtml Да Да
handleHotUpdate Да Нет

Типизация хуков

Импорт Plugin

import type { Plugin } from 'vite'

Типизированный плагин

import type { Plugin } from 'vite'

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

Типизация configureServer

import type { ViteDevServer } from 'vite'

configureServer(server: ViteDevServer) {

}

Типизация handleHotUpdate

import type { HmrContext } from 'vite'

handleHotUpdate(ctx: HmrContext) {

}

Типизация transformIndexHtml

import type { IndexHtmlTransformContext } from 'vite'

transformIndexHtml(
  html: string,
  ctx: IndexHtmlTransformContext
) {

}

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

configureServer — серверный уровень

Работает поверх HTTP-сервера Vite.

Подходит для:

  • middleware;
  • API;
  • SSR;
  • WebSocket;
  • интеграции backend;
  • файловых watcher.

transformIndexHtml — HTML pipeline

Работает с HTML до отправки браузеру.

Подходит для:

  • инъекций;
  • preload;
  • SSR shell;
  • runtime-конфигурации;
  • meta-тегов;
  • аналитики.

handleHotUpdate — HMR pipeline

Работает во время изменения файлов.

Подходит для:

  • кастомного HMR;
  • selective reload;
  • live content systems;
  • markdown engines;
  • CMS integration;
  • runtime invalidation.

Частые ошибки

Отсутствие return []

Если требуется отключить стандартный HMR:

return []

Без этого Vite продолжит обычное обновление.


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

Этот хук никогда не вызывается во время production build.


Мутация HTML строкой вместо tags API

Строковые replace:

html.replace(...)

могут ломать сложные HTML-трансформации.

Безопаснее использовать:

tags: []

Бесконечные full reload

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

может вызывать циклические обновления при неправильной настройке watcher.


Комбинирование хуков

Специфические хуки Vite обычно работают совместно:

  • configureServer создаёт инфраструктуру;
  • transformIndexHtml подготавливает клиент;
  • handleHotUpdate синхронизирует runtime при изменениях.

Именно эта комбинация делает возможными:

  • live CMS;
  • markdown engines;
  • playground systems;
  • SSR dev runtimes;
  • design systems;
  • визуальные редакторы;
  • dev dashboards;
  • state synchronization systems;
  • runtime overlays;
  • кастомные HMR-протоколы.