Skip to content

配置

概述

配置管理对于需要在不同环境(开发、测试、预发布、生产)中运行的 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=100
bash
# .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 joi
javascript
// 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 概念,包括事件驱动编程、流和异步模式。

实践练习

  1. 为多租户应用程序创建配置系统
  2. 实现具有数据库持久化的功能标志系统
  3. 构建与云服务集成的密钥管理器
  4. 为微服务架构创建配置验证

关键要点

  • 对在部署之间变化的配置使用环境变量
  • 验证配置以尽早发现错误
  • 将密钥与常规配置分开
  • 使用功能标志进行逐步推出和 A/B 测试
  • 实现配置热重载以进行动态更新
  • 测试您的配置以确保它在所有环境中都能工作
  • 遵循密钥访问的最小权限原则

本站内容仅供学习和研究使用。