Архитектура плагинов Vite

Система плагинов — центральный элемент архитектуры Vite. Именно через плагины реализуются:

  • обработка модулей;
  • поддержка фреймворков;
  • трансформация исходного кода;
  • работа с CSS;
  • генерация ассетов;
  • SSR;
  • оптимизация сборки;
  • интеграция с экосистемой Rollup.

Архитектура Vite строится вокруг идеи «development server + production bundler», поэтому система плагинов должна одинаково эффективно работать:

  • во время dev-сервера;
  • при production-сборке;
  • при SSR;
  • во время pre-bundling зависимостей.

Главная особенность заключается в том, что Vite не создаёт собственную полностью отдельную экосистему плагинов. Вместо этого он использует совместимость с плагинами Rollup, расширяя их дополнительными хуками и dev-server API.


Основа архитектуры плагинов

Архитектура Vite-плагинов состоит из нескольких уровней:

  1. Rollup-compatible hooks
  2. Vite-specific hooks
  3. Dev server middleware layer
  4. Module graph integration
  5. HMR integration
  6. Transform pipeline

Каждый плагин представляет собой объект с набором хуков.

Простейшая структура:

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

Плагин регистрируется через plugins в vite.config.js:

import { defineConfig } from 'vite'
import myPlugin from './plugins/myPlugin.js'

export default defineConfig({
    plugins: [myPlugin()]
})

Жизненный цикл плагина

Плагин проходит через несколько фаз:

  1. Инициализация
  2. Конфигурация
  3. Создание dev-сервера
  4. Разрешение модулей
  5. Загрузка файлов
  6. Трансформация кода
  7. Генерация bundle
  8. Завершение сборки

Фаза конфигурации

Hook config

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

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

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

Параметры:

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

Объект env:

{
    command: 'serve',
    mode: 'development'
}

Hook configResolved

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

export default function resolvedPlugin() {
    let resolvedConfig

    return {
        name: 'resolved-plugin',

        configResolved(config) {
            resolvedConfig = config
        }
    }
}

Этот хук особенно важен для:

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

Архитектура dev server

Во время запуска vite dev создаётся внутренний HTTP-сервер.

Плагины могут вмешиваться в его работу через специальные hooks.


Hook configureServer

Позволяет получить доступ к объекту dev server.

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

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

Объект server содержит:

Поле Назначение
middlewares Connect middleware
moduleGraph Граф модулей
watcher Chokidar watcher
ws WebSocket HMR
pluginContainer Контейнер плагинов

Middleware layer

Внутри Vite используется стек middleware на основе Connect.

Схема обработки запроса:

HTTP Request
    ↓
Connect Middleware
    ↓
Vite Plugin Pipeline
    ↓
Transform Pipeline
    ↓
Response

Плагины могут:

  • создавать собственные API;
  • перехватывать запросы;
  • изменять заголовки;
  • отдавать виртуальные модули;
  • реализовывать mock-серверы.

Пример API endpoint:

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'
                }))
            })
        }
    }
}

Контейнер плагинов

Внутри Vite создаётся Plugin Container.

Он отвечает за:

  • вызов hook-цепочек;
  • управление transform pipeline;
  • обработку resolve/load;
  • совместимость с Rollup;
  • последовательность выполнения плагинов.

Упрощённая схема:

Plugin Container
    ├── resolveId
    ├── load
    ├── transform
    ├── handleHotUpdate
    └── generateBundle

Разрешение модулей

Hook resolveId

Отвечает за определение пути модуля.

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

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

source — импортируемый путь:

import config from 'virtual:config'

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

Одно из важнейших архитектурных решений Vite.

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

  • не существует на диске;
  • генерируется динамически;
  • создаётся через resolveId + load.

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

export default function virtualPlugin() {
    const virtualId = 'virtual:env'
    const resolvedId = '\0' + virtualId

    return {
        name: 'virtual-plugin',

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

        load(id) {
            if (id === resolvedId) {
                return `
                    export const MODE = "development"
                `
            }
        }
    }
}

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

import { MODE } from 'virtual:env'

Hook load

Используется для загрузки содержимого модуля.

load(id) {
    if (id.endsWith('.txt')) {
        return 'export default "text file"'
    }
}

Хук может:

  • читать файлы;
  • генерировать код;
  • создавать runtime;
  • выполнять компиляцию.

Архитектура transform pipeline

Transform pipeline — ядро обработки модулей.

Каждый модуль проходит через последовательность transform hooks.

Схема:

Source File
    ↓
resolveId
    ↓
load
    ↓
transform #1
    ↓
transform #2
    ↓
transform #3
    ↓
Browser

Hook transform

Главный hook Vite-плагинов.

transform(code, id) {
    if (id.endsWith('.js')) {
        return {
            code: code.replace('__VERSION__', '1.0.0'),
            map: null
        }
    }
}

Возвращаемые значения transform

Hook может возвращать:

return code

или:

return {
    code,
    map
}

Source maps критически важны для:

  • HMR;
  • debugger;
  • stack trace;
  • SSR;
  • production build.

Последовательность transform hooks

Плагины выполняются по порядку.

plugins: [
    pluginA(),
    pluginB(),
    pluginC()
]

Pipeline:

module.js
    ↓
pluginA.transform()
    ↓
pluginB.transform()
    ↓
pluginC.transform()

Результат каждого transform становится входом следующего.


enforce

Порядок можно контролировать через enforce.

{
    name: 'pre-plugin',
    enforce: 'pre'
}

Варианты:

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

Pipeline:

pre plugins
    ↓
normal plugins
    ↓
post plugins

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

Почти все плагины используют фильтрацию файлов.

if (!id.endsWith('.vue')) {
    return
}

Часто используется createFilter из:

@rollup/pluginutils

import { createFilter } from '@rollup/pluginutils'

const filter = createFilter(
    ['**/*.js'],
    ['node_modules/**']
)

Module Graph

Во время dev-режима Vite строит граф зависимостей модулей.

App.vue
 ├── main.js
 ├── styles.css
 └── api.js

Module Graph используется для:

  • HMR;
  • invalidation;
  • кэширования;
  • повторной трансформации.

Работа Module Graph

При изменении файла:

File Changed
    ↓
Watcher Event
    ↓
Module Graph Invalidate
    ↓
Affected Modules
    ↓
HMR Update

HMR-архитектура

Hot Module Replacement — один из ключевых компонентов Vite.

Плагин может управлять HMR через:

handleHotUpdate(ctx) {

}

Hook handleHotUpdate

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

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

Объект ctx:

Поле Назначение
file Изменённый файл
modules Затронутые модули
server Dev server
timestamp Время обновления

WebSocket HMR

Vite использует WebSocket-соединение между браузером и dev server.

Схема:

File Change
    ↓
Watcher
    ↓
Plugin Hook
    ↓
Module Graph
    ↓
WebSocket Message
    ↓
Browser Update

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

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

На клиенте:

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

SSR и плагины

Vite поддерживает SSR через ту же систему плагинов.

Плагин может определять режим:

transform(code, id, options) {
    if (options?.ssr) {
        // SSR transform
    }
}

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

Архитектурно Vite разделён на две части:

Режим Основа
Dev Native ESM + Vite
Build Rollup

Во время production build Vite использует Rollup pipeline.

Это позволяет:

  • использовать Rollup ecosystem;
  • подключать Rollup plugins;
  • переиспользовать existing tooling;
  • уменьшать дублирование архитектуры.

Rollup hooks в Vite

Поддерживаются почти все основные Rollup hooks:

Hook Назначение
resolveId Разрешение модулей
load Загрузка кода
transform Трансформация
buildStart Начало сборки
buildEnd Конец сборки
generateBundle Генерация bundle
writeBundle Запись файлов

Build hooks

buildStart

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

generateBundle

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

generateBundle(options, bundle) {
    for (const file in bundle) {
        console.log(file)
    }
}

writeBundle

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

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

Архитектура pre-bundling

Vite использует отдельный этап dependency optimization.

Для этого применяется esbuild.

Схема:

Dependencies
    ↓
esbuild pre-bundle
    ↓
Optimized cache
    ↓
Dev Server

Это ускоряет:

  • cold start;
  • import analysis;
  • dependency resolution.

Import Analysis

Vite анализирует imports после transform pipeline.

Пример:

import { ref } from 'vue'

После анализа:

import { ref } from '/node_modules/.vite/vue.js'

Архитектура Vue-плагина

Официальный плагин:

@vitejs/plugin-vue

реализует:

  • SFC parsing;
  • template compilation;
  • style processing;
  • HMR boundaries;
  • scoped CSS;
  • SSR transforms.

Pipeline Vue SFC

App.vue
    ↓
parse SFC
    ↓
template transform
    ↓
script transform
    ↓
style transform
    ↓
HMR integration

Архитектура React-плагина

Официальный React-плагин:

@vitejs/plugin-react

добавляет:

  • JSX transform;
  • Fast Refresh;
  • Babel integration;
  • React runtime injection.

Fast Refresh integration

React Fast Refresh работает через:

  1. transform JSX;
  2. inject runtime;
  3. HMR boundary detection;
  4. WebSocket updates.

Plugin Context

Внутри hooks доступен специальный context.

transform(code, id) {
    this.warn('warning')
}

Основные методы:

Метод Назначение
this.warn() Предупреждение
this.error() Ошибка
this.emitFile() Генерация файла
this.resolve() Resolve module

Генерация ассетов

this.emitFile({
    type: 'asset',
    fileName: 'info.txt',
    source: 'generated'
})

Внутренние Vite-плагины

Vite сам использует множество встроенных плагинов:

Плагин Назначение
Alias plugin alias resolution
CSS plugin CSS transforms
Asset plugin static assets
HTML plugin index.html transforms
Import analysis ESM analysis

HTML Transform Hooks

Vite умеет трансформировать index.html.

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

Архитектура CSS plugins

CSS обрабатывается отдельным pipeline.

Поддерживаются:

  • PostCSS;
  • CSS Modules;
  • Sass;
  • Less;
  • Stylus;
  • Lightning CSS.

Pipeline:

CSS File
    ↓
Preprocessor
    ↓
PostCSS
    ↓
CSS Modules
    ↓
HMR
    ↓
Browser

Условное выполнение плагинов

Плагин может работать только в нужном режиме.

apply: 'serve'

или:

apply: 'build'

Также поддерживается функция:

apply(config, env) {
    return env.mode === 'development'
}

Композиция плагинов

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

export default function composedPlugin() {
    return [
        pluginA(),
        pluginB()
    ]
}

Асинхронные hooks

Большинство hooks поддерживают async.

async transform(code, id) {
    const result = await compile(code)

    return {
        code: result.code,
        map: result.map
    }
}

Ошибки и диагностика

Плагин может выбрасывать ошибки:

this.error('Compilation failed')

Vite отображает:

  • overlay в браузере;
  • stack trace;
  • позицию ошибки;
  • source map location.

Производительность плагинов

Наиболее дорогие операции:

Операция Стоимость
AST parsing Высокая
Babel transform Высокая
Source map merge Высокая
File system access Средняя
Regex replace Низкая

Кэширование

Многие плагины используют собственный cache layer.

const cache = new Map()

Типичные стратегии:

  • memoization;
  • AST cache;
  • file hash cache;
  • transform result cache.

AST-трансформации

Сложные плагины работают через AST.

Популярные инструменты:

Инструмент Назначение
Babel JS transform
Acorn Parsing
ESTree AST format
MagicString Code mutation

MagicString

Широко используется внутри Vite-плагинов.

import MagicString from 'magic-string'

const s = new MagicString(code)

s.prepend('const DEV = true')

return {
    code: s.toString(),
    map: s.generateMap()
}

Source Maps

Source maps критически важны в архитектуре Vite.

Без них ломаются:

  • debugger;
  • stack traces;
  • HMR;
  • browser overlays;
  • SSR diagnostics.

Архитектурные преимущества системы плагинов

Унификация dev/build pipeline

Один и тот же плагин работает:

  • в dev;
  • в build;
  • в SSR.

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

Vite получает доступ к тысячам существующих плагинов.


Изоляция ответственности

Каждый плагин отвечает за отдельный pipeline stage:

  • resolve;
  • transform;
  • assets;
  • CSS;
  • HTML;
  • HMR.

Расширяемость

Новые возможности добавляются без изменения ядра Vite.


Высокая производительность

Архитектура минимизирует:

  • полные rebundle;
  • повторные transforms;
  • ненужные invalidation;
  • избыточные file operations.

Типичная структура production-grade плагина

plugin/
├── index.js
├── transform.js
├── runtime.js
├── utils.js
├── cache.js
├── hmr.js
└── types.d.ts

Архитектурные ограничения

Несмотря на гибкость, система плагинов имеет ограничения:

Ограничение Причина
Разный pipeline dev/build Native ESM vs Rollup
HMR complexity Module graph invalidation
Source map overhead merge operations
Plugin ordering issues transform dependencies
SSR edge cases server/client divergence

Эволюция архитектуры Vite

Архитектура плагинов постепенно развивается в направлениях:

  • улучшения SSR;
  • более быстрого transform pipeline;
  • интеграции Rust-инструментов;
  • partial bundling;
  • incremental compilation;
  • унификации dev/build behavior;
  • улучшения plugin container internals.