Команда vite build

Система плагинов в Vite является центральным механизмом расширения сборщика. Через плагины реализуются:

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

Архитектурно система плагинов Vite построена поверх плагинной модели Rollup, однако дополнительно включает собственные Vite-специфичные хуки и поведение dev-сервера.


Принцип работы плагинов Vite

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

Каждый модуль проходит через набор хуков:

  1. resolveId
  2. load
  3. transform
  4. generateBundle
  5. другие lifecycle-хуки

Схема обработки выглядит следующим образом:

Импорт модуля
      ↓
resolveId()
      ↓
load()
      ↓
transform()
      ↓
dev server / bundle
      ↓
generateBundle()

Каждый плагин может:

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

Подключение плагинов

Плагины подключаются через массив plugins в конфигурации Vite.

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

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

Плагин обычно представляет собой функцию, возвращающую объект конфигурации плагина.


Структура плагина

Минимальный плагин выглядит так:

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

Полноценный плагин включает хуки жизненного цикла:

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

        config(config) {
            console.log(config)
        },

        resolveId(id) {
            if (id === 'virtual:module') {
                return id
            }
        },

        load(id) {
            if (id === 'virtual:module') {
                return 'export const msg = "Hello"'
            }
        },

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

Поле name

Поле name является обязательным.

{
    name: 'custom-plugin'
}

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

  • в логах;
  • при диагностике;
  • в сообщениях об ошибках;
  • во внутреннем порядке выполнения.

Рекомендуется использовать уникальные префиксы:

name: 'vite-plugin-custom'

Типы плагинов

Runtime plugins

Работают во время dev-сервера.

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

Build plugins

Работают только во время production-сборки.

apply: 'build'

Serve plugins

Работают только при vite dev.

apply: 'serve'

Universal plugins

Работают и в dev, и в build.

{
    name: 'universal-plugin'
}

Свойство apply

Поле apply определяет режим выполнения.

Только dev

{
    apply: 'serve'
}

Только build

{
    apply: 'build'
}

Условное применение

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

Свойство enforce

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

pre

Выполняется раньше остальных.

{
    enforce: 'pre'
}

post

Выполняется после остальных.

{
    enforce: 'post'
}

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

Общий порядок:

pre plugins
↓
normal plugins
↓
post plugins

Внутри каждой группы плагины выполняются последовательно.


Хук config

Позволяет модифицировать конфигурацию Vite.

config(config, env) {
    return {
        define: {
            __APP_VERSION__: '"1.0.0"'
        }
    }
}

Хук configResolved

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

configResolved(resolvedConfig) {
    console.log(resolvedConfig.root)
}

Этот хук часто используется для:

  • анализа конфигурации;
  • сохранения параметров;
  • подготовки внутренних структур.

Хук configureServer

Даёт доступ к dev-серверу.

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

Middleware внутри Vite

Vite использует middleware-подход, аналогичный Express.

Можно создавать собственные обработчики:

configureServer(server) {
    server.middlewares.use('/api/test', (req, res) => {
        res.end('Hello')
    })
}

Хук transformIndexHtml

Позволяет изменять HTML перед отправкой браузеру.

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

Инъекция тегов в HTML

Можно возвращать массив тегов:

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

Хук resolveId

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

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

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

Виртуальный модуль не существует физически на диске.

resolveId(id) {
    if (id === 'virtual:env') {
        return '\0virtual:env'
    }
}

Загрузка содержимого:

load(id) {
    if (id === '\0virtual:env') {
        return `
            export const MODE = 'development'
        `
    }
}

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

import { MODE } from 'virtual:env'

Префикс \0

Символ \0 сообщает Rollup и Vite, что модуль является внутренним виртуальным модулем.

Без него модуль может участвовать в обычном файловом резолвинге.


Хук load

Отвечает за загрузку содержимого модуля.

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

Хук transform

Самый важный хук большинства плагинов.

Позволяет изменять исходный код.

transform(code, id) {
    if (id.endsWith('.js')) {
        return code.replace(/__VERSION__/g, '1.0.0')
    }
}

Возврат SourceMap

Плагин может возвращать sourcemap.

transform(code) {
    return {
        code,
        map: null
    }
}

Асинхронные плагины

Все хуки могут быть асинхронными.

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

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

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

Частая практика — ограничивать обработку по расширению.

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

    return compile(code)
}

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

const filter = /\.jsx?$/

transform(code, id) {
    if (!filter.test(id)) {
        return
    }

    return process(code)
}

Работа с query-параметрами

Vite использует query-суффиксы:

Component.vue?vue&type=script
style.css?inline

Плагин должен учитывать это.

if (id.includes('?inline')) {
    return
}

Разделение path и query

const [path, query] = id.split('?')

Хук handleHotUpdate

Используется для управления HMR.

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

Контекст содержит:

{
    file,
    server,
    modules,
    timestamp,
    read
}

Принудительный HMR

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

Отмена HMR

handleHotUpdate() {
    return []
}

Работа с WebSocket

Vite использует WebSocket для HMR.

Можно отправлять собственные события:

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

Клиент:

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

Хук buildStart

Вызывается перед началом сборки.

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

Хук buildEnd

buildEnd() {
    console.log('build finished')
}

Хук generateBundle

Позволяет изменять выходной bundle.

generateBundle(options, bundle) {
    console.log(bundle)
}

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

generateBundle() {
    this.emitFile({
        type: 'asset',
        fileName: 'meta.json',
        source: JSON.stringify({
            version: '1.0.0'
        })
    })
}

Типы файлов emitFile

asset

{
    type: 'asset'
}

chunk

{
    type: 'chunk'
}

Удаление файлов из bundle

generateBundle(options, bundle) {
    delete bundle['old.js']
}

Изменение содержимого chunk

generateBundle(options, bundle) {
    for (const file of Object.values(bundle)) {
        if (file.type === 'chunk') {
            file.code = file.code.replace(
                /DEBUG/g,
                'false'
            )
        }
    }
}

SSR-плагины

Плагин может анализировать режим SSR.

transform(code, id, options) {
    if (options?.ssr) {
        return processSSR(code)
    }
}

Работа с import.meta.env

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

define: {
    __API_URL__: JSON.stringify(process.env.API_URL)
}

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

import dotenv from 'dotenv'

dotenv.config()

Интеграция с Babel

import { transformAsync } from '@babel/core'

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

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

            const result = await transformAsync(code, {
                presets: ['@babel/preset-env']
            })

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

Интеграция с esbuild

Vite активно использует esbuild для сверхбыстрой трансформации модулей.

Пример прямого использования:

import { transform } from 'esbuild'

async transform(code) {
    const result = await transform(code, {
        loader: 'ts'
    })

    return result
}

Кэширование

Плагины могут самостоятельно реализовывать кэш.

const cache = new Map()

transform(code, id) {
    if (cache.has(id)) {
        return cache.get(id)
    }

    const result = compile(code)

    cache.se t(id, result)

    return result
}

Инвалидация кэша

handleHotUpdate(ctx) {
    cache.delete(ctx.file)
}

Работа с файловой системой

import fs from 'fs/promises'

async load(id) {
    if (id.endsWith('.md')) {
        return await fs.readFile(id, 'utf-8')
    }
}

Markdown-плагин

Простейший markdown-loader:

import fs from 'fs/promises'
import { marked } from 'marked'

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

        async load(id) {
            if (!id.endsWith('.md')) {
                return
            }

            const raw = await fs.readFile(id, 'utf-8')

            const html = marked(raw)

            return `
                export default ${JSON.stringify(html)}
            `
        }
    }
}

Генерация виртуального API

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

        resolveId(id) {
            if (id === 'virtual:api') {
                return '\0virtual:api'
            }
        },

        load(id) {
            if (id === '\0virtual:api') {
                return `
                    export async function getUsers() {
                        return fetch('/api/users')
                    }
                `
            }
        }
    }
}

Плагины и monorepo

В monorepo-проектах плагины часто:

  • анализируют workspace;
  • генерируют alias;
  • синхронизируют tsconfig paths;
  • управляют пакетами.

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

Утилита из Rollup:

import { createFilter } from '@rollup/pluginutils'

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

Применение filter

transform(code, id) {
    if (!filter(id)) {
        return
    }

    return process(code)
}

Ошибки внутри плагинов

Для корректной диагностики используется:

this.error('Compilation failed')

Предупреждения

this.warn('Deprecated API')

Логирование

configureServer(server) {
    server.config.logger.info(
        'Custom plugin enabled'
    )
}

Доступ к watcher

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

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

this.addWatchFile('config/custom.json')

Доступ к mode

config(config, env) {
    console.log(env.mode)
}

Доступ к command

config(config, env) {
    console.log(env.command)
}

Значения:

serve
build

Dev-only логика

apply(config, env) {
    return env.command === 'serve'
}

Build-only логика

apply(config, env) {
    return env.command === 'build'
}

Интеграция с PostCSS

Vite автоматически поддерживает PostCSS.

Можно подключать плагины:

export default {
    css: {
        postcss: {
            plugins: [
                require('autoprefixer')
            ]
        }
    }
}

Интеграция с Tailwind

import tailwindcss from 'tailwindcss'

export default {
    css: {
        postcss: {
            plugins: [
                tailwindcss()
            ]
        }
    }
}

Плагины фреймворков

Наиболее популярные официальные плагины:

Плагин Назначение
@vitejs/plugin-vue Vue
@vitejs/plugin-react React
@vitejs/plugin-legacy Legacy browser support

Плагин React

import react from '@vitejs/plugin-react'

export default {
    plugins: [react()]
}

Плагин Vue

import vue from '@vitejs/plugin-vue'

export default {
    plugins: [vue()]
}

Плагин legacy

import legacy from '@vitejs/plugin-legacy'

export default {
    plugins: [
        legacy({
            targets: ['defaults', 'not IE 11']
        })
    ]
}

Совместимость с Rollup-плагинами

Большинство Rollup-плагинов работает в Vite без изменений.

import replace from '@rollup/plugin-replace'

export default {
    plugins: [
        replace({
            __TEST__: true
        })
    ]
}

Ограничения Rollup-плагинов

Некоторые Rollup-плагины:

  • не поддерживают dev server;
  • не учитывают HMR;
  • не работают с HTML-entry;
  • ориентированы только на production build.

Отладка плагинов

Для диагностики часто используют:

console.log(id)

или:

debugger

Анализ цепочки трансформаций

Полезно логировать:

transform(code, id) {
    console.log('transform:', id)
}

Performance-проблемы

Медленные плагины обычно вызывают:

  • задержки HMR;
  • медленный cold start;
  • долгую сборку;
  • высокий расход памяти.

Оптимизация производительности

Основные подходы:

  • фильтрация файлов;
  • кэширование;
  • минимизация AST-анализа;
  • отказ от лишних RegExp;
  • использование esbuild;
  • асинхронная обработка.

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

Для сложных преобразований используют AST.

Популярные библиотеки:

  • Babel parser;
  • Acorn;
  • ESTree;
  • MagicString.

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

import MagicString from 'magic-string'

transform(code) {
    const s = new MagicString(code)

    s.prepend('const DEV = true;\n')

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

Почему важен SourceMap

Без sourcemap:

  • ломается debugging;
  • появляются неверные stack trace;
  • ухудшается DX.

HMR и границы обновления

Vite отслеживает импорт-граф и определяет:

  • partial reload;
  • module replacement;
  • full reload.

Плагин может влиять на этот процесс через handleHotUpdate.


Внутренний pipeline Vite

Во время dev-режима:

Browser request
↓
Vite dev server
↓
Plugin pipeline
↓
Transformed module
↓
Browser

Во время build:

Entry
↓
Rollup graph
↓
Plugin hooks
↓
Chunks/assets
↓
Dist

Архитектурные рекомендации

Качественный Vite-плагин обычно:

  • изолирует ответственность;
  • минимизирует side effects;
  • поддерживает sourcemap;
  • учитывает SSR;
  • корректно работает с HMR;
  • имеет фильтрацию файлов;
  • избегает глобального состояния;
  • поддерживает async pipeline;
  • совместим с Rollup API;
  • не блокирует event loop.

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

plugin/
├── index.js
├── transform.js
├── runtime.js
├── utils.js
├── cache.js
└── constants.js

Тестирование плагинов

Проверяются:

  • корректность transform;
  • работа HMR;
  • SSR-совместимость;
  • sourcemap;
  • build-output;
  • производительность.

Распространённые проблемы

Двойная трансформация

transform(code, id) {
    if (id.includes('node_modules')) {
        return
    }
}

Потеря sourcemap

return {
    code,
    map
}

Бесконечный HMR

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


Медленный startup

Причины:

  • тяжёлые AST-парсеры;
  • синхронный fs;
  • обработка node_modules;
  • отсутствие кэша.

Экосистема vite-plugin-*

В экосистеме существует множество популярных решений:

  • vite-plugin-pages
  • vite-plugin-pwa
  • vite-plugin-svg-icons
  • vite-plugin-inspect
  • vite-plugin-checker
  • vite-plugin-compression

Они реализуют:

  • файловый роутинг;
  • PWA;
  • SVG-спрайты;
  • инспекцию pipeline;
  • type-checking;
  • gzip/brotli compression.