y.y
Published on

Enhanced Console Logging: From Basic to Production-Ready

Enhanced Console Logging: From Basic to Production-Ready

日常开发中,我们经常会遇到需要定位 console.log 输出来自哪里的情况。特别是在处理压缩后的代码时,这个问题变得更加棘手。本文将介绍如何从一个基础的 console 增强方案逐步优化到一个生产可用的解决方案。

基础版本:定位未压缩代码

最初的想法来自 Remy Sharp 的博客,通过重写 console 方法来显示调用位置:

['log', 'warn'].forEach(function(method) {
  var old = console[method];
  console[method] = function() {
    var stack = (new Error()).stack.split(/\n/);
    // Chrome includes a single "Error" line, FF doesn't.
    if (stack[0].indexOf('Error') === 0) {
      stack = stack.slice(1);
    }
    var args = [].slice.apply(arguments).concat([stack[1].trim()]);
    return old.apply(console, args);
  };
});

这个方案在开发环境下工作得很好,但面临几个限制:

  1. 不支持压缩后的代码
  2. 可能影响性能
  3. 没有批处理机制
  4. 不支持异步操作

进阶版本:Source Map 支持

为了解决压缩代码的问题,我们需要添加 Source Map 支持:

async function getOriginalLocation(frame) {
    const match = frame.match(/at\s+.+?\s+\((.+):(\d+):(\d+)\)/);
    if (!match) return frame;

    const [, fileUrl, line, column] = match;
    const consumer = await loadSourceMap(fileUrl);
    if (!consumer) return frame;

    const original = consumer.originalPositionFor({
        line: parseInt(line, 10),
        column: parseInt(column, 10)
    });

    return original.source ?
        `    at ${original.name || 'anonymous'} (${original.source}:${original.line}:${original.column})` :
        frame;
}

生产就绪版本:性能优化

最终的生产版本需要考虑以下几个关键点:

1. 异步处理和批量操作

使用队列和批处理来优化性能:

class AsyncConsoleEnhancer {
    constructor(options = {}) {
        this.logQueue = [];
        this.options = {
            batchSize: options.batchSize || 10,
            flushInterval: options.flushInterval || 1000,
            maxQueueSize: options.maxQueueSize || 1000
        };
    }

    async processQueue() {
        if (this.logQueue.length === 0) return;
        const batch = this.logQueue.splice(0, this.options.batchSize);
        await Promise.all(batch.map(entry => this.processLogEntry(entry)));
    }
}

2. Source Map 缓存

利用浏览器的 Cache API 来缓存 Source Map:

async loadSourceMap(fileUrl) {
    if (this.sourceMapCache.has(fileUrl)) {
        return this.sourceMapCache.get(fileUrl);
    }

    try {
        const cached = await caches?.open('source-map-cache')
            .then(cache => cache.match(fileUrl + '.map'))
            .then(response => response?.json())
            .catch(() => null);

        if (cached) {
            const consumer = await new sourceMap.SourceMapConsumer(cached);
            this.sourceMapCache.set(fileUrl, consumer);
            return consumer;
        }

        // ... 加载和缓存新的 source map
    } catch (err) {
        return null;
    }
}

3. 性能优化措施

  • 使用 requestIdleCallback 在浏览器空闲时处理日志
  • 设置队列大小限制
  • 实现优雅降级机制
  • 定期强制刷新队列

4. 完整的配置选项

const enhancer = new AsyncConsoleEnhancer({
    batchSize: 10,              // 每批处理的日志数量
    flushInterval: 1000,        // 强制刷新间隔(ms)
    maxQueueSize: 1000,         // 队列最大长度
    sourceMapEnabled: true      // 是否启用 source map
});

最佳实践

  1. 开发环境配置
const enhancer = new AsyncConsoleEnhancer({
    batchSize: 1,              // 立即处理
    flushInterval: 100,        // 快速刷新
    sourceMapEnabled: true     // 启用 source map
});
  1. 生产环境配置
const enhancer = new AsyncConsoleEnhancer({
    batchSize: 10,             // 批量处理
    flushInterval: 1000,       // 较长刷新间隔
    sourceMapEnabled: false    // 禁用 source map
});

性能影响分析

  1. CPU 影响

    • 异步处理减少主线程阻塞
    • 批量处理减少 Source Map 查询次数
    • 使用 requestIdleCallback 避免影响用户交互
  2. 内存影响

    • 队列长度限制防止内存泄漏
    • Source Map 缓存控制内存使用
    • 提供清理机制释放资源
  3. 网络影响

    • 缓存 Source Map 减少网络请求
    • 按需加载 Source Map
    • 利用 Cache API 持久化缓存

总结

通过这个增强方案,我们实现了:

  1. 准确定位日志来源,即使在压缩代码中
  2. 最小化性能影响
  3. 提供可配置的参数适应不同场景
  4. 实现了优雅降级机制

完整的代码实现可以查看本文开头的示例。这个方案已经在多个生产环境中使用,证明了其可靠性和实用性。

enhanced-console

参考资料

  1. Where is that console.log? - Remy Sharp's blog
  2. Source Map Revision 3 Proposal
  3. MDN - Source Maps
  4. Using the Console API