.env файлы и dotenv-webpack

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

  • development
  • production
  • staging
  • testing

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

  • адрес API;
  • режим логирования;
  • ключи аналитики;
  • URL CDN;
  • feature flags;
  • режимы оптимизации.

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

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

Для решения этой задачи используются .env файлы и библиотека dotenv-webpack.


Что такое .env

.env — текстовый файл с переменными окружения в формате:

API_URL=https://api.example.com
APP_MODE=development
ENABLE_ANALYTICS=false

Каждая строка представляет собой пару:

КЛЮЧ=ЗНАЧЕНИЕ

Такие файлы широко используются:

  • в Node.js;
  • Docker;
  • CI/CD;
  • backend-приложениях;
  • frontend-сборщиках.

Проблема использования process.env в Webpack

В Node.js объект process.env существует глобально.

Пример:

console.log(process.env.NODE_ENV);

Однако браузер не имеет объекта process.

Webpack заменяет обращения к process.env.* во время сборки. Без специальных плагинов переменные окружения внутри frontend-кода работать не будут.


Библиотека dotenv-webpack

Плагин dotenv-webpack автоматически:

  • загружает .env файл;
  • читает переменные;
  • подставляет значения в код;
  • заменяет обращения к process.env.*.

Установка:

npm install dotenv-webpack --save-dev

Базовое подключение

Структура проекта

project/
├── src/
│   └── index.js
├── .env
├── webpack.config.js
└── package.json

Файл .env

API_URL=https://api.site.com
MODE=development

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

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

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

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

console.log(process.env.API_URL);
console.log(process.env.MODE);

После сборки Webpack заменит выражения на строковые литералы:

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

Как работает подстановка переменных

dotenv-webpack выполняет замену на этапе компиляции.

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

  • браузер не читает .env;
  • .env не попадает в runtime;
  • значения становятся частью итогового bundle.

Фактически плагин работает аналогично DefinePlugin.


Важная особенность безопасности

Все переменные, используемые во frontend-коде, становятся доступны пользователю.

Например:

SECRET_KEY=my-secret

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

console.log(process.env.SECRET_KEY);

то значение попадёт в итоговый JS-файл.

Поэтому нельзя хранить во frontend:

  • приватные API-ключи;
  • пароли;
  • токены;
  • секреты backend-сервисов.

Использование нескольких .env файлов

Часто проект содержит разные конфигурации.

Пример:

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

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

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

module.exports = {
    plugins: [
        new Dotenv({
            path: './.env.production'
        })
    ]
};

Разделение development и production

.env.development

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

.env.production

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

webpack.config.js

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

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

    return {
        plugins: [
            new Dotenv({
                path: isProd
                    ? './.env.production'
                    : './.env.development'
            })
        ]
    };
};

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

Можно использовать файл .env.example.

.env.example

API_URL=https://default.api
DEBUG=false

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

new Dotenv({
    safe: true
})

Режим safe проверяет наличие всех обязательных переменных.

Если какая-либо переменная отсутствует — сборка завершится ошибкой.


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

По умолчанию dotenv-webpack читает .env.

Для подключения системных переменных используется:

new Dotenv({
    systemvars: true
})

Пример системной переменной

Linux/macOS:

API_URL=https://prod.api npm run build

Windows CMD:

set API_URL=https://prod.api && npm run build

Приоритет переменных

При использовании systemvars: true:

  1. системные переменные;
  2. .env;
  3. значения по умолчанию.

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

new Dotenv({
    allowEmptyValues: true
})

Позволяет использовать пустые значения:

API_KEY=

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

function App() {
    return (
        <div>
            {process.env.API_URL}
        </div>
    );
}

После сборки строка будет встроена в bundle.


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

export default {
    mounted() {
        console.log(process.env.API_URL);
    }
}

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

Angular чаще использует собственную систему environments, однако Webpack-конфигурации также могут применять dotenv-webpack.


Переменные и tree shaking

Webpack умеет удалять неиспользуемый код.

Пример:

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

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

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

Минификатор удалит недостижимую ветку.


Feature flags

.env часто используется для feature flags.

.env

ENABLE_CHAT=true
ENABLE_ADMIN=false

Код

if (process.env.ENABLE_CHAT === 'true') {
    initChat();
}

Настройка API endpoint

.env

API_URL=https://api.site.com

api.js

export const api = {
    baseURL: process.env.API_URL
};

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

import axios from 'axios';

export default axios.create({
    baseURL: process.env.API_URL
});

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

fetch(`${process.env.API_URL}/users`)

Работа с NODE_ENV

Webpack автоматически поддерживает mode.

webpack --mode production

Однако .env может дополнять поведение:

NODE_ENV=production

Отличие mode от .env

mode

Управляет внутренними оптимизациями Webpack:

  • minification;
  • tree shaking;
  • production optimizations.

.env

Управляет пользовательскими переменными проекта.


Поддержка .env.local

Файл:

.env.local

обычно:

  • не коммитится;
  • содержит локальные настройки разработчика;
  • добавляется в .gitignore.

Пример .gitignore

.env.local
.env.production.local

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

Некоторые проекты используют переменные внутри переменных.

.env

HOST=localhost
PORT=3000
API_URL=http://${HOST}:${PORT}

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

new Dotenv({
    expand: true
})

Результат

process.env.API_URL

будет заменён на:

"http://localhost:3000"

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

Подключение файла со значениями по умолчанию:

new Dotenv({
    defaults: true
})

.env.defaults

API_URL=https://default.api
DEBUG=false

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

new Dotenv({
    silent: true
})

Отключает вывод предупреждений.


Проверка существования переменной

if (!process.env.API_URL) {
    throw new Error('API_URL not found');
}

Централизованная конфигурация

config.js

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

Преимущества централизованного подхода

  • единая точка конфигурации;
  • упрощение тестирования;
  • уменьшение дублирования;
  • удобная типизация;
  • простая миграция.

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

Все значения из .env являются строками.

.env

PORT=3000
DEBUG=true

Код

const port = Number(process.env.PORT);

const debug = process.env.DEBUG === 'true';

Типичные ошибки

Отсутствует new Dotenv()

Ошибка:

process is not defined

или:

undefined

Неправильный путь

new Dotenv({
    path: './config/.env'
})

Использование переменной без перезапуска dev server

Webpack Dev Server может не подхватить изменения .env автоматически.

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

npm run dev

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

Нельзя:

DB_PASSWORD=123456
JWT_SECRET=secret

во frontend-проекте.


Сравнение dotenv-webpack и DefinePlugin

dotenv-webpack

Преимущества:

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

DefinePlugin

Требует ручного описания:

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

Главное отличие

dotenv-webpack автоматизирует работу с .env, а DefinePlugin является низкоуровневым механизмом замены констант.


Совместное использование с cross-env

Установка:

npm install cross-env --save-dev

package.json

{
    "scripts": {
        "build": "cross-env NODE_ENV=production webpack"
    }
}

Почему используется cross-env

Windows и Unix-системы имеют разные способы задания переменных окружения.

cross-env делает команды кроссплатформенными.


Использование dotenv-webpack в монорепозиториях

В monorepo .env может располагаться:

root/
├── packages/
│   ├── app/
│   └── admin/
└── .env

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

new Dotenv({
    path: '../. ./.env'
})

Производительность

dotenv-webpack практически не влияет на скорость сборки, поскольку:

  • .env файлы имеют маленький размер;
  • чтение выполняется один раз;
  • замена производится на этапе компиляции.

Интеграция с CI/CD

В production .env часто не хранится в репозитории.

Переменные передаются:

  • GitHub Actions;
  • GitLab CI;
  • Jenkins;
  • Docker;
  • Kubernetes.

Пример GitHub Actions

env:
  API_URL: https://api.production.com

Production best practices

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

API_URL=
DEBUG=

Исключение секретов из Git

.env
.env.local

Разделение окружений

.env.development
.env.staging
.env.production

Централизация конфигурации

export const ENV = {
    API_URL: process.env.API_URL
};

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

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

src/
├── config/
│   ├── env.js
│   ├── api.js
│   └── featureFlags.js

Пример env.js

export const env = {
    mode: process.env.NODE_ENV,
    apiUrl: process.env.API_URL,
    analytics: process.env.ENABLE_ANALYTICS === 'true'
};

Проверка обязательных переменных

const required = [
    'API_URL',
    'NODE_ENV'
];

required.forEach(key => {
    if (!process.env[key]) {
        throw new Error(`${key} is required`);
    }
});

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

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

Например:

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

В таких случаях используют:

  • JSON-конфиги;
  • window.CONFIG;
  • backend endpoint;
  • server-side rendering.

.env ориентирован прежде всего на compile-time конфигурацию.