跳到主要内容

Node.js编程题✅

本章节涵盖Node.js开发中的典型编程题,包括热更新实现、日志系统设计等实际应用场景。

什么是热更新,Node.js如何实现热更新?

答案

核心概念

热更新(Hot Reload)是指在应用运行时动态更新代码,无需重启整个应用程序。在Node.js中,热更新主要通过监听文件变化、清除模块缓存、重新加载模块来实现。

实现原理

1. 文件监听机制

  • 使用fs.watch()或第三方库如chokidar监听文件变化
  • 检测.js.json等模块文件的修改、删除事件

2. 模块缓存管理

  • Node.js使用require.cache缓存已加载的模块
  • 热更新时需要删除对应模块的缓存
  • 重新require()模块获取最新代码

3. 依赖关系处理

  • 清理被修改模块的依赖模块缓存
  • 处理循环依赖的情况
  • 保持应用状态的一致性

示例实现:

// Node.js 热更新实现示例
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const chokidar = require('chokidar'); // npm install chokidar

/**
 * 简单的模块热更新实现
 */
class SimpleHotReload {
  constructor() {
    this.cache = new Map(); // 模块缓存
    this.watchers = new Map(); // 文件监听器
    this.callbacks = new Map(); // 变更回调
  }

  // 加载模块并监听文件变化
  require(modulePath) {
    const absolutePath = path.resolve(modulePath);
    
    // 如果已经缓存,直接返回
    if (this.cache.has(absolutePath)) {
      return this.cache.get(absolutePath);
    }

    try {
      // 动态导入模块
      delete require.cache[absolutePath];
      const module = require(absolutePath);
      
      // 缓存模块
      this.cache.set(absolutePath, module);
      
      // 监听文件变化
      this.watchFile(absolutePath);
      
      return module;
    } catch (error) {
      console.error(`加载模块失败: ${modulePath}`, error);
      return null;
    }
  }

  // 监听文件变化
  watchFile(filePath) {
    if (this.watchers.has(filePath)) {
      return;
    }

    const watcher = chokidar.watch(filePath, {
      ignoreInitial: true,
      persistent: true
    });

    watcher.on('change', () => {
      console.log(`文件变化检测到: ${filePath}`);
      this.reloadModule(filePath);
    });

    watcher.on('unlink', () => {
      console.log(`文件被删除: ${filePath}`);
      this.unloadModule(filePath);
    });

    this.watchers.set(filePath, watcher);
  }

  // 重新加载模块
  reloadModule(filePath) {
    try {
      // 清除require缓存
      delete require.cache[filePath];
      
      // 重新加载模块
      const newModule = require(filePath);
      const oldModule = this.cache.get(filePath);
      
      // 更新缓存
      this.cache.set(filePath, newModule);
      
      // 触发回调
      const callback = this.callbacks.get(filePath);
      if (callback) {
        callback(newModule, oldModule);
      }
      
      console.log(`模块热更新成功: ${filePath}`);
    } catch (error) {
      console.error(`模块热更新失败: ${filePath}`, error);
    }
  }

  // 卸载模块
  unloadModule(filePath) {
    // 清除缓存
    this.cache.delete(filePath);
    delete require.cache[filePath];
    
    // 关闭文件监听
    const watcher = this.watchers.get(filePath);
    if (watcher) {
      watcher.close();
      this.watchers.delete(filePath);
    }
    
    // 清除回调
    this.callbacks.delete(filePath);
  }

  // 注册模块变更回调
  onModuleChange(modulePath, callback) {
    const absolutePath = path.resolve(modulePath);
    this.callbacks.set(absolutePath, callback);
  }

  // 关闭所有监听器
  destroy() {
    for (const [filePath, watcher] of this.watchers) {
      watcher.close();
    }
    this.watchers.clear();
    this.cache.clear();
    this.callbacks.clear();
  }
}

/**
 * HTTP服务器热更新实现
 */
class HTTPServerHotReload {
  constructor(port = 3000) {
    this.port = port;
    this.server = null;
    this.hotReload = new SimpleHotReload();
  }

  start() {
    // 加载路由模块
    const router = this.hotReload.require('./router');
    
    // 创建HTTP服务器
    const http = require('http');
    this.server = http.createServer((req, res) => {
      try {
        // 获取当前路由模块
        const currentRouter = this.hotReload.cache.get(path.resolve('./router'));
        if (currentRouter && typeof currentRouter.handle === 'function') {
          currentRouter.handle(req, res);
        } else {
          res.writeHead(500);
          res.end('Router module not found or invalid');
        }
      } catch (error) {
        console.error('Request handling error:', error);
        res.writeHead(500);
        res.end('Internal Server Error');
      }
    });

    // 监听路由模块变化
    this.hotReload.onModuleChange('./router', (newRouter, oldRouter) => {
      console.log('路由模块已热更新');
      // 可以在这里执行一些清理工作
      if (oldRouter && typeof oldRouter.cleanup === 'function') {
        oldRouter.cleanup();
      }
    });

    this.server.listen(this.port, () => {
      console.log(`HTTP服务器启动在端口 ${this.port}`);
      console.log('支持热更新,修改 router.js 文件试试');
    });
  }

  stop() {
    if (this.server) {
      this.server.close();
    }
    this.hotReload.destroy();
  }
}

/**
 * 配置文件热更新实现
 */
class ConfigHotReload {
  constructor(configPath) {
    this.configPath = path.resolve(configPath);
    this.config = {};
    this.callbacks = [];
    this.watcher = null;
    
    this.loadConfig();
    this.watchConfig();
  }

  // 加载配置文件
  loadConfig() {
    try {
      const content = fs.readFileSync(this.configPath, 'utf8');
      this.config = JSON.parse(content);
      console.log('配置文件加载成功:', this.configPath);
    } catch (error) {
      console.error('配置文件加载失败:', error);
      this.config = {};
    }
  }

  // 监听配置文件变化
  watchConfig() {
    this.watcher = chokidar.watch(this.configPath, {
      ignoreInitial: true
    });

    this.watcher.on('change', () => {
      console.log('配置文件变化检测到');
      const oldConfig = { ...this.config };
      this.loadConfig();
      
      // 触发变更回调
      this.callbacks.forEach(callback => {
        try {
          callback(this.config, oldConfig);
        } catch (error) {
          console.error('配置变更回调执行失败:', error);
        }
      });
    });
  }

  // 获取配置
  get(key) {
    return key ? this.config[key] : this.config;
  }

  // 注册配置变更回调
  onChange(callback) {
    this.callbacks.push(callback);
  }

  // 销毁监听器
  destroy() {
    if (this.watcher) {
      this.watcher.close();
    }
    this.callbacks = [];
  }
}

// 演示用法
function demonstrateHotReload() {
  console.log('=== Node.js 热更新演示 ===\n');

  // 1. 简单模块热更新演示
  console.log('1. 简单模块热更新演示');
  const hotReload = new SimpleHotReload();
  
  // 创建一个示例模块文件
  const moduleContent = `
// 示例模块 - 计算器
module.exports = {
  add: (a, b) => {
    console.log(\`执行加法: \${a} + \${b} = \${a + b}\`);
    return a + b;
  },
  version: '1.0.0'
};
  `;
  
  fs.writeFileSync('./example-module.js', moduleContent.trim());
  
  // 加载模块
  let calculator = hotReload.require('./example-module.js');
  console.log('初始版本:', calculator.version);
  console.log('执行计算:', calculator.add(2, 3));

  // 监听模块变化
  hotReload.onModuleChange('./example-module.js', (newModule, oldModule) => {
    console.log('模块已更新!');
    console.log('新版本:', newModule.version);
    console.log('旧版本:', oldModule.version);
    calculator = newModule; // 更新引用
  });

  // 2. 配置热更新演示
  setTimeout(() => {
    console.log('\n2. 配置热更新演示');
    
    // 创建配置文件
    const configContent = {
      port: 3000,
      debug: false,
      maxConnections: 100
    };
    
    fs.writeFileSync('./config.json', JSON.stringify(configContent, null, 2));
    
    // 配置热更新
    const config = new ConfigHotReload('./config.json');
    
    config.onChange((newConfig, oldConfig) => {
      console.log('配置已更新!');
      console.log('新配置:', JSON.stringify(newConfig, null, 2));
    });
    
    console.log('当前配置:', JSON.stringify(config.get(), null, 2));
    
    // 3. 演示配置变更
    setTimeout(() => {
      console.log('\n3. 模拟配置文件变更');
      const updatedConfig = {
        port: 8080,
        debug: true,
        maxConnections: 200,
        newFeature: 'enabled'
      };
      
      fs.writeFileSync('./config.json', JSON.stringify(updatedConfig, null, 2));
    }, 2000);
    
    // 4. 演示模块文件变更
    setTimeout(() => {
      console.log('\n4. 模拟模块文件变更');
      const updatedModuleContent = `
// 示例模块 - 增强版计算器
module.exports = {
  add: (a, b) => {
    const result = a + b;
    console.log(\`执行加法: \${a} + \${b} = \${result}\`);
    return result;
  },
  subtract: (a, b) => {
    const result = a - b;
    console.log(\`执行减法: \${a} - \${b} = \${result}\`);
    return result;
  },
  version: '2.0.0'
};
      `;
      
      fs.writeFileSync('./example-module.js', updatedModuleContent.trim());
      
      // 测试更新后的功能
      setTimeout(() => {
        console.log('测试更新后的模块:');
        console.log('加法:', calculator.add(5, 3));
        if (calculator.subtract) {
          console.log('减法:', calculator.subtract(10, 4));
        }
      }, 1000);
    }, 4000);
    
    // 清理资源
    setTimeout(() => {
      console.log('\n清理演示资源...');
      hotReload.destroy();
      config.destroy();
      
      // 删除示例文件
      try {
        fs.unlinkSync('./example-module.js');
        fs.unlinkSync('./config.json');
      } catch (error) {
        // 忽略删除错误
      }
      
      console.log('演示结束');
    }, 8000);
    
  }, 1000);
}

// Express.js 风格的热更新中间件
function createHotReloadMiddleware(options = {}) {
  const { watchDirs = ['./routes', './middleware'], ignored = /node_modules/ } = options;
  const hotReload = new SimpleHotReload();
  
  return {
    // 中间件函数
    middleware: (req, res, next) => {
      // 为每个请求重新加载模块
      req.hotReload = hotReload;
      next();
    },
    
    // 监听指定目录
    watch: () => {
      watchDirs.forEach(dir => {
        const watcher = chokidar.watch(dir, {
          ignored,
          ignoreInitial: false,
          persistent: true
        });
        
        watcher.on('change', (filePath) => {
          console.log(`文件变化: ${filePath}`);
          // 清除该文件及其依赖的缓存
          delete require.cache[path.resolve(filePath)];
        });
      });
    },
    
    // 销毁
    destroy: () => {
      hotReload.destroy();
    }
  };
}

// 如果直接运行此文件,则执行演示
if (require.main === module) {
  demonstrateHotReload();
}

module.exports = {
  SimpleHotReload,
  HTTPServerHotReload,
  ConfigHotReload,
  createHotReloadMiddleware,
  demonstrateHotReload
};

应用场景:

开发环境:

  • 提升开发效率,无需频繁重启应用
  • 保持应用状态,如WebSocket连接、内存数据
  • 快速验证代码修改效果

生产环境:

  • 配置文件热更新,如数据库连接配置
  • 业务规则热更新,如价格策略、权限配置
  • 模板文件热更新,如邮件模板、页面模板

注意事项:

  • 内存泄漏: 未正确清理的定时器、事件监听器会导致内存泄漏
  • 状态一致性: 模块更新可能导致应用状态不一致
  • 依赖管理: 复杂的依赖关系需要仔细处理
  • 错误处理: 新代码可能包含语法错误,需要优雅处理

面试官视角

该题考察候选人对Node.js模块系统和文件监听的理解:

  • 要点清单: 理解require.cache机制;掌握文件监听API;能设计热更新方案;了解注意事项
  • 加分项: 有实际热更新开发经验;了解生产环境应用;能处理复杂依赖关系;考虑性能和稳定性
  • 常见失误: 只知道基本概念;不了解缓存清理;忽视内存泄漏;缺乏错误处理

延伸阅读

如何设计一个日志系统?

答案

核心概念

日志系统是应用程序的重要基础设施,用于记录、管理和分析应用运行时的各种信息。一个完善的日志系统应该包括日志级别、格式化、传输、轮转等功能。

日志系统架构

1. 日志级别管理

  • ERROR: 错误信息,需要立即处理
  • WARN: 警告信息,需要关注
  • INFO: 一般信息,记录重要事件
  • DEBUG: 调试信息,开发时使用
  • TRACE: 跟踪信息,详细执行流程

2. 日志格式化

  • 时间戳: ISO 8601格式或自定义格式
  • 级别标识: 清晰的级别标记
  • 分类标签: 模块或服务标识
  • 消息内容: 实际日志信息
  • 元数据: 上下文信息,如用户ID、请求ID

3. 传输器设计

  • Console传输器: 控制台输出
  • File传输器: 文件存储,支持轮转
  • HTTP传输器: 远程日志服务
  • Database传输器: 数据库存储

示例实现:

// Node.js 日志系统设计实现
const fs = require('fs');
const path = require('path');
const util = require('util');
const EventEmitter = require('events');

/**
 * 日志级别定义
 */
const LOG_LEVELS = {
  ERROR: { name: 'ERROR', value: 0, color: '\x1b[31m' }, // 红色
  WARN: { name: 'WARN', value: 1, color: '\x1b[33m' },  // 黄色
  INFO: { name: 'INFO', value: 2, color: '\x1b[32m' },  // 绿色
  DEBUG: { name: 'DEBUG', value: 3, color: '\x1b[36m' }, // 青色
  TRACE: { name: 'TRACE', value: 4, color: '\x1b[37m' }  // 白色
};

const RESET_COLOR = '\x1b[0m';

/**
 * 日志格式化器
 */
class LogFormatter {
  constructor(options = {}) {
    this.template = options.template || '[{timestamp}] [{level}] [{category}] {message}';
    this.timestampFormat = options.timestampFormat || 'iso';
    this.colorize = options.colorize !== false;
  }

  format(logEntry) {
    const { level, message, category, timestamp, metadata } = logEntry;
    
    let formattedMessage = this.template
      .replace('{timestamp}', this.formatTimestamp(timestamp))
      .replace('{level}', level.name)
      .replace('{category}', category || 'APP')
      .replace('{message}', this.formatMessage(message));

    // 添加元数据
    if (metadata && Object.keys(metadata).length > 0) {
      formattedMessage += ' | ' + JSON.stringify(metadata);
    }

    // 添加颜色
    if (this.colorize && process.stdout.isTTY) {
      formattedMessage = level.color + formattedMessage + RESET_COLOR;
    }

    return formattedMessage;
  }

  formatTimestamp(timestamp) {
    switch (this.timestampFormat) {
      case 'iso':
        return timestamp.toISOString();
      case 'locale':
        return timestamp.toLocaleString();
      case 'epoch':
        return timestamp.getTime().toString();
      default:
        return timestamp.toISOString();
    }
  }

  formatMessage(message) {
    if (typeof message === 'string') {
      return message;
    }
    if (message instanceof Error) {
      return `${message.message}\n${message.stack}`;
    }
    return util.inspect(message, { depth: 3, colors: false });
  }
}

/**
 * 日志传输器基类
 */
class LogTransport extends EventEmitter {
  constructor(options = {}) {
    super();
    this.level = options.level || LOG_LEVELS.INFO;
    this.formatter = options.formatter || new LogFormatter(options);
    this.silent = options.silent || false;
  }

  shouldLog(level) {
    return !this.silent && level.value <= this.level.value;
  }

  log(logEntry) {
    throw new Error('log method must be implemented by subclass');
  }
}

/**
 * 控制台传输器
 */
class ConsoleTransport extends LogTransport {
  constructor(options = {}) {
    super(options);
    this.stream = options.stream || (
      options.level && options.level.value <= LOG_LEVELS.WARN.value
        ? process.stderr
        : process.stdout
    );
  }

  log(logEntry) {
    if (!this.shouldLog(logEntry.level)) {
      return;
    }

    const formatted = this.formatter.format(logEntry);
    this.stream.write(formatted + '\n');
    this.emit('logged', logEntry);
  }
}

/**
 * 文件传输器
 */
class FileTransport extends LogTransport {
  constructor(options = {}) {
    super(options);
    this.filename = options.filename || 'app.log';
    this.maxSize = options.maxSize || 10 * 1024 * 1024; // 10MB
    this.maxFiles = options.maxFiles || 5;
    this.dirname = options.dirname || './logs';
    this.writeStream = null;
    this.currentSize = 0;

    this.ensureDirectory();
    this.createWriteStream();
  }

  ensureDirectory() {
    if (!fs.existsSync(this.dirname)) {
      fs.mkdirSync(this.dirname, { recursive: true });
    }
  }

  createWriteStream() {
    const filepath = path.join(this.dirname, this.filename);
    
    // 检查现有文件大小
    try {
      const stats = fs.statSync(filepath);
      this.currentSize = stats.size;
    } catch (error) {
      this.currentSize = 0;
    }

    this.writeStream = fs.createWriteStream(filepath, { flags: 'a' });
    
    this.writeStream.on('error', (error) => {
      this.emit('error', error);
    });
  }

  log(logEntry) {
    if (!this.shouldLog(logEntry.level)) {
      return;
    }

    const formatted = this.formatter.format(logEntry) + '\n';
    const messageSize = Buffer.byteLength(formatted, 'utf8');

    // 检查是否需要轮转日志文件
    if (this.currentSize + messageSize > this.maxSize) {
      this.rotateLogFile();
    }

    this.writeStream.write(formatted);
    this.currentSize += messageSize;
    this.emit('logged', logEntry);
  }

  rotateLogFile() {
    this.writeStream.end();

    // 轮转现有文件
    for (let i = this.maxFiles - 1; i >= 1; i--) {
      const oldFile = path.join(this.dirname, `${this.filename}.${i}`);
      const newFile = path.join(this.dirname, `${this.filename}.${i + 1}`);
      
      try {
        if (fs.existsSync(oldFile)) {
          if (i === this.maxFiles - 1) {
            fs.unlinkSync(oldFile); // 删除最老的文件
          } else {
            fs.renameSync(oldFile, newFile);
          }
        }
      } catch (error) {
        this.emit('error', error);
      }
    }

    // 重命名当前文件
    const currentFile = path.join(this.dirname, this.filename);
    const rotatedFile = path.join(this.dirname, `${this.filename}.1`);
    
    try {
      if (fs.existsSync(currentFile)) {
        fs.renameSync(currentFile, rotatedFile);
      }
    } catch (error) {
      this.emit('error', error);
    }

    // 创建新的写入流
    this.currentSize = 0;
    this.createWriteStream();
  }

  close() {
    if (this.writeStream) {
      this.writeStream.end();
    }
  }
}

/**
 * HTTP传输器 - 将日志发送到远程服务器
 */
class HTTPTransport extends LogTransport {
  constructor(options = {}) {
    super(options);
    this.url = options.url || 'http://localhost:3000/logs';
    this.method = options.method || 'POST';
    this.headers = options.headers || { 'Content-Type': 'application/json' };
    this.batchSize = options.batchSize || 10;
    this.flushInterval = options.flushInterval || 5000;
    this.batch = [];
    this.timer = null;

    this.startBatchTimer();
  }

  log(logEntry) {
    if (!this.shouldLog(logEntry.level)) {
      return;
    }

    this.batch.push(logEntry);

    if (this.batch.length >= this.batchSize) {
      this.flush();
    }
  }

  startBatchTimer() {
    this.timer = setInterval(() => {
      if (this.batch.length > 0) {
        this.flush();
      }
    }, this.flushInterval);
  }

  flush() {
    if (this.batch.length === 0) {
      return;
    }

    const logs = this.batch.splice(0);
    const payload = JSON.stringify({ logs });

    // 使用内置http模块发送请求
    const url = new URL(this.url);
    const http = url.protocol === 'https:' ? require('https') : require('http');

    const options = {
      hostname: url.hostname,
      port: url.port,
      path: url.pathname,
      method: this.method,
      headers: {
        ...this.headers,
        'Content-Length': Buffer.byteLength(payload)
      }
    };

    const req = http.request(options, (res) => {
      if (res.statusCode < 200 || res.statusCode >= 300) {
        this.emit('error', new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
      } else {
        this.emit('sent', logs);
      }
    });

    req.on('error', (error) => {
      this.emit('error', error);
      // 将失败的日志重新放回批次队列
      this.batch.unshift(...logs);
    });

    req.write(payload);
    req.end();
  }

  close() {
    if (this.timer) {
      clearInterval(this.timer);
    }
    this.flush();
  }
}

/**
 * 主日志器类
 */
class Logger extends EventEmitter {
  constructor(options = {}) {
    super();
    this.category = options.category || 'default';
    this.transports = options.transports || [];
    this.level = options.level || LOG_LEVELS.INFO;
    this.metadata = options.metadata || {};
  }

  addTransport(transport) {
    this.transports.push(transport);
    transport.on('error', (error) => {
      this.emit('error', error);
    });
    return this;
  }

  removeTransport(transport) {
    const index = this.transports.indexOf(transport);
    if (index !== -1) {
      this.transports.splice(index, 1);
    }
    return this;
  }

  log(level, message, metadata = {}) {
    if (!this.shouldLog(level)) {
      return;
    }

    const logEntry = {
      level,
      message,
      category: this.category,
      timestamp: new Date(),
      metadata: { ...this.metadata, ...metadata }
    };

    // 发送到所有传输器
    this.transports.forEach(transport => {
      try {
        transport.log(logEntry);
      } catch (error) {
        this.emit('error', error);
      }
    });

    this.emit('logged', logEntry);
  }

  shouldLog(level) {
    return level.value <= this.level.value;
  }

  // 便捷方法
  error(message, metadata) {
    return this.log(LOG_LEVELS.ERROR, message, metadata);
  }

  warn(message, metadata) {
    return this.log(LOG_LEVELS.WARN, message, metadata);
  }

  info(message, metadata) {
    return this.log(LOG_LEVELS.INFO, message, metadata);
  }

  debug(message, metadata) {
    return this.log(LOG_LEVELS.DEBUG, message, metadata);
  }

  trace(message, metadata) {
    return this.log(LOG_LEVELS.TRACE, message, metadata);
  }

  // 创建子日志器
  child(options = {}) {
    return new Logger({
      category: options.category || this.category,
      level: options.level || this.level,
      metadata: { ...this.metadata, ...options.metadata },
      transports: this.transports
    });
  }

  // 关闭所有传输器
  close() {
    this.transports.forEach(transport => {
      if (typeof transport.close === 'function') {
        transport.close();
      }
    });
  }
}

/**
 * 日志系统管理器
 */
class LogSystem {
  constructor() {
    this.loggers = new Map();
    this.defaultConfig = {
      level: LOG_LEVELS.INFO,
      transports: [
        new ConsoleTransport({
          level: LOG_LEVELS.DEBUG,
          colorize: true
        })
      ]
    };
  }

  // 获取或创建日志器
  getLogger(category = 'default', options = {}) {
    if (this.loggers.has(category)) {
      return this.loggers.get(category);
    }

    const config = { ...this.defaultConfig, ...options };
    const logger = new Logger({
      category,
      ...config
    });

    this.loggers.set(category, logger);
    return logger;
  }

  // 配置默认设置
  configure(config) {
    this.defaultConfig = { ...this.defaultConfig, ...config };
    return this;
  }

  // 关闭所有日志器
  shutdown() {
    for (const [category, logger] of this.loggers) {
      logger.close();
    }
    this.loggers.clear();
  }
}

// 创建全局日志系统实例
const logSystem = new LogSystem();

// 演示用法
function demonstrateLogSystem() {
  console.log('=== Node.js 日志系统演示 ===\n');

  // 1. 基础用法演示
  console.log('1. 基础日志记录演示');
  const logger = logSystem.getLogger('demo');

  logger.info('应用启动成功');
  logger.warn('这是一个警告消息');
  logger.error('这是一个错误消息');
  logger.debug('调试信息,可能不会显示');

  // 2. 带元数据的日志
  console.log('\n2. 带元数据的日志记录');
  logger.info('用户登录', {
    userId: '12345',
    ip: '192.168.1.1',
    userAgent: 'Mozilla/5.0...'
  });

  // 3. 错误对象日志
  console.log('\n3. 错误对象日志记录');
  try {
    throw new Error('示例错误');
  } catch (error) {
    logger.error(error);
  }

  // 4. 文件日志传输器
  console.log('\n4. 文件日志传输器演示');
  const fileLogger = new Logger({
    category: 'file-demo',
    transports: [
      new FileTransport({
        filename: 'demo.log',
        level: LOG_LEVELS.INFO,
        maxSize: 1024, // 1KB for demo
        maxFiles: 3
      })
    ]
  });

  // 生成一些日志来演示文件轮转
  for (let i = 1; i <= 20; i++) {
    fileLogger.info(`这是第 ${i} 条日志消息,用于演示文件轮转功能。`.repeat(3));
  }

  // 5. 子日志器演示
  console.log('\n5. 子日志器演示');
  const childLogger = logger.child({
    category: 'auth-service',
    metadata: { service: 'authentication', version: '1.0.0' }
  });

  childLogger.info('认证服务初始化完成');
  childLogger.error('认证失败', { username: 'john_doe', reason: 'invalid_password' });

  // 6. HTTP传输器演示(模拟)
  console.log('\n6. HTTP传输器演示(仅创建,不实际发送)');
  const httpTransport = new HTTPTransport({
    url: 'http://localhost:3000/logs',
    batchSize: 5,
    level: LOG_LEVELS.WARN
  });

  const httpLogger = new Logger({
    category: 'http-demo',
    transports: [httpTransport]
  });

  // 模拟一些日志
  httpLogger.warn('这将被发送到HTTP端点');
  httpLogger.error('HTTP传输器错误测试');

  // 7. 自定义格式化器
  console.log('\n7. 自定义格式化器演示');
  const customFormatter = new LogFormatter({
    template: '{timestamp} | {level} | {message}',
    timestampFormat: 'locale',
    colorize: true
  });

  const customLogger = new Logger({
    category: 'custom',
    transports: [
      new ConsoleTransport({
        formatter: customFormatter,
        level: LOG_LEVELS.DEBUG
      })
    ]
  });

  customLogger.info('使用自定义格式的日志消息');
  customLogger.debug('自定义调试信息');

  // 清理资源
  setTimeout(() => {
    console.log('\n清理演示资源...');
    fileLogger.close();
    httpTransport.close();
    
    // 删除演示日志文件
    try {
      const logDir = './logs';
      if (fs.existsSync(logDir)) {
        const files = fs.readdirSync(logDir);
        files.forEach(file => {
          if (file.startsWith('demo.log')) {
            fs.unlinkSync(path.join(logDir, file));
          }
        });
        fs.rmdirSync(logDir);
      }
    } catch (error) {
      console.error('清理文件时出错:', error);
    }
    
    console.log('日志系统演示完成');
  }, 3000);
}

// Express.js 集成中间件
function createLoggerMiddleware(options = {}) {
  const logger = logSystem.getLogger('http', options);
  
  return (req, res, next) => {
    const start = Date.now();
    
    // 记录请求开始
    logger.info('Request started', {
      method: req.method,
      url: req.url,
      ip: req.ip,
      userAgent: req.get('User-Agent')
    });

    // 监听响应完成
    res.on('finish', () => {
      const duration = Date.now() - start;
      const level = res.statusCode >= 400 ? LOG_LEVELS.ERROR : LOG_LEVELS.INFO;
      
      logger.log(level, 'Request completed', {
        method: req.method,
        url: req.url,
        statusCode: res.statusCode,
        duration: `${duration}ms`,
        contentLength: res.get('content-length')
      });
    });

    next();
  };
}

// 如果直接运行此文件,则执行演示
if (require.main === module) {
  demonstrateLogSystem();
}

module.exports = {
  LOG_LEVELS,
  LogFormatter,
  LogTransport,
  ConsoleTransport,
  FileTransport,
  HTTPTransport,
  Logger,
  LogSystem,
  logSystem,
  createLoggerMiddleware,
  demonstrateLogSystem
};

高级特性

1. 日志轮转

  • 按大小轮转: 文件超过指定大小时创建新文件
  • 按时间轮转: 按天、小时轮转日志文件
  • 压缩归档: 旧日志文件压缩存储
  • 自动清理: 删除过期的日志文件

2. 结构化日志

  • JSON格式: 便于机器解析和检索
  • 统一字段: 标准的字段名称和格式
  • 元数据丰富: 包含足够的上下文信息

3. 性能优化

  • 异步写入: 避免阻塞主线程
  • 批量写入: 减少I/O操作次数
  • 缓冲机制: 内存缓冲提升性能
  • 采样日志: 高频日志采样记录

4. 监控告警

  • 错误统计: 统计错误日志数量和类型
  • 性能指标: 记录应用性能数据
  • 告警触发: 根据日志内容触发告警
  • 健康检查: 日志系统自身的健康监控

生产环境考虑:

安全性:

  • 敏感信息过滤: 避免记录密码、令牌等
  • 访问控制: 限制日志文件访问权限
  • 数据脱敏: 对个人信息进行脱敏处理

可靠性:

  • 故障恢复: 传输器故障时的处理机制
  • 数据完整性: 确保日志不丢失
  • 备份策略: 重要日志的备份方案

可扩展性:

  • 插件化架构: 支持自定义传输器和格式化器
  • 配置管理: 支持动态配置更新
  • 多实例支持: 支持分布式应用日志聚合

集成示例:

// Express.js 集成
const express = require('express');
const { logSystem } = require('./log-system');

const app = express();
const logger = logSystem.getLogger('web');

// 请求日志中间件
app.use((req, res, next) => {
const start = Date.now();

res.on('finish', () => {
const duration = Date.now() - start;
logger.info('Request processed', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`
});
});

next();
});

// 错误处理
app.use((error, req, res, next) => {
logger.error('Request error', {
error: error.message,
stack: error.stack,
url: req.url
});
next(error);
});

面试官视角

该题考察候选人的系统设计能力:

  • 要点清单: 理解日志系统架构;掌握设计模式应用;考虑性能和可靠性;有实际开发经验
  • 加分项: 有大型系统日志设计经验;了解ELK等日志栈;考虑分布式日志聚合;有监控告警经验
  • 常见失误: 设计过于简单;不考虑性能问题;忽视安全性;缺乏扩展性设计

延伸阅读