- 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);
};
});
这个方案在开发环境下工作得很好,但面临几个限制:
- 不支持压缩后的代码
- 可能影响性能
- 没有批处理机制
- 不支持异步操作
进阶版本: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
});
最佳实践
- 开发环境配置
const enhancer = new AsyncConsoleEnhancer({
batchSize: 1, // 立即处理
flushInterval: 100, // 快速刷新
sourceMapEnabled: true // 启用 source map
});
- 生产环境配置
const enhancer = new AsyncConsoleEnhancer({
batchSize: 10, // 批量处理
flushInterval: 1000, // 较长刷新间隔
sourceMapEnabled: false // 禁用 source map
});
性能影响分析
CPU 影响
- 异步处理减少主线程阻塞
- 批量处理减少 Source Map 查询次数
- 使用
requestIdleCallback
避免影响用户交互
内存影响
- 队列长度限制防止内存泄漏
- Source Map 缓存控制内存使用
- 提供清理机制释放资源
网络影响
- 缓存 Source Map 减少网络请求
- 按需加载 Source Map
- 利用 Cache API 持久化缓存
总结
通过这个增强方案,我们实现了:
- 准确定位日志来源,即使在压缩代码中
- 最小化性能影响
- 提供可配置的参数适应不同场景
- 实现了优雅降级机制
完整的代码实现可以查看本文开头的示例。这个方案已经在多个生产环境中使用,证明了其可靠性和实用性。