环境变量是操作系统中用于存储配置信息的动态值,它们可以在进程运行时被访问和修改。在 Node.js 中,环境变量通过 process.env 对象访问。
| 问题 | 传统硬编码方式 | 环境变量方式 |
|---|---|---|
| 配置灵活性 | 修改需重新部署 | 无需重新部署 |
| 安全性 | 敏感信息暴露在代码中 | 敏感信息隔离 |
| 多环境支持 | 维护多份代码 | 同一代码适配多环境 |
| 团队协作 | 配置冲突频发 | 各自独立配置 |
.env 文件加载环境变量到 process.env┌─────────────────────────────────────────────────────────────────┐
│ 环境变量管理架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ .env 文件 │ │ 命令行参数 │ │ 系统环境变量 │ │
│ │ (dotenv) │ │ (cross-env) │ │ (OS) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └───────────────────┼───────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ process.env │ │
│ │ (Node.js 进程) │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 数据库配置 │ │ API 密钥 │ │ 服务端口 │ │
│ │ DB_HOST │ │ API_KEY │ │ PORT │ │
│ │ DB_PORT │ │ SECRET │ │ HOST │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘命令行参数 (cross-env) > 系统环境变量 > .env 文件 (dotenv) > 默认值
(最高优先级) (最低优先级)// process.env 是一个对象,存储所有环境变量
console.log(process.env.NODE_ENV); // 'development' 或 'production'
console.log(process.env.PATH); // 系统路径
console.log(process.env.HOME); // 用户主目录
// 设置环境变量(仅在当前进程有效)
process.env.MY_VAR = 'my_value';
// 删除环境变量
delete process.env.MY_VAR;# macOS/Linux - 临时设置
export NODE_ENV=production
node app.js
# macOS/Linux - 一次性使用
NODE_ENV=production node app.js
# Windows CMD
set NODE_ENV=production
node app.js
# Windows PowerShell
$env:NODE_ENV="production"
node app.js跨平台问题: 不同操作系统设置环境变量的语法不同,这就是需要 cross-env 的原因。
npm install dotenv
# 或
yarn add dotenv
# 或
pnpm add dotenv在项目根目录创建 .env 文件:
# 服务器配置
PORT=3000
HOST=localhost
NODE_ENV=development
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp_dev
DB_USER=admin
DB_PASSWORD=password123
# 第三方服务
API_KEY=your_api_key_here
SECRET_KEY=your_secret_key_here// 方式一:推荐 - 尽早加载
import dotenv from 'dotenv';
dotenv.config();
// 方式二:指定文件路径
dotenv.config({ path: '.env.production' });
// 方式三:使用 Node.js -r 参数(推荐用于生产环境)
// node -r dotenv/config app.jsimport dotenv from 'dotenv';
dotenv.config();
const config = {
port: process.env.PORT || 3000,
host: process.env.HOST || 'localhost',
database: {
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
},
api: {
key: process.env.API_KEY,
secret: process.env.SECRET_KEY,
}
};
console.log(`Server running on ${config.host}:${config.port}`);加载 .env 文件到 process.env。
/**
* @param {Object} options - 配置选项
* @param {string} [options.path='.env'] - .env 文件路径
* @param {string} [options.encoding='utf8'] - 文件编码
* @param {boolean} [options.debug=false] - 启用调试日志
* @param {boolean} [options.override=false] - 是否覆盖已存在的环境变量
* @returns {Object} { parsed: { ... }, error: null } 或 { parsed: undefined, error: Error }
*/
// 基本使用
const result = dotenv.config();
if (result.error) {
throw result.error;
}
console.log(result.parsed); // { PORT: '3000', ... }
// 自定义路径
dotenv.config({ path: '.env.production' });
// 多文件加载(配合 override 让后面的环境特定文件覆盖通用配置)
dotenv.config({ path: '.env' });
dotenv.config({ path: `.env.${process.env.NODE_ENV}`, override: true });解析环境变量字符串。
/**
* @param {string} content - .env 文件内容字符串
* @returns {Object} 解析后的键值对对象
*/
const envConfig = dotenv.parse('PORT=3000\nHOST=localhost');
console.log(envConfig); // { PORT: '3000', HOST: 'localhost' }
// 从 Buffer 解析
const buf = Buffer.from('API_KEY=abc123');
const config = dotenv.parse(buf);
console.log(config); // { API_KEY: 'abc123' }将解析的环境变量填充到目标对象。
/**
* @param {Object} target - 目标对象(通常是 process.env)
* @param {Object} parsed - 解析后的键值对
* @param {Object} [options] - 配置选项
* @param {boolean} [options.override=false] - 是否覆盖已存在的变量
*/
const parsed = { PORT: '4000', HOST: '0.0.0.0' };
dotenv.populate(process.env, parsed, { override: true });| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
path | string | .env | 环境变量文件路径 |
encoding | string | utf8 | 文件编码格式 |
debug | boolean | false | 输出调试信息到控制台 |
override | boolean | false | 是否覆盖已存在的环境变量 |
// config.js
import dotenv from 'dotenv';
import path from 'path';
// 根据 NODE_ENV 加载不同配置文件
const env = process.env.NODE_ENV || 'development';
const envPath = path.resolve(process.cwd(), `.env.${env}`);
// 加载通用配置
dotenv.config({ path: '.env' });
// 加载环境特定配置(覆盖通用配置)
dotenv.config({ path: envPath, override: true });
console.log(`Loaded config for ${env} environment`);# 项目结构
.env # 通用配置
.env.development # 开发环境配置
.env.test # 测试环境配置
.env.production # 生产环境配置# 基本格式:KEY=VALUE
PORT=3000
# 多行值(双引号内可直接换行)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIE...
-----END RSA PRIVATE KEY-----"
# 注释(以 # 开头)
# 这是数据库配置
DB_HOST=localhost
# 空格处理
# VALUE 会被解析为 "value with spaces"
MESSAGE=value with spaces
# 等号在值中
CONNECTION_STRING=postgresql://user:pass@host:5432/db?ssl=true
# 变量展开(需要 dotenv-expand)
DATABASE_URL=postgresql://localhost:5432/mydb
TEST_DATABASE_URL=${DATABASE_URL}_testnpm install dotenv-expandimport dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
const env = dotenv.config();
dotenvExpand.expand(env);# .env 文件
BASE_URL=https://api.example.com
API_ENDPOINT=${BASE_URL}/v1
FULL_URL=${API_ENDPOINT}/usersnpm install --save-dev cross-env
# 或
yarn add --dev cross-env
# 或
pnpm add -D cross-env{
"scripts": {
"start": "cross-env NODE_ENV=production node app.js",
"dev": "cross-env NODE_ENV=development PORT=3000 node app.js",
"test": "cross-env NODE_ENV=test jest",
"build": "cross-env NODE_ENV=production webpack --config webpack.config.js"
}
}# 设置单个环境变量
npx cross-env NODE_ENV=production node app.js
# 设置多个环境变量
npx cross-env NODE_ENV=production PORT=8080 HOST=0.0.0.0 node app.js
# 带引号的值(包含空格)
npx cross-env APP_NAME="My Application" node app.js
# 跨平台设置 PATH
npx cross-env PATH=./bin:$PATH node app.js{
"scripts": {
"start": "cross-env NODE_ENV=production node -r dotenv/config app.js",
"dev": "cross-env NODE_ENV=development node -r dotenv/config app.js",
"test": "cross-env NODE_ENV=test node -r dotenv/config jest"
}
}// app.js
// 通过 -r dotenv/config 已自动加载 .env 文件
console.log('NODE_ENV:', process.env.NODE_ENV); // 来自 cross-env
console.log('DB_HOST:', process.env.DB_HOST); // 来自 .env 文件| 参数 | 说明 | 示例 |
|---|---|---|
VAR=value | 设置环境变量 | cross-env NODE_ENV=production |
-- | 分隔环境变量和命令 | cross-env NODE_ENV=prod -- node app.js |
"value with spaces" | 包含空格的值 | cross-env MSG="Hello World" |
{
"scripts": {
"build:analyze": "cross-env NODE_ENV=production ANALYZE=true webpack",
"build:dev": "cross-env NODE_ENV=development webpack",
"start:prod": "cross-env NODE_ENV=production PORT=80 HOST=0.0.0.0 node app.js",
"test:coverage": "cross-env NODE_ENV=test CI=true jest --coverage"
}
}| 特性 | dotenv | cross-env |
|---|---|---|
| 用途 | 从 .env 文件加载环境变量 | 跨平台设置命令行环境变量 |
| 安装依赖 | dotenv (运行时依赖) | cross-env (开发依赖) |
| 配置方式 | .env 文件 + 应用中调用 | 直接在 npm 脚本或命令行中设置 |
| 跨平台支持 | ✅ 是 | ✅ 是 |
| 持久化存储 | ✅ 是(文件) | ❌ 否(仅运行时) |
| 变量覆盖 | 可配置 | 自动覆盖 |
| 适用场景 | 配置文件加载(所有环境) | 脚本运行时设置环境变量 |
dotenv 工作流程:
┌──────────┐ dotenv.config() ┌──────────────┐
│ .env 文件│ ──────────────────► │ process.env │
└──────────┘ └──────────────┘
│
▼
┌──────────────┐
│ 应用读取配置 │
└──────────────┘
cross-env 工作流程:
┌──────────────┐ cross-env ... ┌──────────────┐
│ npm scripts │ ─────────────► │ process.env │
└──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ 应用执行 │
└──────────────┘┌─────────────────────────────────────────────────────────────┐
│ 使用场景决策树 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 需要管理环境变量? │
│ │ │
│ ├─ 是 ──► 是否需要跨平台? │
│ │ │ │
│ │ ├─ 是 ──► 使用 cross-env │
│ │ │ │
│ │ └─ 否 ──► 是否需要持久化配置? │
│ │ │ │
│ │ ├─ 是 ──► dotenv │
│ │ │ │
│ │ └─ 否 ──► 直接设置 │
│ │ │
│ └─ 推荐组合 ──► dotenv + cross-env │
│ │
└─────────────────────────────────────────────────────────────┘my-project/
├── .env # 通用配置(不提交到 Git)
├── .env.example # 示例配置(提交到 Git)
├── .env.development # 开发环境配置
├── .env.test # 测试环境配置
├── .env.production # 生产环境配置
├── .gitignore # 忽略 .env 文件
├── config/
│ ├── index.js # 配置管理入口
│ └── validate.js # 配置验证
└── package.json// config/index.js
import dotenv from 'dotenv';
import path from 'path';
// 加载环境变量
const nodeEnv = process.env.NODE_ENV || 'development';
dotenv.config({ path: path.resolve(process.cwd(), `.env.${nodeEnv}`) });
// 配置验证
function validateConfig(config) {
const required = ['DB_HOST', 'DB_NAME', 'JWT_SECRET'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`缺少必需的环境变量: ${missing.join(', ')}`);
}
}
// 配置对象
const config = {
env: nodeEnv,
port: parseInt(process.env.PORT || '3000', 10),
host: process.env.HOST || 'localhost',
database: {
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432', 10),
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
password: process.env.REDIS_PASSWORD,
},
logs: {
level: process.env.LOG_LEVEL || 'info',
},
};
// 验证配置
validateConfig(config);
export default config;{
"scripts": {
"start": "cross-env NODE_ENV=production node -r dotenv/config src/app.js",
"dev": "cross-env NODE_ENV=development nodemon -r dotenv/config src/app.js",
"test": "cross-env NODE_ENV=test jest --coverage",
"test:watch": "cross-env NODE_ENV=test jest --watch",
"build": "cross-env NODE_ENV=production webpack"
}
}// src/types/env.d.ts
export interface EnvConfig {
NODE_ENV: 'development' | 'test' | 'production';
PORT?: string;
DB_HOST: string;
DB_PORT?: string;
DB_NAME: string;
DB_USER: string;
DB_PASSWORD: string;
JWT_SECRET: string;
}
declare global {
namespace NodeJS {
interface ProcessEnv extends EnvConfig {}
}
}
export {};// src/config/index.ts
import dotenv from 'dotenv';
import path from 'path';
dotenv.config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV}`)
});
const config = {
port: parseInt(process.env.PORT || '3000', 10),
database: {
host: process.env.DB_HOST!,
port: parseInt(process.env.DB_PORT || '5432', 10),
name: process.env.DB_NAME!,
user: process.env.DB_USER!,
password: process.env.DB_PASSWORD!,
},
jwt: {
secret: process.env.JWT_SECRET!,
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
},
} as const;
export default config;# 环境变量文件
.env
.env.local
.env.*.local
.env.development
.env.test
.env.production
# 保留示例文件
!.env.example# 服务器配置
PORT=3000
HOST=localhost
NODE_ENV=development
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_NAME=your_database_name
DB_USER=your_username
DB_PASSWORD=your_password
# JWT 配置
JWT_SECRET=your_jwt_secret_here
JWT_EXPIRES_IN=7d
# Redis 配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# 第三方 API
API_KEY=your_api_key_here// ❌ 错误:硬编码敏感信息
const dbPassword = 'mypassword123';
// ✅ 正确:从环境变量读取
const dbPassword = process.env.DB_PASSWORD;
// ✅ 更好:验证并设置默认值
const dbPassword = process.env.DB_PASSWORD || (() => {
if (process.env.NODE_ENV === 'development') {
console.warn('警告:使用默认数据库密码');
return 'dev_password';
}
throw new Error('DB_PASSWORD 环境变量未设置');
})();// config/security.js
export function validateProductionConfig() {
if (process.env.NODE_ENV === 'production') {
const checks = [
{ key: 'DB_PASSWORD', message: '数据库密码未设置' },
{ key: 'JWT_SECRET', message: 'JWT 密钥未设置' },
{ key: 'API_KEY', message: 'API 密钥未设置' },
];
const errors = checks
.filter(check => !process.env[check.key])
.map(check => check.message);
if (errors.length > 0) {
throw new Error(`生产环境配置错误:\n${errors.join('\n')}`);
}
// 检查是否使用弱密钥
const weakSecrets = ['password', 'secret', '123456', 'admin'];
if (weakSecrets.some(weak => process.env.JWT_SECRET?.includes(weak))) {
throw new Error('JWT_SECRET 使用了弱密钥,请更换');
}
}
}# Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
# 不复制 .env 文件,通过 docker-compose 或 k8s 注入
CMD ["node", "src/app.js"]# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
env_file:
- .env.production
# 或使用 secrets
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt问题: 配置了 .env 文件但 process.env 中读取不到变量。
解决方案:
// 1. 确保 dotenv.config() 在最前面调用
import dotenv from 'dotenv';
dotenv.config(); // 必须在使用环境变量之前
import express from 'express'; // 在 dotenv 之后导入// 2. 检查文件路径
import dotenv from 'dotenv';
import path from 'path';
const result = dotenv.config({
path: path.resolve(__dirname, '../.env')
});
if (result.error) {
console.error('加载 .env 文件失败:', result.error);
}// 3. 启用调试模式
dotenv.config({ debug: true });问题: process.env.PORT 是字符串,需要转换为数字。
解决方案:
// 数字转换
const port = parseInt(process.env.PORT || '3000', 10);
// 布尔值转换
const debug = process.env.DEBUG === 'true';
// 数组转换
const hosts = (process.env.ALLOWED_HOSTS || '').split(',');
// JSON 对象转换
const config = JSON.parse(process.env.CONFIG_JSON || '{}');
// 封装类型转换函数
function getEnvNumber(key: string, defaultValue: number): number {
const value = process.env[key];
return value ? parseInt(value, 10) : defaultValue;
}
function getEnvBoolean(key: string, defaultValue = false): boolean {
const value = process.env[key];
if (value === undefined) return defaultValue;
return value === 'true' || value === '1';
}
function getEnvArray(key: string, separator = ','): string[] {
const value = process.env[key];
return value ? value.split(separator).filter(Boolean) : [];
}解决方案:
// config/index.js
import dotenv from 'dotenv';
import path from 'path';
const env = process.env.NODE_ENV || 'development';
// 加载优先级:.env.{NODE_ENV}.local > .env.{NODE_ENV} > .env.local > .env
const envFiles = [
`.env.${env}.local`,
`.env.${env}`,
'.env.local',
'.env',
];
envFiles.forEach(file => {
const result = dotenv.config({
path: path.resolve(process.cwd(), file)
});
if (!result.error) {
console.log(`Loaded ${file}`);
}
});解决方案:
// 1. 使用环境变量
const password = process.env.DB_PASSWORD;
// 2. 生产环境不使用 .env 文件
// 通过 CI/CD 或容器编排工具注入
// 3. 使用 Vault 等密钥管理服务
import vault from 'node-vault';
const client = vault({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN,
});
const secret = await client.read('secret/database');
const dbPassword = secret.data.password;
// 4. 加密 .env 文件
// 使用 dotenv-vault 或 sops 等工具优先级:
cross-env 设置的变量 > 系统环境变量 > .env 文件 > 默认值示例:
{
"scripts": {
"start": "cross-env PORT=8080 node -r dotenv/config app.js"
}
}// app.js
console.log(process.env.PORT); // 8080 (cross-env 优先)# .env 文件
PORT=3000最终 PORT 值为 8080,因为 cross-env 设置的变量优先级高于 .env 文件。
解决方案:
// config/validate.js
const requiredEnvVars = [
'DB_HOST',
'DB_NAME',
'JWT_SECRET',
'API_KEY',
];
export function validateEnv() {
const missing = requiredEnvVars.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('❌ 缺少必需的环境变量:');
missing.forEach(key => console.error(` - ${key}`));
console.error('\n请检查 .env 文件或环境变量配置');
process.exit(1);
}
console.log('✅ 环境变量验证通过');
}// app.js
import dotenv from 'dotenv';
dotenv.config();
import { validateEnv } from './config/validate';
validateEnv();
// 启动应用...解决方案:
// jest.config.js
module.exports = {
testEnvironment: 'node',
setupFiles: ['<rootDir>/tests/setup.js'],
};
// tests/setup.js
import dotenv from 'dotenv';
dotenv.config({ path: '.env.test' });
// 或使用 jest 环境变量
process.env.NODE_ENV = 'test';
process.env.DB_NAME = 'test_database';// package.json
{
"scripts": {
"test": "cross-env NODE_ENV=test jest",
"test:coverage": "cross-env NODE_ENV=test jest --coverage"
}
}环境变量管理是 Node.js 应用的核心配置方案。通过合理使用 dotenv 和 cross-env,可以实现:
推荐实践:
.env 文件管理配置.env.example 作为配置模板相关文档: