Node.js 环境变量管理
概述
环境变量是操作系统中用于存储配置信息的动态值,它们可以在进程运行时被访问和修改。在 Node.js 中,环境变量通过 process.env 对象访问
为什么需要环境变量管理?
| 问题 | 传统硬编码方式 | 环境变量方式 |
|---|---|---|
| 配置灵活性 | 修改需重新部署 | 无需重新部署 |
| 安全性 | 敏感信息暴露在代码中 | 敏感信息隔离 |
| 多环境支持 | 维护多份代码 | 同一代码适配多环境 |
| 团队协作 | 配置冲突频发 | 各自独立配置 |
核心工具介绍
- dotenv:从
.env文件加载环境变量到process.env - cross-env:跨平台设置命令行环境变量
适用场景
- 多环境部署:开发、测试、生产环境配置隔离
- 敏感信息保护:数据库密码、API 密钥等不在代码中硬编码
- 配置热更新:无需修改代码即可调整配置
- CI/CD 集成:自动化流程中的环境配置管理
系统架构
环境变量流转架构
code
┌─────────────────────────────────────────────────────────────────┐
│ 环境变量管理架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ .env 文件 │ │ 命令行参数 │ │ 系统环境变量 │ │
│ │ (dotenv) │ │ (cross-env) │ │ (OS) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └───────────────────┼───────────────────┘ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ process.env │ │
│ │ (Node.js 进程) │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 数据库配置 │ │ API 密钥 │ │ 服务端口 │ │
│ │ DB_HOST │ │ API_KEY │ │ PORT │ │
│ │ DB_PORT │ │ SECRET │ │ HOST │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘优先级顺序
code
命令行参数 (cross-env) > 系统环境变量 > .env 文件 (dotenv) > 默认值
(最高优先级) (最低优先级)环境变量基础
process.env 对象
javascript
// 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;操作系统层面的环境变量
bash
# 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 的原因。
dotenv 详解
安装
bash
npm install dotenv
# 或
yarn add dotenv
# 或
pnpm add dotenv基本使用
1. 创建 .env 文件
在项目根目录创建 .env 文件:
bash
# 服务器配置
PORT=3000
HOST=localhost
NODE_ENV=development
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp_dev
DB_USER=admin
DB_PASS=password123
# 第三方服务
API_KEY=your_api_key_here
SECRET_KEY=your_secret_key_here2. 在应用中加载
javascript
// 方式一:推荐 - 尽早加载
import dotenv from 'dotenv';
dotenv.config();
// 方式二:指定文件路径
dotenv.config({ path: '.env.production' });
// 方式三:使用 Node.js -r 参数(推荐用于生产环境)
// node -r dotenv/config app.js3. 使用环境变量
javascript
import 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_PASS,
},
api: {
key: process.env.API_KEY,
secret: process.env.SECRET_KEY,
}
};
console.log(`Server running on ${config.host}:${config.port}`);API 接口说明
dotenv.config(options)
加载 .env 文件到 process.env。
javascript
/**
* @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' });
// 多文件加载(后面的优先级更高)
dotenv.config({ path: '.env' });
dotenv.config({ path: `.env.${process.env.NODE_ENV}` });dotenv.parse(content)
解析环境变量字符串。
javascript
/**
* @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' }dotenv.populate(target, parsed, options)
将解析的环境变量填充到目标对象。
javascript
/**
* @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 | 是否覆盖已存在的环境变量 |
示例:多环境配置
javascript
// 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`);bash
# 项目结构
.env # 通用配置
.env.development # 开发环境配置
.env.test # 测试环境配置
.env.production # 生产环境配置.env 文件规则
bash
# 基本格式:KEY=VALUE
PORT=3000
# 多行值(使用双引号包裹)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----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}_testdotenv-expand(变量展开)
bash
npm install dotenv-expandjavascript
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
const env = dotenv.config();
dotenvExpand.expand(env);bash
# .env 文件
BASE_URL=https://api.example.com
API_ENDPOINT=${BASE_URL}/v1
FULL_URL=${API_ENDPOINT}/userscross-env 详解
安装
bash
npm install --save-dev cross-env
# 或
yarn add --dev cross-env
# 或
pnpm add -D cross-env基本使用
package.json 脚本配置
json
{
"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"
}
}命令行使用
bash
# 设置单个环境变量
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与 dotenv 结合使用
json
{
"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"
}
}javascript
// 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 文件cross-env 配置参数
| 参数 | 说明 | 示例 |
|---|---|---|
VAR=value | 设置环境变量 | cross-env NODE_ENV=production |
-- | 分隔环境变量和命令 | cross-env NODE_ENV=prod -- node app.js |
"value with spaces" | 包含空格的值 | cross-env MSG="Hello World" |
高级示例
json
{
"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 脚本或命令行中设置 |
| 跨平台支持 | ✅ 是 | ✅ 是 |
| 持久化存储 | ✅ 是(文件) | ❌ 否(仅运行时) |
| 变量覆盖 | 可配置 | 自动覆盖 |
| 适用场景 | 配置文件加载(所有环境) | 脚本运行时设置环境变量 |
工作流程对比
code
dotenv 工作流程:
┌──────────┐ dotenv.config() ┌──────────────┐
│ .env 文件│ ──────────────────► │ process.env │
└──────────┘ └──────────────┘
│
▼
┌──────────────┐
│ 应用读取配置 │
└──────────────┘
cross-env 工作流程:
┌──────────────┐ cross-env ... ┌──────────────┐
│ npm scripts │ ─────────────► │ process.env │
└──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ 应用执行 │
└──────────────┘选择建议
code
┌─────────────────────────────────────────────────────────────┐
│ 使用场景决策树 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 需要管理环境变量? │
│ │ │
│ ├─ 是 ──► 是否需要跨平台? │
│ │ │ │
│ │ ├─ 是 ──► 使用 cross-env │
│ │ │ │
│ │ └─ 否 ──► 是否需要持久化配置? │
│ │ │ │
│ │ ├─ 是 ──► dotenv │
│ │ │ │
│ │ └─ 否 ──► 直接设置 │
│ │ │
│ └─ 推荐组合 ──► dotenv + cross-env │
│ │
└─────────────────────────────────────────────────────────────┘最佳实践
项目结构
code
my-project/
├── .env # 通用配置(不提交到 Git)
├── .env.example # 示例配置(提交到 Git)
├── .env.development # 开发环境配置
├── .env.test # 测试环境配置
├── .env.production # 生产环境配置
├── .gitignore # 忽略 .env 文件
├── config/
│ ├── index.js # 配置管理入口
│ └── validate.js # 配置验证
└── package.json配置管理模块
javascript
// 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 => !config[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;package.json 脚本配置
json
{
"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"
}
}TypeScript 支持
typescript
// 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 {};typescript
// 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;安全指南
.gitignore 配置
gitignore
# 环境变量文件
.env
.env.local
.env.*.local
.env.development
.env.test
.env.production
# 保留示例文件
!.env.example.env.example 示例文件
bash
# 服务器配置
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敏感信息处理最佳实践
javascript
// ❌ 错误:硬编码敏感信息
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 环境变量未设置');
})();生产环境安全检查
javascript
// 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 使用了弱密钥,请更换');
}
}
}Docker 环境配置
dockerfile
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# 不复制 .env 文件,通过 docker-compose 或 k8s 注入
CMD ["node", "src/app.js"]yaml
# docker-compose.yml
version: '3.8'
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常见问题解答
Q1: .env 文件不生效怎么办?
问题: 配置了 .env 文件但 process.env 中读取不到变量。
解决方案:
javascript
// 1. 确保 dotenv.config() 在最前面调用
import dotenv from 'dotenv';
dotenv.config(); // 必须在使用环境变量之前
import express from 'express'; // 在 dotenv 之后导入javascript
// 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);
}javascript
// 3. 启用调试模式
dotenv.config({ debug: true });Q2: 环境变量是字符串类型,如何转换类型?
问题: process.env.PORT 是字符串,需要转换为数字。
解决方案:
javascript
// 数字转换
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) : [];
}Q3: 如何在多环境中管理不同的配置?
解决方案:
javascript
// 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}`);
}
});Q4: 如何处理敏感信息?
解决方案:
javascript
// 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 等工具Q5: cross-env 和 dotenv 的执行顺序是怎样的?
优先级:
code
cross-env 设置的变量 > 系统环境变量 > .env 文件 > 默认值示例:
json
{
"scripts": {
"start": "cross-env PORT=8080 node -r dotenv/config app.js"
}
}javascript
// app.js
console.log(process.env.PORT); // 8080 (cross-env 优先)bash
# .env 文件
PORT=3000最终 PORT 值为 8080,因为 cross-env 设置的变量优先级高于 .env 文件。
Q6: 如何验证必需的环境变量?
解决方案:
javascript
// 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('✅ 环境变量验证通过');
}javascript
// app.js
import dotenv from 'dotenv';
dotenv.config();
import { validateEnv } from './config/validate';
validateEnv();
// 启动应用...Q7: 测试环境如何处理环境变量?
解决方案:
javascript
// 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';json
// 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文件管理配置 - 生产环境通过 CI/CD 或容器编排工具注入
- 使用
.env.example作为配置模板 - 启动时验证必需的环境变量
- 敏感信息使用密钥管理服务
相关文档: