Доступ к переменным в клиентском коде

Современные frontend-приложения почти всегда используют переменные окружения и конфигурационные значения:

  • URL API
  • режим работы приложения
  • ключи аналитики
  • feature flags
  • параметры сборки
  • номера версий
  • идентификаторы окружений
  • настройки CDN
  • параметры логирования

Webpack не предоставляет браузеру доступ к реальному process.env, как это происходит в Node.js. Во время выполнения клиентского JavaScript в браузере объекта окружения операционной системы не существует.

Поэтому Webpack выполняет другую задачу: подставляет значения в код на этапе сборки.


Почему process.env не работает в браузере

В Node.js переменные окружения доступны напрямую:

console.log(process.env.NODE_ENV);

Но браузер:

  • не имеет доступа к системным переменным
  • не знает о process.env
  • не содержит API операционной системы

Если собрать такой код без специальных настроек, возникнет ошибка:

process is not defined

Webpack решает проблему через:

  • DefinePlugin
  • EnvironmentPlugin
  • dotenv-webpack
  • пользовательские конфиги
  • инъекцию переменных во время build-процесса

Подстановка переменных через DefinePlugin

Базовый принцип работы

DefinePlugin выполняет текстовую замену выражений во время компиляции.

Пример:

new webpack.DefinePlugin({
    API_URL: JSON.stringify('https://api.site.com')
});

Клиентский код:

console.log(API_URL);

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

console.log("https://api.site.com");

Webpack буквально заменяет идентификатор на строковое значение.


Подключение DefinePlugin

const webpack = require('webpack');

module.exports = {
    plugins: [
        new webpack.DefinePlugin({
            API_URL: JSON.stringify('https://api.site.com')
        })
    ]
};

Почему используется JSON.stringify

Очень распространённая ошибка:

new webpack.DefinePlugin({
    API_URL: 'https://api.site.com'
});

Webpack воспримет значение как JavaScript-код:

https://api.site.com

Это приведёт к синтаксической ошибке.

Правильный вариант:

new webpack.DefinePlugin({
    API_URL: JSON.stringify('https://api.site.com')
});

После stringify:

"https://api.site.com"

Использование process.env в клиентском коде

Наиболее популярный подход:

new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify('production')
});

Код приложения:

if (process.env.NODE_ENV === 'production') {
    console.log('Production mode');
}

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

if ("production" === 'production') {
    console.log('Production mode');
}

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


Удаление development-кода

Пример:

if (process.env.NODE_ENV !== 'production') {
    console.log('Debug info');
}

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

if ("production" !== 'production') {
    console.log('Debug info');
}

Минификатор понимает, что условие всегда ложное, и удаляет блок полностью.

Это называется:

  • dead code elimination
  • tree shaking для условных ветвей
  • compile-time optimization

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

Назначение

EnvironmentPlugin — упрощённая оболочка над DefinePlugin.

Пример:

const webpack = require('webpack');

module.exports = {
    plugins: [
        new webpack.EnvironmentPlugin([
            'NODE_ENV',
            'API_URL'
        ])
    ]
};

Webpack возьмёт значения из системного окружения:

NODE_ENV=production
API_URL=https://api.site.com

Доступ в клиентском коде

console.log(process.env.API_URL);

Значения по умолчанию

new webpack.EnvironmentPlugin({
    NODE_ENV: 'development',
    API_URL: 'http://localhost:3000'
})

Если переменная отсутствует в системе, будет использовано значение по умолчанию.


Отличие DefinePlugin от EnvironmentPlugin

Возможность DefinePlugin EnvironmentPlugin
Произвольные глобальные константы Да Нет
Работа с process.env Да Да
Автоматическое чтение env Нет Да
Подходит для любых выражений Да Ограниченно
Гибкость Максимальная Средняя

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

Проблема большого количества переменных

При большом количестве конфигураций неудобно:

  • экспортировать десятки env вручную
  • хранить значения в shell
  • поддерживать разные окружения

Поэтому используются .env файлы.


Формат .env

API_URL=https://api.site.com
APP_VERSION=1.0.0
FEATURE_CHAT=true

Подключение dotenv-webpack

Установка:

npm install dotenv-webpack --save-dev

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

const Dotenv = require('dotenv-webpack');

module.exports = {
    plugins: [
        new Dotenv()
    ]
};

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

console.log(process.env.API_URL);

Webpack автоматически подставит значения из .env.


Что делает dotenv-webpack

Плагин:

  1. читает .env
  2. загружает переменные
  3. создаёт DefinePlugin внутри себя
  4. подставляет значения в клиентский код

Фактически это автоматизация ручной конфигурации.


Разделение env-файлов

Development

API_URL=http://localhost:3000
DEBUG=true

Production

API_URL=https://api.production.com
DEBUG=false

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

Для поддержки множества окружений применяется:

npm install dotenv-flow --save-dev

Структура:

.env
.env.development
.env.production
.env.local

Передача переменных через npm scripts

package.json

{
  "scripts": {
    "dev": "NODE_ENV=development webpack serve",
    "build": "NODE_ENV=production webpack"
  }
}

Проблема Windows

Команда:

NODE_ENV=production webpack

не работает в cmd.exe.


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

Установка:

npm install cross-env --save-dev

Скрипты:

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack serve",
    "build": "cross-env NODE_ENV=production webpack"
  }
}

Проверка режима сборки

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

module.exports = {
    mode: 'production'
};

Webpack автоматически устанавливает:

process.env.NODE_ENV === 'production'

Связь mode и DefinePlugin

Webpack internally создаёт DefinePlugin:

'process.env.NODE_ENV': JSON.stringify('production')

Переопределение NODE_ENV

new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify('development')
})

Если mode: 'production', но вручную задан development, могут возникнуть:

  • неожиданные оптимизации
  • неправильная работа React
  • проблемы с tree shaking
  • отключение production-поведения библиотек

Значения должны совпадать.


Доступ к пользовательским переменным

Пример

APP_THEME=dark
ENABLE_CHAT=true

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

new webpack.EnvironmentPlugin([
    'APP_THEME',
    'ENABLE_CHAT'
])

Код:

console.log(process.env.APP_THEME);

Работа с boolean-значениями

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

Пример:

ENABLE_CHAT=true

В приложении:

console.log(typeof process.env.ENABLE_CHAT);

Результат:

string

Преобразование типов

Boolean

const enabled = process.env.ENABLE_CHAT === 'true';

Number

const timeout = Number(process.env.API_TIMEOUT);

JSON

APP_CONFIG={"theme":"dark"}
const config = JSON.parse(process.env.APP_CONFIG);

Безопасность env-переменных

Критически важное правило

Все переменные, попавшие в клиентский bundle:

  • видны пользователю
  • доступны через DevTools
  • могут быть извлечены из JS-файлов

Нельзя хранить:

  • секретные ключи
  • пароли
  • private tokens
  • database credentials
  • API secret keys

Что можно хранить в клиентских env

Допустимо:

  • публичные URL
  • feature flags
  • client-side identifiers
  • analytics ids
  • публичные API endpoints
  • настройки интерфейса

Ошибочная модель безопасности

Неверно:

SECRET_API_KEY=123456
fetch('/api', {
    headers: {
        Authorization: process.env.SECRET_API_KEY
    }
});

После сборки ключ окажется внутри JS-бандла.


Как проверить утечку

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

grep -r "SECRET_API_KEY" dist

или поиск по bundle-файлам.


Использование runtime-конфигурации

Проблема compile-time переменных

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

Если требуется:

  • менять настройки без rebuild
  • использовать один bundle для разных серверов
  • конфигурировать docker deployment
  • переключать API после сборки

compile-time env недостаточно.


Runtime-конфигурация через global object

config.js

window.APP_CONFIG = {
    API_URL: 'https://api.site.com'
};

Подключение в HTML

<script src="/config.js"></script>
<script src="/bundle.js"></script>

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

console.log(window.APP_CONFIG.API_URL);

Преимущества runtime-конфига

  • изменение без rebuild
  • гибкость deployment
  • один bundle для разных окружений
  • совместимость с Docker
  • динамическая настройка

Комбинированный подход

На практике часто используется смешанная схема:

Compile-time

Через Webpack:

  • NODE_ENV
  • debug flags
  • feature compilation
  • build constants

Runtime

Через window-конфиг:

  • URL backend
  • CDN адреса
  • deployment settings
  • tenant configuration

Feature Flags

Пример

if (process.env.FEATURE_CHAT === 'true') {
    import('./chat');
}

Удаление функциональности

Если:

process.env.FEATURE_CHAT === 'false'

Webpack может полностью исключить модуль из production bundle.


Build Metadata

Версия приложения

new webpack.DefinePlugin({
    APP_VERSION: JSON.stringify(require('./package.json').version)
})

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

console.log(APP_VERSION);

Передача даты сборки

new webpack.DefinePlugin({
    BUILD_DATE: JSON.stringify(new Date().toISOString())
})

Git Commit Hash

const childProcess = require('child_process');

const commitHash = childProcess
    .execSync('git rev-parse --short HEAD')
    .toString()
    .trim();

new webpack.DefinePlugin({
    COMMIT_HASH: JSON.stringify(commitHash)
});

Использование глобальных констант

Пример

new webpack.DefinePlugin({
    __DEV__: JSON.stringify(true),
    __TEST__: JSON.stringify(false)
})

Проверка режима

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

Почему используют двойные подчёркивания

Имена:

__DEV__
__TEST__
__PRODUCTION__

визуально отличаются от обычных переменных и уменьшают вероятность конфликтов.


Ошибки при работе с env

Отсутствие stringify

Ошибка:

DEBUG: true

Правильно:

DEBUG: JSON.stringify(true)

Использование process.env целиком

Плохо:

new webpack.DefinePlugin({
    process: {
        env: process.env
    }
})

Проблемы:

  • огромный bundle
  • утечка секретов
  • нестабильность
  • избыточная замена

Правильный подход

new webpack.DefinePlugin({
    'process.env.API_URL': JSON.stringify(process.env.API_URL)
})

Проверка undefined

Если переменная отсутствует:

console.log(process.env.UNKNOWN_VAR);

результат может быть:

undefined

или ошибка в зависимости от конфигурации.


Защитные проверки

const apiUrl = process.env.API_URL || 'http://localhost:3000';

Валидация env

Пример

if (!process.env.API_URL) {
    throw new Error('API_URL is required');
}

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


Централизованный config-модуль

config.js

export const config = {
    apiUrl: process.env.API_URL,
    debug: process.env.DEBUG === 'true',
    version: process.env.APP_VERSION
};

Преимущества централизации

  • единая точка доступа
  • преобразование типов
  • валидация
  • упрощение поддержки
  • отсутствие дублирования

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

if (process.env.NODE_ENV === 'development') {
    console.log('React dev mode');
}

React активно использует compile-time проверки.

Production build React сильно отличается от development.


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

if (process.env.NODE_ENV !== 'production') {
    Vue.config.devtools = true;
}

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

Angular CLI также внедряет env-переменные на этапе сборки через Webpack-инфраструктуру.


Различие между compile-time и runtime

Тип Когда определяется Можно изменить без rebuild
Compile-time Во время сборки Нет
Runtime Во время запуска Да

Когда использовать compile-time

Подходит для:

  • production/development логики
  • tree shaking
  • удаления debug-кода
  • build constants
  • оптимизации bundle

Когда использовать runtime

Подходит для:

  • deployment settings
  • API endpoints
  • docker configuration
  • multi-tenant systems
  • cloud deployments

Архитектурный подход

В крупных приложениях обычно используются:

  • .env
  • dotenv-webpack
  • runtime config
  • config module
  • feature flags
  • build metadata
  • compile-time optimizations

Все эти механизмы вместе формируют полноценную систему управления конфигурацией frontend-приложения.