配置
概述
配置管理对于需要在不同环境(开发、测试、预发布、生产)中运行的 Node.js 应用程序至关重要。本章介绍管理配置、环境变量和应用程序设置的最佳实践。
环境变量
基础环境变量
环境变量提供了一种无需硬编码即可配置应用程序的方式:
javascript
// Basic usage
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL || 'mongodb://localhost:27017/myapp';
const jwtSecret = process.env.JWT_SECRET || 'fallback-secret';
console.log('Server will run on port:', port);
console.log('Database URL:', dbUrl);使用 dotenv
dotenv 包从 .env 文件加载环境变量:
bash
npm install dotenv创建环境文件:
bash
# .env (development)
NODE_ENV=development
PORT=3000
HOST=localhost
# Database
DATABASE_URL=mongodb://localhost:27017/myapp_dev
DB_HOST=localhost
DB_PORT=27017
DB_NAME=myapp_dev
DB_USER=
DB_PASSWORD=
# Redis
REDIS_URL=redis://localhost:6379
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# JWT
JWT_SECRET=your-development-secret-key
JWT_EXPIRES_IN=24h
JWT_REFRESH_EXPIRES_IN=7d
# Email
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password
# File Storage
UPLOAD_DIR=./uploads
MAX_FILE_SIZE=5242880
ALLOWED_FILE_TYPES=jpg,jpeg,png,gif,pdf,doc,docx
# External APIs
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
SENDGRID_API_KEY=SG...
# Logging
LOG_LEVEL=debug
LOG_FILE=./logs/app.log
# Security
BCRYPT_ROUNDS=12
RATE_LIMIT_WINDOW=900000
RATE_LIMIT_MAX=100bash
# .env.production
NODE_ENV=production
PORT=8080
HOST=0.0.0.0
# Database (use connection string in production)
DATABASE_URL=mongodb+srv://user:password@cluster.mongodb.net/myapp_prod
# Redis
REDIS_URL=redis://redis-server:6379
# JWT (use strong secrets in production)
JWT_SECRET=super-secure-production-secret-key-change-this
JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d
# Email
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASSWORD=your-sendgrid-api-key
# File Storage (use cloud storage in production)
UPLOAD_DIR=/app/uploads
MAX_FILE_SIZE=10485760
ALLOWED_FILE_TYPES=jpg,jpeg,png,pdf
# External APIs
STRIPE_SECRET_KEY=sk_live_...
STRIPE_PUBLISHABLE_KEY=pk_live_...
# Logging
LOG_LEVEL=info
LOG_FILE=/var/log/app.log
# Security
BCRYPT_ROUNDS=14
RATE_LIMIT_WINDOW=900000
RATE_LIMIT_MAX=50加载环境变量
javascript
// config/env.js
const dotenv = require('dotenv');
const path = require('path');
// Determine which .env file to load
const envFile = process.env.NODE_ENV === 'production'
? '.env.production'
: process.env.NODE_ENV === 'test'
? '.env.test'
: '.env';
// Load environment variables
const result = dotenv.config({ path: path.resolve(process.cwd(), envFile) });
if (result.error && process.env.NODE_ENV !== 'production') {
console.warn(`Warning: Could not load ${envFile} file`);
}
// Validate required environment variables
const requiredEnvVars = [
'JWT_SECRET',
'DATABASE_URL'
];
const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar]);
if (missingEnvVars.length > 0) {
console.error('Missing required environment variables:', missingEnvVars);
process.exit(1);
}
module.exports = {
loaded: !result.error,
envFile,
requiredEnvVars
};配置对象
集中式配置
创建集中式配置对象:
javascript
// config/index.js
require('./env'); // Load environment variables first
const config = {
// Application
app: {
name: process.env.APP_NAME || 'Node.js App',
version: process.env.APP_VERSION || '1.0.0',
env: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT, 10) || 3000,
host: process.env.HOST || 'localhost',
url: process.env.APP_URL || `http://localhost:${process.env.PORT || 3000}`
},
// Database
database: {
url: process.env.DATABASE_URL,
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT, 10) || 27017,
name: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || '',
password: process.env.DB_PASSWORD || '',
ssl: process.env.DB_SSL === 'true',
poolSize: parseInt(process.env.DB_POOL_SIZE, 10) || 10,
options: {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: parseInt(process.env.DB_POOL_SIZE, 10) || 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
}
},
// Redis
redis: {
url: process.env.REDIS_URL,
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_PASSWORD || '',
db: parseInt(process.env.REDIS_DB, 10) || 0,
keyPrefix: process.env.REDIS_KEY_PREFIX || 'myapp:',
ttl: parseInt(process.env.REDIS_TTL, 10) || 3600
},
// JWT
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '24h',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
issuer: process.env.JWT_ISSUER || 'myapp',
audience: process.env.JWT_AUDIENCE || 'myapp-users'
},
// Email
email: {
smtp: {
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT, 10) || 587,
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD
}
},
from: {
name: process.env.EMAIL_FROM_NAME || 'MyApp',
address: process.env.EMAIL_FROM_ADDRESS || 'noreply@myapp.com'
},
templates: {
welcome: 'welcome',
resetPassword: 'reset-password',
emailVerification: 'email-verification'
}
},
// File Upload
upload: {
dir: process.env.UPLOAD_DIR || './uploads',
maxSize: parseInt(process.env.MAX_FILE_SIZE, 10) || 5 * 1024 * 1024, // 5MB
allowedTypes: (process.env.ALLOWED_FILE_TYPES || 'jpg,jpeg,png,gif,pdf').split(','),
storage: process.env.STORAGE_TYPE || 'local', // local, s3, cloudinary
s3: {
bucket: process.env.S3_BUCKET,
region: process.env.S3_REGION || 'us-east-1',
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY
}
},
// Security
security: {
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS, 10) || 12,
rateLimit: {
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW, 10) || 15 * 60 * 1000, // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100,
message: 'Too many requests from this IP'
},
cors: {
origin: process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : true,
credentials: process.env.CORS_CREDENTIALS === 'true'
},
helmet: {
contentSecurityPolicy: process.env.NODE_ENV === 'production',
crossOriginEmbedderPolicy: false
}
},
// Logging
logging: {
level: process.env.LOG_LEVEL || 'info',
file: process.env.LOG_FILE || './logs/app.log',
maxSize: process.env.LOG_MAX_SIZE || '20m',
maxFiles: parseInt(process.env.LOG_MAX_FILES, 10) || 5,
format: process.env.LOG_FORMAT || 'json'
},
// External APIs
apis: {
stripe: {
secretKey: process.env.STRIPE_SECRET_KEY,
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET
},
sendgrid: {
apiKey: process.env.SENDGRID_API_KEY
},
twilio: {
accountSid: process.env.TWILIO_ACCOUNT_SID,
authToken: process.env.TWILIO_AUTH_TOKEN,
phoneNumber: process.env.TWILIO_PHONE_NUMBER
}
}
};
// Environment-specific overrides
if (config.app.env === 'test') {
config.database.name = config.database.name + '_test';
config.logging.level = 'error';
}
if (config.app.env === 'production') {
config.logging.level = 'info';
config.security.rateLimit.max = 50; // Stricter rate limiting in production
}
module.exports = config;配置验证
模式验证
使用 Joi 等模式验证库:
bash
npm install joijavascript
// config/validation.js
const Joi = require('joi');
const configSchema = Joi.object({
app: Joi.object({
name: Joi.string().required(),
version: Joi.string().required(),
env: Joi.string().valid('development', 'test', 'staging', 'production').required(),
port: Joi.number().port().required(),
host: Joi.string().required(),
url: Joi.string().uri().required()
}).required(),
database: Joi.object({
url: Joi.string().required(),
host: Joi.string().required(),
port: Joi.number().port().required(),
name: Joi.string().required(),
user: Joi.string().allow(''),
password: Joi.string().allow(''),
ssl: Joi.boolean(),
poolSize: Joi.number().min(1).max(100)
}).required(),
jwt: Joi.object({
secret: Joi.string().min(32).required(),
expiresIn: Joi.string().required(),
refreshExpiresIn: Joi.string().required(),
issuer: Joi.string().required(),
audience: Joi.string().required()
}).required(),
email: Joi.object({
smtp: Joi.object({
host: Joi.string().required(),
port: Joi.number().port().required(),
secure: Joi.boolean(),
auth: Joi.object({
user: Joi.string().required(),
pass: Joi.string().required()
}).required()
}).required(),
from: Joi.object({
name: Joi.string().required(),
address: Joi.string().email().required()
}).required()
}).required(),
security: Joi.object({
bcryptRounds: Joi.number().min(10).max(15).required(),
rateLimit: Joi.object({
windowMs: Joi.number().min(1000).required(),
max: Joi.number().min(1).required(),
message: Joi.string().required()
}).required()
}).required(),
logging: Joi.object({
level: Joi.string().valid('error', 'warn', 'info', 'debug').required(),
file: Joi.string().required(),
maxSize: Joi.string().required(),
maxFiles: Joi.number().min(1).required(),
format: Joi.string().valid('json', 'simple').required()
}).required()
});
function validateConfig(config) {
const { error, value } = configSchema.validate(config, {
allowUnknown: true,
abortEarly: false
});
if (error) {
const errorMessages = error.details.map(detail => detail.message);
throw new Error(`Configuration validation failed:\n${errorMessages.join('\n')}`);
}
return value;
}
module.exports = {
configSchema,
validateConfig
};使用验证
javascript
// config/index.js (updated)
const { validateConfig } = require('./validation');
// ... config object definition ...
// Validate configuration
try {
const validatedConfig = validateConfig(config);
module.exports = validatedConfig;
} catch (error) {
console.error('Configuration Error:', error.message);
process.exit(1);
}动态配置
配置加载器
javascript
// config/loader.js
const fs = require('fs');
const path = require('path');
class ConfigLoader {
constructor() {
this.config = {};
this.watchers = new Map();
}
load(configPath) {
try {
const fullPath = path.resolve(configPath);
if (!fs.existsSync(fullPath)) {
throw new Error(`Configuration file not found: ${fullPath}`);
}
const configData = fs.readFileSync(fullPath, 'utf8');
const parsedConfig = JSON.parse(configData);
this.config = { ...this.config, ...parsedConfig };
return this.config;
} catch (error) {
throw new Error(`Failed to load configuration: ${error.message}`);
}
}
watch(configPath, callback) {
const fullPath = path.resolve(configPath);
if (this.watchers.has(fullPath)) {
return;
}
const watcher = fs.watchFile(fullPath, (curr, prev) => {
if (curr.mtime !== prev.mtime) {
try {
this.load(configPath);
callback(this.config);
} catch (error) {
console.error('Error reloading configuration:', error.message);
}
}
});
this.watchers.set(fullPath, watcher);
}
get(key, defaultValue = null) {
const keys = key.split('.');
let value = this.config;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return defaultValue;
}
}
return value;
}
set(key, value) {
const keys = key.split('.');
let current = this.config;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i];
if (!(k in current) || typeof current[k] !== 'object') {
current[k] = {};
}
current = current[k];
}
current[keys[keys.length - 1]] = value;
}
stopWatching() {
for (const [path, watcher] of this.watchers) {
fs.unwatchFile(path);
}
this.watchers.clear();
}
}
module.exports = ConfigLoader;功能标志
功能标志系统
javascript
// config/features.js
class FeatureFlags {
constructor(config = {}) {
this.flags = new Map();
this.loadFlags(config);
}
loadFlags(config) {
// Load from environment variables
Object.keys(process.env).forEach(key => {
if (key.startsWith('FEATURE_')) {
const flagName = key.replace('FEATURE_', '').toLowerCase();
const flagValue = process.env[key] === 'true';
this.flags.set(flagName, flagValue);
}
});
// Load from config object
if (config.features) {
Object.entries(config.features).forEach(([key, value]) => {
this.flags.set(key.toLowerCase(), value);
});
}
}
isEnabled(flagName) {
return this.flags.get(flagName.toLowerCase()) || false;
}
enable(flagName) {
this.flags.set(flagName.toLowerCase(), true);
}
disable(flagName) {
this.flags.set(flagName.toLowerCase(), false);
}
toggle(flagName) {
const current = this.isEnabled(flagName);
this.flags.set(flagName.toLowerCase(), !current);
}
getAllFlags() {
return Object.fromEntries(this.flags);
}
// Middleware for Express
middleware() {
return (req, res, next) => {
req.features = this;
next();
};
}
}
// Usage in environment variables
// FEATURE_NEW_UI=true
// FEATURE_BETA_API=false
// FEATURE_ANALYTICS=true
module.exports = FeatureFlags;使用功能标志
javascript
// app.js
const FeatureFlags = require('./config/features');
const config = require('./config');
const features = new FeatureFlags(config);
// Use feature flags in middleware
app.use(features.middleware());
// Use in routes
app.get('/api/users', (req, res) => {
if (req.features.isEnabled('beta_api')) {
// Use new API version
return res.json({ version: 'v2', users: [] });
}
// Use old API version
res.json({ version: 'v1', users: [] });
});
// Use in services
class UserService {
async getUsers() {
if (features.isEnabled('new_user_query')) {
return this.getUsersOptimized();
}
return this.getUsersLegacy();
}
}配置最佳实践
密钥管理
javascript
// config/secrets.js
const fs = require('fs');
const path = require('path');
class SecretsManager {
constructor() {
this.secrets = new Map();
this.loadSecrets();
}
loadSecrets() {
// Load from environment variables
this.loadFromEnv();
// Load from files (Docker secrets, Kubernetes secrets)
this.loadFromFiles();
// Load from external services (AWS Secrets Manager, HashiCorp Vault)
// this.loadFromExternalService();
}
loadFromEnv() {
const secretEnvVars = [
'JWT_SECRET',
'DATABASE_PASSWORD',
'STRIPE_SECRET_KEY',
'SENDGRID_API_KEY'
];
secretEnvVars.forEach(envVar => {
if (process.env[envVar]) {
this.secrets.set(envVar.toLowerCase(), process.env[envVar]);
}
});
}
loadFromFiles() {
const secretsDir = process.env.SECRETS_DIR || '/run/secrets';
if (!fs.existsSync(secretsDir)) {
return;
}
const secretFiles = fs.readdirSync(secretsDir);
secretFiles.forEach(file => {
try {
const secretPath = path.join(secretsDir, file);
const secretValue = fs.readFileSync(secretPath, 'utf8').trim();
this.secrets.set(file.toLowerCase(), secretValue);
} catch (error) {
console.warn(`Failed to load secret from file ${file}:`, error.message);
}
});
}
get(secretName) {
return this.secrets.get(secretName.toLowerCase());
}
has(secretName) {
return this.secrets.has(secretName.toLowerCase());
}
// Mask secrets for logging
mask(secretName) {
const secret = this.get(secretName);
if (!secret) return null;
if (secret.length <= 8) {
return '*'.repeat(secret.length);
}
return secret.substring(0, 4) + '*'.repeat(secret.length - 8) + secret.substring(secret.length - 4);
}
}
module.exports = SecretsManager;配置测试
javascript
// tests/config.test.js
const config = require('../config');
describe('Configuration', () => {
test('should have required properties', () => {
expect(config.app).toBeDefined();
expect(config.database).toBeDefined();
expect(config.jwt).toBeDefined();
});
test('should have valid port number', () => {
expect(config.app.port).toBeGreaterThan(0);
expect(config.app.port).toBeLessThan(65536);
});
test('should have secure JWT secret in production', () => {
if (config.app.env === 'production') {
expect(config.jwt.secret).toBeDefined();
expect(config.jwt.secret.length).toBeGreaterThanOrEqual(32);
}
});
test('should have valid database configuration', () => {
expect(config.database.url).toBeDefined();
expect(config.database.host).toBeDefined();
expect(config.database.port).toBeGreaterThan(0);
});
test('should have valid email configuration', () => {
expect(config.email.smtp.host).toBeDefined();
expect(config.email.smtp.port).toBeGreaterThan(0);
expect(config.email.from.address).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
});
});下一步
在下一章中,我们将探索核心 Node.js 概念,包括事件驱动编程、流和异步模式。
实践练习
- 为多租户应用程序创建配置系统
- 实现具有数据库持久化的功能标志系统
- 构建与云服务集成的密钥管理器
- 为微服务架构创建配置验证
关键要点
- 对在部署之间变化的配置使用环境变量
- 验证配置以尽早发现错误
- 将密钥与常规配置分开
- 使用功能标志进行逐步推出和 A/B 测试
- 实现配置热重载以进行动态更新
- 测试您的配置以确保它在所有环境中都能工作
- 遵循密钥访问的最小权限原则