Плагины с учётом окружения

В крупных проектах на базе Vite нередко возникает необходимость изменять поведение сборки в зависимости от текущего окружения. Разработка, тестирование, production-сборка, SSR, preview-режим, staging — каждое окружение может требовать собственных плагинов, отдельных настроек или различной конфигурации уже существующих расширений.

Механизм условного подключения плагинов позволяет:

  • уменьшать размер production-сборки;
  • отключать отладочные инструменты;
  • подключать mock-серверы только в development;
  • активировать анализаторы bundle исключительно при production build;
  • разделять клиентские и серверные плагины;
  • изменять трансформации кода;
  • подключать разные версии оптимизации.

Основы условного подключения плагинов

Конфигурация vite.config.js или vite.config.ts может экспортировать не объект, а функцию. Эта функция получает информацию о текущем режиме запуска.

Пример:

import { defineConfig } from 'vite'

export default defineConfig(({ command, mode }) => {
    console.log(command)
    console.log(mode)

    return {
        plugins: []
    }
})

Параметры:

Параметр Описание
command Тип запуска: serve или build
mode Активный режим окружения
isSsrBuild Флаг SSR-сборки
isPreview Режим preview

Разница между command и mode

Многие разработчики смешивают эти понятия, хотя они решают разные задачи.

command

Определяет сам сценарий запуска:

vite
vite build

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

command === 'serve'
command === 'build'

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

plugins: command === 'serve'
    ? [devPlugin()]
    : [prodPlugin()]

mode

Mode — логическое окружение приложения.

Примеры:

vite --mode development
vite build --mode production
vite build --mode staging

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

if (mode === 'production') {
    // production-конфигурация
}

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

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

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

export default defineConfig(({ command }) => {
    return {
        plugins: [
            vue(),

            command === 'serve'
                ? checker({
                    typescript: true
                })
                : null
        ]
    }
})

Фильтрация null-значений

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

Стандартный подход:

plugins: [
    isDev ? devPlugin() : null
].filter(Boolean)

Полный пример:

export default defineConfig(({ command }) => {
    const isDev = command === 'serve'

    return {
        plugins: [
            vue(),

            isDev
                ? devOnlyPlugin()
                : null
        ].filter(Boolean)
    }
})

Использование mode для подключения плагинов

Production-only плагины

export default defineConfig(({ mode }) => {
    return {
        plugins: [
            mode === 'production'
                ? compressionPlugin()
                : null
        ].filter(Boolean)
    }
})

Staging-конфигурация

export default defineConfig(({ mode }) => {
    return {
        plugins: [
            mode === 'staging'
                ? debugPlugin()
                : null
        ].filter(Boolean)
    }
})

Использование переменных окружения

Плагины могут подключаться через .env-переменные.

env-файлы

VITE_ENABLE_ANALYZER=true

Загрузка env

import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {
    const env = loadEnv(mode, process.cwd())

    return {
        plugins: [
            env.VITE_ENABLE_ANALYZER === 'true'
                ? analyzerPlugin()
                : null
        ].filter(Boolean)
    }
})

Плагины для production-сборки

Production-режим обычно содержит:

  • минификацию;
  • компрессию;
  • анализ bundle;
  • удаление debug-кода;
  • оптимизацию ассетов.

Подключение gzip-сжатия

import compression from 'vite-plugin-compression'

export default defineConfig(({ mode }) => {
    const isProd = mode === 'production'

    return {
        plugins: [
            isProd
                ? compression({
                    algorithm: 'gzip'
                })
                : null
        ].filter(Boolean)
    }
})

Анализ bundle

import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig(({ mode }) => {
    return {
        plugins: [
            mode === 'production'
                ? visualizer({
                    open: true
                })
                : null
        ].filter(Boolean)
    }
})

Development-only плагины

Во время разработки активно используются:

  • инспекторы компонентов;
  • mock API;
  • проверка типов;
  • live debugging;
  • HMR-инструменты.

Подключение mock-сервера

import { viteMockServe } from 'vite-plugin-mock'

export default defineConfig(({ command }) => {
    const isDev = command === 'serve'

    return {
        plugins: [
            isDev
                ? viteMockServe({
                    mockPath: 'mock'
                })
                : null
        ].filter(Boolean)
    }
})

SSR-плагины и окружение

SSR требует отдельной логики подключения.


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

export default defineConfig(({ isSsrBuild }) => {
    return {
        plugins: [
            isSsrBuild
                ? serverPlugin()
                : clientPlugin()
        ]
    }
})

Разделение клиентских и серверных трансформаций

plugins: [
    isSsrBuild
        ? ssrTransformPlugin()
        : browserTransformPlugin()
]

Использование пользовательских режимов

Создание режима staging

Команда:

vite build --mode staging

Файл:

.env.staging

Конфигурация:

export default defineConfig(({ mode }) => {
    const isStaging = mode === 'staging'

    return {
        plugins: [
            isStaging
                ? stagingPlugin()
                : null
        ].filter(Boolean)
    }
})

Группировка плагинов по окружению

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


Выделение наборов плагинов

function createDevPlugins() {
    return [
        mockPlugin(),
        checkerPlugin()
    ]
}

function createProdPlugins() {
    return [
        compressionPlugin(),
        analyzerPlugin()
    ]
}

Использование в конфиге

export default defineConfig(({ mode }) => {
    const isProd = mode === 'production'

    return {
        plugins: [
            vue(),

            ...(isProd
                ? createProdPlugins()
                : createDevPlugins())
        ]
    }
})

Динамическая загрузка плагинов

Иногда требуется избежать импорта тяжёлых зависимостей в development.


Lazy import

export default defineConfig(async ({ mode }) => {
    const plugins = []

    if (mode === 'production') {
        const { visualizer } =
            await import('rollup-plugin-visualizer')

        plugins.push(
            visualizer()
        )
    }

    return {
        plugins
    }
})

Асинхронная конфигурация

Конфигурация Vite поддерживает async-функции.

export default defineConfig(async () => {
    const data = await loadRemoteConfig()

    return {
        plugins: [
            createPlugin(data)
        ]
    }
})

Условная настройка одного и того же плагина

Необязательно подключать разные плагины. Часто достаточно менять параметры.


Разная конфигурация для development и production

checker({
    overlay: mode === 'development',
    terminal: true
})

Изменение уровня логирования

debugPlugin({
    verbose: mode !== 'production'
})

Комбинирование mode и command

Наиболее гибкий вариант.

export default defineConfig(({ command, mode }) => {
    const isDevServer =
        command === 'serve'

    const isProduction =
        mode === 'production'

    return {
        plugins: [
            isDevServer && devPlugin(),

            isProduction && compressionPlugin()
        ].filter(Boolean)
    }
})

Использование preview-режима

vite preview запускает production-сборку локально.


isPreview

export default defineConfig(({ isPreview }) => {
    return {
        plugins: [
            isPreview
                ? previewPlugin()
                : null
        ].filter(Boolean)
    }
})

Работа с пользовательскими флагами

Иногда mode недостаточно.


Передача флагов через env

VITE_USE_LEGACY=true
const useLegacy =
    env.VITE_USE_LEGACY === 'true'

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

plugins: [
    useLegacy
        ? legacyPlugin()
        : null
].filter(Boolean)

Создание фабрики конфигурации

Крупные проекты обычно используют фабрики.


Структура

export function createPlugins(options) {
    const plugins = []

    if (options.dev) {
        plugins.push(devPlugin())
    }

    if (options.prod) {
        plugins.push(prodPlugin())
    }

    return plugins
}

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

export default defineConfig(({ mode, command }) => {
    return {
        plugins: createPlugins({
            dev: command === 'serve',
            prod: mode === 'production'
        })
    }
})

Типизация условий в TypeScript

Интерфейс параметров

interface PluginOptions {
    dev: boolean
    prod: boolean
    ssr: boolean
}

Типизированная фабрика

function createPlugins(
    options: PluginOptions
) {
    return []
}

Распространённые ошибки

Отсутствие filter(Boolean)

Ошибка:

plugins: [
    isProd ? plugin() : null
]

Некоторые плагины или инструменты анализа могут некорректно обрабатывать null.

Правильно:

plugins: [
    isProd ? plugin() : null
].filter(Boolean)

Путаница между mode и NODE_ENV

mode и process.env.NODE_ENV — не одно и то же.

mode === 'production'

не всегда эквивалентно:

process.env.NODE_ENV === 'production'

В экосистеме Vite рекомендуется использовать именно mode.


Импорт тяжёлых production-плагинов в development

Плохо:

import { visualizer }
from 'rollup-plugin-visualizer'

Даже если плагин не используется, модуль всё равно загружается.

Лучше:

const { visualizer } =
    await import('rollup-plugin-visualizer')

Архитектура конфигурации в крупных проектах

Типичная структура:

config/
├─ plugins/
│  ├─ dev.ts
│  ├─ prod.ts
│  ├─ ssr.ts
│  └─ index.ts
├─ env/
├─ build/
└─ vite/

Пример index.ts

import { createDevPlugins }
from './dev'

import { createProdPlugins }
from './prod'

export function createPlugins(ctx) {
    return [
        ...createDevPlugins(ctx),
        ...createProdPlugins(ctx)
    ]
}

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

Так как Vite использует Rollup внутри production-сборки, правила распространяются и на Rollup-плагины.

build: {
    rollupOptions: {
        plugins: [
            isProd
                ? rollupPlugin()
                : null
        ].filter(Boolean)
    }
}

Условная настройка transform-плагинов

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

function customPlugin(isProd) {
    return {
        name: 'custom-plugin',

        transform(code) {
            if (isProd) {
                return optimize(code)
            }

            return injectDebug(code)
        }
    }
}

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

В monorepo часто используются разные окружения для разных пакетов.

const packageMode =
    process.env.PACKAGE_TARGET
plugins: [
    packageMode === 'admin'
        ? adminPlugin()
        : sitePlugin()
]

Подходы к организации конфигурации

Централизованный

vite.config.ts

Содержит всю логику.

Подходит для небольших проектов.


Модульный

plugins/
builders/
env/
ssr/

Используется в enterprise-проектах.


Гибридный

Главный конфиг содержит только orchestration-логику:

plugins: createPlugins(context)

Вся остальная логика распределяется по модулям.


Практический production-конфиг

import { defineConfig, loadEnv }
from 'vite'

import vue from '@vitejs/plugin-vue'

export default defineConfig(async ({
    mode,
    command,
    isSsrBuild
}) => {
    const env =
        loadEnv(mode, process.cwd())

    const plugins = [vue()]

    if (command === 'serve') {
        const { default: checker }
            = await import(
                'vite-plugin-checker'
            )

        plugins.push(
            checker({
                typescript: true
            })
        )
    }

    if (mode === 'production') {
        const { visualizer }
            = await import(
                'rollup-plugin-visualizer'
            )

        plugins.push(
            visualizer()
        )
    }

    if (isSsrBuild) {
        plugins.push(
            ssrPlugin()
        )
    }

    if (
        env.VITE_ENABLE_GZIP === 'true'
    ) {
        const compression =
            await import(
                'vite-plugin-compression'
            )

        plugins.push(
            compression.default()
        )
    }

    return {
        plugins
    }
})