Использование define для статических замен

В Vite опция define в конфигурации используется для выполнения статических замен во время сборки. Эти замены происходят на этапе трансформации кода и полностью удаляются из рантайма, превращаясь в литеральные значения. Такой подход позволяет внедрять константы, флаги окружения и конфигурационные значения без накладных расходов на выполнение.

Ключевая особенность механизма заключается в том, что define работает на уровне AST-подстановки, а не через обычные переменные JavaScript. Это означает, что выражения подменяются до выполнения кода и не существуют в итоговом бандле как переменные.

Базовая конфигурация define

Опция задаётся в vite.config.js или vite.config.ts и представляет собой объект, где ключи — это идентификаторы, а значения — выражения, которые будут подставлены вместо них.

import { defineConfig } from 'vite'

export default defineConfig({
  define: {
    __APP_VERSION__: '"1.0.0"',
    __DEV__: true,
    __API_URL__: '"https://api.example.com"'
  }
})

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

console.log(__APP_VERSION__)

код преобразуется в:

console.log("1.0.0")

Важно учитывать, что строковые значения должны быть корректно сериализованы, иначе подстановка приведёт к синтаксической ошибке.

Статическая природа подстановок

Все значения в define являются константами времени сборки. Они не могут изменяться в runtime и не поддерживают динамическую логику.

Пример недопустимого ожидания динамики:

define: {
  __TIME__: Date.now()
}

Такое выражение не будет пересчитываться при каждом запуске приложения. Оно вычисляется один раз при сборке и фиксируется в итоговом коде.

Использование JSON.stringify для безопасной подстановки

Одной из распространённых практик является использование JSON.stringify для автоматической корректной сериализации значений.

define: {
  __API__: JSON.stringify('https://api.example.com'),
  __CONFIG__: JSON.stringify({
    retry: 3,
    timeout: 5000
  })
}

Без JSON.stringify сложные структуры могут быть интерпретированы некорректно, особенно строки и объекты.

Подстановка глобальных флагов окружения

Частый сценарий использования — определение режимов приложения:

define: {
  __DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
  __PROD__: JSON.stringify(process.env.NODE_ENV === 'production')
}

Это позволяет полностью исключать ветки кода при минификации:

if (__DEV__) {
  console.log('Development mode')
}

После сборки в production:

if (false) {
  console.log('Development mode')
}

Дальнейшая оптимизация бандлером приводит к удалению мёртвого кода.

Отличие define от import.meta.env

Хотя обе механики связаны с окружением, они принципиально различаются.

import.meta.env:

  • доступен во время выполнения
  • содержит только строки (с ограничениями)
  • управляется Vite автоматически

define:

  • работает на этапе сборки
  • заменяет идентификаторы напрямую в коде
  • может подставлять любые JS-выражения

Пример различия:

console.log(import.meta.env.MODE)

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

console.log(__MODE__)

значение подставляется как литерал ещё до выполнения кода.

Область применения define

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

  • инлайнинг версий приложения
  • внедрение feature flags
  • отключение debug-логики
  • подмена API-эндпоинтов
  • создание условной компиляции кода

Пример feature flag:

define: {
  __FEATURE_NEW_UI__: JSON.stringify(true)
}
if (__FEATURE_NEW_UI__) {
  renderNewUI()
} else {
  renderLegacyUI()
}

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

Особенности подстановки идентификаторов

Vite выполняет замену только для глобальных идентификаторов, поэтому важно избегать конфликтов с локальными переменными.

const __API_URL__ = 'local'

Если такой идентификатор определён в define, он будет заменён даже внутри локальной области видимости, что может привести к неожиданному поведению.

Работа с объектами и вложенными структурами

Подстановка сложных объектов требует сериализации:

define: {
  __APP_CONFIG__: JSON.stringify({
    api: {
      base: '/api',
      timeout: 3000
    },
    features: {
      auth: true
    }
  })
}

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

const config = __APP_CONFIG__

После сборки:

const config = {"api":{"base":"/api","timeout":3000},"features":{"auth":true}}

Влияние на оптимизацию бандла

Статические замены напрямую влияют на tree-shaking и dead code elimination. Поскольку значения становятся литералами, сборщик может:

  • удалять недостижимые ветки условий
  • упрощать выражения
  • исключать импортированные зависимости, связанные с условным кодом

Пример:

if (__DEV__) {
  import('./dev-tools').then(m => m.init())
}

После замены:

if (false) {
  import('./dev-tools').then(m => m.init())
}

Такой код полностью исключается из production-бандла.

Ограничения механизма define

Несмотря на гибкость, механизм имеет ряд ограничений:

  • невозможность динамического вычисления значений
  • отсутствие доступа к runtime-данным браузера
  • риск загрязнения глобального пространства имён
  • необходимость строгой сериализации сложных структур

Также следует учитывать, что подстановка происходит текстово, поэтому синтаксическая корректность значений критична.

Рекомендации по использованию

При проектировании конфигурации через define обычно применяются следующие подходы:

  • использование префиксов для идентификаторов (__APP__, __FEATURE__)
  • обязательная сериализация через JSON.stringify
  • минимизация количества глобальных констант
  • разделение конфигурации по окружениям
  • избегание хранения чувствительных данных

Статические значения должны оставаться действительно статическими, иначе теряется смысл сборочной подстановки и усложняется предсказуемость поведения приложения.