Кастомный HTML-трансформ через transformIndexHtml

## Механизм `transformIndexHtml` в архитектуре Vite В Vite HTML рассматривается как полноценная точка входа в приложение. В отличие от классических бандлеров, где HTML является лишь шаблоном для вставки итоговых бандлов, Vite обрабатывает HTML через цепочку плагинов, позволяя модифицировать его на уровне разработки и сборки. Центральным механизмом кастомизации HTML выступает хук `transformIndexHtml`. Этот хук является частью плагинной системы Vite и позволяет перехватывать и преобразовывать содержимое HTML до его отправки в браузер или записи в билд-выход. Он интегрирован в жизненный цикл обработки index.html и может работать как в dev-режиме, так и в production-сборке. --- ## Базовая модель обработки HTML в Vite Vite воспринимает `index.html` как модуль, который проходит через несколько стадий: 1. Загрузка исходного HTML 2. Применение плагинов через `transformIndexHtml` 3. Обработка специальных директив (`%ENV%`, `@vite/client`, `type="module"` скрипты) 4. Инжект HMR-клиента в dev-режиме 5. Финальная сборка HTML На каждом этапе плагины могут вмешиваться в структуру документа. --- ## Сигнатура и режимы работы `transformIndexHtml` Хук может быть объявлен в нескольких формах: * синхронная функция * асинхронная функция * объект с методами `transformIndexHtml` и `enforce` ### Базовая сигнатура ```js export default function myPlugin() { return { name: 'my-plugin', transformIndexHtml(html, ctx) { return html } } } ``` ### Асинхронный вариант ```js transformIndexHtml: async (html, ctx) => { const modified = await someAsyncProcessing(html) return modified } ``` --- ## Контекст выполнения (`ctx`) Вторым параметром передаётся контекст, содержащий информацию о текущем этапе обработки: ```ts interface IndexHtmlTransformContext { path: string filename: string server?: ViteDevServer bundle?: Record chunk?: any } ``` ### Основные поля: * **path** — путь к HTML файлу * **filename** — абсолютный путь на файловой системе * **server** — доступен в dev-режиме, позволяет взаимодействовать с Vite dev server * **bundle** — доступен в build-режиме, содержит собранные ассеты * **chunk** — информация о текущем чанке (в некоторых сценариях SSR или продвинутых сборках) Контекст позволяет реализовывать поведение, зависящее от окружения. --- ## Форматы возврата результата `transformIndexHtml` поддерживает несколько типов возврата. ### 1. Строка HTML Самый простой вариант: ```js transformIndexHtml(html) { return html.replace('', '') } ``` ### 2. Массив тегов (HTML transform pipeline) Vite поддерживает структурированное добавление тегов: ```js transformIndexHtml() { return [ { tag: 'script', attrs: { src: '/inject.js' }, injectTo: 'body' } ] } ``` ### 3. Комбинированный подход Можно вернуть как строку, так и массив, но чаще выбирается один формат для предсказуемости пайплайна. --- ## Модель инъекций HTML-тегов Vite использует внутреннюю систему размещения тегов: * `head-prepend` * `head` * `body-prepend` * `body` * `body-append` Пример: ```js transformIndexHtml() { return [ { tag: 'meta', attrs: { charset: 'UTF-8' }, injectTo: 'head-prepend' }, { tag: 'script', attrs: { type: 'module', src: '/main.js' }, injectTo: 'body' } ] } ``` Порядок критичен: `head-prepend` выполняется раньше остальных вставок в ``. --- ## Фазы выполнения transformIndexHtml Хук может выполняться в разных фазах pipeline: ### 1. Pre-transform Перед основными преобразованиями HTML: ```js enforce: 'pre' ``` Используется для: * удаления лишних тегов * подготовки шаблонов * подмены переменных --- ### 2. Normal transform Стандартная стадия. Используется чаще всего. --- ### 3. Post-transform ```js enforce: 'post' ``` Применяется после всех остальных плагинов. Полезно для: * финальной модификации * инжекта аналитики * корректировки уже собранного HTML --- ## Пример полного плагина ```js export default function htmlPlugin() { return { name: 'html-injector', transformIndexHtml: { enforce: 'pre', transform(html, ctx) { const isDev = !!ctx.server if (isDev) { return [ { tag: 'script', attrs: { src: '/dev-only.js' }, injectTo: 'body' } ] } return [ { tag: 'script', attrs: { src: '/analytics.js' }, injectTo: 'head' } ] } } } } ``` --- ## Работа с шаблонами HTML Vite не ограничивает HTML как статический файл. Через `transformIndexHtml` можно реализовать полноценный шаблонизатор. ### Пример подстановки переменных ```js transformIndexHtml(html) { return html.replaceAll('%APP_NAME%', 'My Vite App') } ``` ### Более сложный вариант ```js transformIndexHtml(html, ctx) { const mode = ctx.server ? 'development' : 'production' return html.replace('%MODE%', mode) } ``` --- ## Интеграция с dev-server В режиме разработки `server` в контексте позволяет: * подписываться на WebSocket события * проверять конфигурацию Vite * динамически менять HTML в зависимости от HMR состояния Пример: ```js transformIndexHtml(html, ctx) { if (ctx.server) { ctx.server.ws.on('connection', () => { console.log('HMR connected') }) } return html } ``` --- ## Взаимодействие с build pipeline В production-режиме доступно поле `bundle`, содержащее итоговые ассеты: ```js transformIndexHtml(html, ctx) { if (ctx.bundle) { const scripts = Object.keys(ctx.bundle) .filter(file => file.endsWith('.js')) return html.replace( '', scripts.map(s => ``).join('') + '' ) } return html } ``` --- ## Порядок выполнения нескольких плагинов Если зарегистрировано несколько плагинов, порядок определяется: 1. `enforce: 'pre'` 2. плагины без `enforce` 3. `enforce: 'post'` Внутри каждой группы соблюдается порядок подключения в `vite.config.js`. --- ## Типичные сценарии использования ### Инжект аналитики ```js transformIndexHtml() { return [ { tag: 'script', attrs: { src: 'https://analytics.js' }, injectTo: 'body' } ] } ``` ### Условные скрипты для окружений ```js transformIndexHtml(html, ctx) { const isProd = !ctx.server return html + (isProd ? '' : '') } ``` ### Подмена CDN ресурсов ```js transformIndexHtml(html) { return html.replace( 'https://cdn.example.com/lib.js', '/local/lib.js' ) } ``` --- ## Ограничения и особенности * HTML не проходит через Rollup pipeline напрямую * `transformIndexHtml` не предназначен для тяжёлой логики парсинга DOM * порядок инъекций влияет на итоговую структуру документа * необходимо учитывать различия dev/build режимов --- ## Сочетание с другими хуками Vite `transformIndexHtml` часто используется вместе с: * `transform` (для JS/TS модулей) * `configureServer` (для dev-server логики) * `generateBundle` (для финальной сборки ассетов) Комбинация позволяет реализовывать комплексную модификацию приложения от HTML до финальных чанков.