日志记录在前端项目中的实战应用与性能优化策略
我的写法,亲测靠谱
日志记录这事,我干了快八年,前三年是 console.log(1)、console.log(2)、console.log(‘这里好像卡了’),后五年开始被运维拉着骂:你打的日志根本没法查!线上报错找不到上下文,用户反馈“点不动”,我们翻了二十分钟日志才定位到是某个 Promise 被 silent reject 了,但 log 里连个 traceId 都没有。
现在我团队里所有前端项目(React/Vue/纯 JS)都用这套轻量级日志方案,不依赖任何第三方 SDK(比如 Sentry 的 logger 模块太重,且默认行为容易误打敏感字段),核心就一个 Logger 类,50 行搞定,稳定跑了两年多,没出过漏打、乱序、污染 console 的问题。
先上核心代码:
class Logger {
constructor(prefix = '', options = {}) {
this.prefix = prefix;
this.level = options.level || 'info'; // 'debug' | 'info' | 'warn' | 'error'
this.enabled = options.enabled !== false;
this.silentInProd = options.silentInProd !== false;
}
_shouldLog(level) {
const levels = ['debug', 'info', 'warn', 'error'];
const currentLevelIndex = levels.indexOf(this.level);
const levelIndex = levels.indexOf(level);
return this.enabled &&
levelIndex >= currentLevelIndex &&
!(this.silentInProd && process.env.NODE_ENV === 'production' && level === 'debug');
}
debug(...args) {
if (this._shouldLog('debug')) {
console.debug([${this.prefix}], ...args);
}
}
info(...args) {
if (this._shouldLog('info')) {
console.info([${this.prefix}], ...args);
}
}
warn(...args) {
if (this._shouldLog('warn')) {
console.warn([${this.prefix}], ...args);
}
}
error(...args) {
if (this._shouldLog('error')) {
const err = args.find(arg => arg instanceof Error);
if (err && err.stack) {
// ✅ 关键:保留原始 stack,避免被 console.error 吞掉
console.error([${this.prefix}], ...args.filter(a => a !== err), err);
} else {
console.error([${this.prefix}], ...args);
}
}
}
}
// 全局实例(按模块创建)
const apiLogger = new Logger('API', { level: 'info' });
const authLogger = new Logger('AUTH', { level: 'debug' });
// 使用示例
apiLogger.info('fetch start', { url: '/user/profile' });
apiLogger.error('fetch failed', { url: '/user/profile', status: 503, traceId: 'tr-abc123' });
为什么这么写?三点最实在的体会:
- prefix 是命门:不加前缀的 log 就是垃圾。上线后 grep 日志全靠它,
grep "API" app.log和grep "AUTH" app.log直接分层,比看时间戳靠谱十倍; - error 方法必须透传 Error 实例:我踩过三次坑——第一次直接
console.error(msg, err.toString()),stack 全丢了;第二次console.error(msg, { ...err }),非 enumerable 属性(比如 stack、lineNumber)全没了;最后这个写法,让浏览器 DevTools 点开就能跳转源码,救命; - production 下自动降级 debug 日志:不是为了性能(console.debug 几乎没开销),而是防 QA 或用户手贱打开控制台,看到一堆 debug 日志以为系统崩了——真有用户截图发微博说“你们网站在疯狂报错”,结果全是
debug("user token refreshed")……
这几种错误写法,别再踩坑了
下面这些,都是我亲手写过、被拉去喝茶、改了三版才收敛的反面案例:
❌ 错误 1:把敏感字段直接塞进 log
比如登录成功后:
console.log('login success', { token: res.data.token, user: res.data.user });
token 是 JWT?base64 编码后就是明文,日志一落盘,全公司都能 grep 出来。更糟的是,有些日志系统会自动上报到中心平台(比如 ELK),等于把密钥贴在公告栏上。
✅ 我的处理方式:所有可能含敏感信息的对象,统一走脱敏函数:
function sanitize(obj) {
if (!obj || typeof obj !== 'object') return obj;
const result = { ...obj };
if (result.token) result.token = '[REDACTED]';
if (result.password) result.password = '[REDACTED]';
if (result.idCard) result.idCard = result.idCard.replace(/(d{4})d{10}(d{4})/, '$1******$2');
return result;
}
apiLogger.info('login success', sanitize({ token: res.data.token, user: res.data.user }));
❌ 错误 2:log 里拼字符串,而不是传参
比如:
console.log('[API] fetch /user/' + userId + ' failed: ' + err.message);
问题有二:一是字符串拼接丢失类型信息(userId 是 number 还是 string?err.message 为空时会不会报错?),二是无法被结构化日志系统(如 Datadog)提取字段。DevOps 后期想按 userId 统计失败率,发现全是字符串,正则都写吐了。
✅ 我现在只传参数,不拼串:console.info('[API]', { url: /user/${userId}, error: err.message }),结构清晰,字段可索引。
❌ 错误 3:在 try/catch 外层统一兜底 log
写个全局 error handler,然后所有错误都打一条 catch all error,看似省事,实际等于没 log。因为丢失了调用栈、丢失了业务上下文、丢失了那个关键的 traceId。
✅ 我的做法是:每个异步链路自己负责自己的 error log,比如:
async function loadUserProfile(id) {
const traceId = generateTraceId();
apiLogger.info('loadUserProfile start', { id, traceId });
try {
const res = await fetch(/api/user/${id});
if (!res.ok) throw new Error(HTTP ${res.status});
const data = await res.json();
apiLogger.info('loadUserProfile success', { id, traceId, data });
return data;
} catch (err) {
apiLogger.error('loadUserProfile failed', { id, traceId, error: err });
throw err; // 不吞异常
}
}
实际项目中的坑
我们有个后台管理系统,用了微前端架构(qiankun),主应用和子应用各自打日志。一开始子应用也 new 一个 Logger,prefix 设为 ‘SUB-AUTH’,结果上线后发现:子应用的 error 日志在主应用控制台里不显示——因为 qiankun 默认隔离了 console API(沙箱机制)。折腾半天,最后方案是:子应用通过 props 把 logger 实例传进来,或者直接 postMessage 到主应用,由主应用统一打。
还有个坑:Safari 对 console.group 的支持不稳定,尤其配合 source map 时,折叠组经常展开不了。所以现在我一律不用 group,改用 prefix + emoji 区分层级:
apiLogger.info('🔑 auth init');
apiLogger.info('📦 fetch config');
apiLogger.info('⚡️ send login req');
最后提一句:日志不是越多越好。我们曾经在表单提交前加了一行 console.log('form submit triggered'),结果用户狂点提交按钮,日志刷屏,反而掩盖了真正的报错。现在规则是:只在**分支判断点、异步入口/出口、异常捕获点**打 log,其他地方免谈。
结尾
以上是我这些年在日志记录上踩出来的最佳实践。没搞什么花哨的采样、分级上报、日志聚合——那些是后端或 SRE 的活。前端要做的,就三件事:打得准(位置对)、打得清(结构明)、打得安全(不泄密)。
这套方案不是最优解,但它简单、可控、排查快。我们最近也在试水把部分关键 error 自动上报到 https://jztheme.com/api/log(仅作技术演示用),但还没全量,因为得先跑通 GDPR 合规流程……这事下次再聊。
如果你有更好的日志组织方式,比如怎么优雅地做前端日志采样、怎么对接 OpenTelemetry、或者怎么让 QA 一键复制当前页面所有日志——欢迎评论区砸过来,我真会看,也会试。

暂无评论