DefinePlugin и process.env

DefinePlugin — встроенный плагин Webpack, предназначенный для подстановки констант во время сборки. Чаще всего используется для:

  • передачи переменных окружения;
  • переключения между production и development режимами;
  • удаления отладочного кода;
  • активации feature flags;
  • условной компиляции;
  • внедрения конфигурации приложения.

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

Базовый пример:

const webpack = require('webpack');

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

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

console.log(API_URL);

превратится в:

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

Особенности работы DefinePlugin

DefinePlugin не создаёт настоящие переменные времени выполнения. Он заменяет код ещё до запуска JavaScript.

Это означает:

if (DEBUG) {
    console.log('debug');
}

может быть преобразовано в:

if (false) {
    console.log('debug');
}

После этого минификатор (TerserPlugin) способен полностью удалить мёртвый код.


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

Webpack уже содержит DefinePlugin.

const webpack = require('webpack');

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

plugins: [
    new webpack.DefinePlugin({
        VERSION: JSON.stringify('1.0.0')
    })
]

Почему нужен JSON.stringify

DefinePlugin вставляет код буквально.

Ошибка:

new webpack.DefinePlugin({
    APP_NAME: 'My App'
});

Webpack воспримет My App как JavaScript-код.

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

new webpack.DefinePlugin({
    APP_NAME: JSON.stringify('My App')
});

Результат:

const title = "My App";

Подстановка строк

new webpack.DefinePlugin({
    SITE_NAME: JSON.stringify('NWEB')
});

Исходный код:

console.log(SITE_NAME);

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

console.log("NWEB");

Подстановка чисел

new webpack.DefinePlugin({
    PORT: 3000
});

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

console.log(PORT);

Результат:

console.log(3000);

Подстановка boolean-значений

new webpack.DefinePlugin({
    IS_DEV: true
});

Пример:

if (IS_DEV) {
    console.log('development');
}

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

Одна из главных задач DefinePlugin — условное удаление кода.

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

new webpack.DefinePlugin({
    __DEV__: JSON.stringify(false)
});

Код:

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

После минификации:

// код полностью удалён

Такой подход широко используется в:

  • React;
  • Vue;
  • Angular;
  • Redux;
  • MobX;
  • внутренних enterprise-приложениях.

DefinePlugin и process.env

Самый популярный сценарий — внедрение process.env.

Пример:

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

Теперь:

console.log(process.env.NODE_ENV);

становится:

console.log("production");

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

process.env — объект Node.js.

В браузере:

console.log(process.env);

вызывает ошибку:

process is not defined

Webpack решает проблему через compile-time replacement.


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

Чаще всего:

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

Запуск:

NODE_ENV=production webpack

или:

cross-env NODE_ENV=production webpack

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

if (process.env.NODE_ENV === 'production') {
    enableAnalytics();
}

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

if ("production" === 'production') {
    enableAnalytics();
}

Минификатор упростит выражение.


Условная компиляция

DefinePlugin позволяет создавать compile-time ветвление.

Пример:

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

Production-сборка:

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

После оптимизации:

// код удалён

DefinePlugin и tree shaking

Webpack способен удалять недостижимые ветки.

Пример:

if (FEATURE_X) {
    import('./feature-x');
}

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

new webpack.DefinePlugin({
    FEATURE_X: false
});

В production код feature-x может не попасть в bundle.


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

Допустим:

new webpack.DefinePlugin({
    'process.env': JSON.stringify({
        NODE_ENV: 'production',
        API_URL: 'https://api.site.com'
    })
});

Теперь:

console.log(process.env.API_URL);

Работает корректно.

Но такой способ опасен.


Почему нельзя заменять весь process.env

Плохая практика:

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

Проблемы:

  • в bundle попадут секреты;
  • утечка API-ключей;
  • увеличение размера сборки;
  • раскрытие внутренней инфраструктуры;
  • случайная публикация паролей.

Безопасный подход

Нужно передавать только необходимые переменные.

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

Работа с dotenv

Часто используется библиотека dotenv.

Установка:

npm install dotenv

Файл:

API_URL=https://api.site.com
APP_NAME=NWEB

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

require('dotenv').config();

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

Автоматическое формирование env-объекта

Пример:

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

dotenv.config();

const envKeys = Object.keys(process.env).reduce((prev, next) => {
    prev[`process.env.${next}`] = JSON.stringify(process.env[next]);

    return prev;
}, {});

module.exports = {
    plugins: [
        new webpack.DefinePlugin(envKeys)
    ]
};

Фильтрация переменных окружения

Лучше передавать только публичные переменные.

Например:

PUBLIC_API_URL=https://api.site.com
PUBLIC_THEME=dark

DB_PASSWORD=secret
JWT_SECRET=token

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

const envKeys = Object.keys(process.env)
    .filter(key => key.startsWith('PUBLIC_'))
    .reduce((prev, key) => {
        prev[`process.env.${key}`] = JSON.stringify(process.env[key]);

        return prev;
    }, {});

DefinePlugin и режимы сборки

Часто конфигурация зависит от режима.

module.exports = (env, argv) => {
    const isProd = argv.mode === 'production';

    return {
        plugins: [
            new webpack.DefinePlugin({
                'process.env.NODE_ENV': JSON.stringify(
                    isProd ? 'production' : 'development'
                )
            })
        ]
    };
};

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

Feature flags позволяют включать функции без изменения кода.

new webpack.DefinePlugin({
    ENABLE_CHAT: true,
    ENABLE_ADMIN: false
});

Пример:

if (ENABLE_CHAT) {
    initChat();
}

Глобальные константы приложения

new webpack.DefinePlugin({
    APP_VERSION: JSON.stringify('2.5.1'),
    BUILD_DATE: JSON.stringify(new Date().toISOString())
});

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

console.log(APP_VERSION);
console.log(BUILD_DATE);

Интеграция с package.json

const packageJson = require('./package.json');

new webpack.DefinePlugin({
    APP_VERSION: JSON.stringify(packageJson.version)
});

Использование 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)
});

DefinePlugin и React

React использует process.env.NODE_ENV.

В production:

if (process.env.NODE_ENV !== 'production') {
    warning();
}

Webpack заменяет значение, после чего React удаляет dev-only код.

Это уменьшает размер bundle.


DefinePlugin и Vue

Vue также использует compile-time flags.

Пример:

new webpack.DefinePlugin({
    __VUE_OPTIONS_API__: true,
    __VUE_PROD_DEVTOOLS__: false
});

Отличие DefinePlugin от EnvironmentPlugin

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

Пример:

new webpack.EnvironmentPlugin([
    'NODE_ENV',
    'API_URL'
]);

Эквивалентно:

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

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

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

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

Отсутствие JSON.stringify

Ошибка:

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

Результат:

https://api.site.com

Webpack интерпретирует это как код.


Конфликт имён

Плохо:

new webpack.DefinePlugin({
    window: {}
});

Можно случайно сломать глобальные объекты браузера.


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

DefinePlugin работает только во время сборки.

Нельзя:

fetch(process.env.API_URL);

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


Runtime env против compile-time env

DefinePlugin создаёт compile-time конфигурацию.

Это означает:

  • значение фиксируется при build;
  • переменная не меняется после деплоя;
  • для обновления нужен rebuild.

Runtime-конфигурация обычно реализуется через:

  • JSON-файлы;
  • server-side rendering;
  • window.CONFIG;
  • API-запросы;
  • HTML template injection.

DefinePlugin и SSR

В SSR важно разделять:

  • серверные env;
  • клиентские env.

Нельзя передавать в браузер:

  • пароли;
  • токены;
  • приватные ключи;
  • database credentials.

Безопасно:

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

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

Webpack поддерживает специальный синтаксис:

new webpack.DefinePlugin({
    'typeof window': JSON.stringify('object')
});

Редкий, но полезный сценарий.


DefinePlugin и оптимизация bundle

Плагин помогает:

  • удалять dev-код;
  • уменьшать размер bundle;
  • оптимизировать tree shaking;
  • исключать feature-модули;
  • убирать debug utilities;
  • отключать логирование.

Практическая production-конфигурация

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

dotenv.config();

module.exports = {
    mode: 'production',

    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify('production'),

            'process.env.API_URL': JSON.stringify(
                process.env.API_URL
            ),

            'process.env.APP_VERSION': JSON.stringify(
                process.env.npm_package_version
            ),

            __DEV__: false,

            ENABLE_ANALYTICS: true
        })
    ]
};

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

if (__DEV__) {
    console.log('debug mode');
}

if (ENABLE_ANALYTICS) {
    analytics.init();
}

fetch(`${process.env.API_URL}/users`)
    .then(response => response.json())
    .then(console.log);

console.log(process.env.APP_VERSION);

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

Распространённая структура:

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

Каждый файл содержит собственные значения:

API_URL=https://dev-api.site.com

или:

API_URL=https://prod-api.site.com

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

npm install dotenv-flow

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

require('dotenv-flow').config();

dotenv-flow автоматически подгружает нужный env-файл в зависимости от режима.


Проверка отсутствующих переменных

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

Типизация env-переменных

В TypeScript:

declare namespace NodeJS {
    interface ProcessEnv {
        API_URL: string;
        NODE_ENV: 'development' | 'production';
    }
}

DefinePlugin в monorepo

В monorepo env-переменные часто централизуются:

/packages
/apps
/config

Общий env-loader:

const sharedEnv = require('../. ./config/env');

Масштабируемый env-helper

function getClientEnvironment(prefix) {
    return Object.keys(process.env)
        .filter(key => key.startsWith(prefix))
        .reduce((env, key) => {
            env[`process.env.${key}`] = JSON.stringify(process.env[key]);

            return env;
        }, {});
}

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

new webpack.DefinePlugin(
    getClientEnvironment('PUBLIC_')
);

Основные правила безопасного использования

  • не передавать весь process.env;
  • использовать префиксы публичных переменных;
  • не хранить секреты в frontend;
  • применять JSON.stringify для строк;
  • удалять debug-код через compile-time flags;
  • разделять runtime и compile-time конфигурацию;
  • использовать разные env-файлы для окружений;
  • проверять наличие обязательных переменных;
  • минимизировать количество глобальных define-констант.