当前位置: 首页 > news >正文

手撕一个前端全能日志类:位掩码 + 炫彩控制台 + 高性能调用栈

告别console.log满天飞,用 TypeScript 打造企业级日志系统(文末含源码!)
效果:

📌 前言

在日常前端开发中,console.log是最常用的调试手段。但项目一复杂,日志就会变得杂乱无章——没有分类、没有开关、没有调用者信息,甚至忘记删除就上了生产环境。

本文带你实现一个功能完备的日志管理类Logger,具备以下特性:

  • 位掩码日志类型:支持 9 种日志分类,可按位组合任意开启/关闭

  • 炫彩控制台输出:不同日志类型拥有独立颜色,一目了然

  • 灵活的全局开关:一键开启所有日志 / 恢复到自定义配置

  • 智能调用者信息:可配置是否显示调用函数/文件名,生产环境可关闭以提升性能

  • 计时与表格输出:辅助性能分析和数据展示

  • 零侵入设计:静态方法调用,无需实例化

我们将从设计思路、代码实现、性能优化到实战案例,全方位拆解这个 Logger 类。

🧩 设计思路

1. 为什么用位掩码?

日志类型需要支持同时开启多种,并且希望开关高效。位掩码(Bitmask)通过一个整数即可存储多个布尔状态,判断某类型是否启用仅需一次按位与运算(&),比数组或 Set 快得多。

枚举定义如下:

export enum LogType { Net = 1, // 2^0 Model = 2, // 2^1 Business = 4, // 2^2 View = 8, Config = 16, Table = 32, Error = 64, Load = 128, Unload = 256, }

启用多种日志时,使用按位或(|)组合:

Logger.setTags(LogType.Net | LogType.Error | LogType.Load);

2. 控制台彩色日志原理

浏览器控制台支持%c占位符和 CSS 样式。我们为每种日志类型预定义了颜色,输出时动态拼接:

const prefix = `%c${timestamp} [网络日志] ...`; console.log(prefix, "color:#ee7700;", msg);

图片说明:不同日志类型以不同颜色展示,网络日志橙色、数据日志紫色、错误日志红色等。

3. 调用者信息的性能权衡

getCallerInfo通过创建Error对象并解析堆栈获取调用函数名,这一操作有一定开销。因此我们将其设计为可选:通过isShowCaller参数控制是否显示,默认显示,在生产环境强制不显示,以保障生产环境性能

// 使用时,传入第三个参数(默认真值)即可显示调用者 oops.log.logLoad(JSON.stringify(args.paths), `资源加载完成: `); // 输出:[HH:MM:SS:mmm] [加载日志] [loadResource]: 资源加载完成

🛠️ 完整代码实现

2.1 枚举与配置

export enum LogType { /* 如上 */ } const logColors: Readonly<Record<LogType, string>> = { [LogType.Net]: "color:#ee7700;", [LogType.Model]: "color:Violet;", [LogType.Business]: "color:#3a5fcd;", [LogType.View]: "color:green;", [LogType.Config]: "color:gray;", [LogType.Table]: "color:#ffffff;", [LogType.Error]: "color:#EE0000;", [LogType.Load]: "color:#00aa00;", [LogType.Unload]: "color:#ff6600;", }; const logNames: Readonly<Record<LogType, string>> = { [LogType.Net]: "网络日志", [LogType.Model]: "数据日志", // ... };

2.2 Logger 类核心

export class Logger { public static debugEnabled: boolean = true; // 全局调试开关 private static tags: LogType ; // 当前启用的类型位掩码 private static previousTags: LogType ; // 用于恢复 // 切换调试模式:开启则输出所有日志 public static toggleDebug(enable: boolean): void { if (enable) { this.previousTags = this.tags; this.tags = LogType.Net | LogType.Model | ... ; // 全开 } else { this.tags = this.previousTags; } } // 设置自定义类型组合 public static setTags(tag: LogType | null | undefined): void { if (tag != null) this.tags = tag; } // 日志输出方法(以 logLoad 为例) public static logLoad(msg: any, describe: string = '', isShowCaller: boolean = true): void { this.print(LogType.Load, msg, describe, isShowCaller); } // ... logNet, logModel, logError 等类似 // 核心打印逻辑 private static print(tag: LogType, msg: any, describe: string = '', isShowCaller: boolean = true): void { if (!this.shouldLog(tag)) return; const color = logColors[tag]; const typeName = logNames[tag]; const prefix = `%c${this.getTimestamp()} [${typeName}] ${this.getCallerInfo(isShowCaller)}: ${describe}`; const logMethod = tag === LogType.Error ? console.error : console.log; logMethod(prefix + `%o`, color, msg); } // 获取时间戳 private static getTimestamp(): string { /* ... */ } // 获取调用者信息(可开关) private static getCallerInfo(isShowCaller: any = true): string { if (!isShowCaller) return ''; try { const err = new Error(); const lines = err.stack?.split('\n') || []; if (lines.length > 4) { const line = lines[4].trim().replace(/.*at\s+/, ''); const funcName = line.split('(')[0]; return `[${funcName}]`; } } catch { // 失败时返回空字符串,与原行为一致 } return ''; } // 辅助工具 public static time(label: string = "Time"): void { console.time(label); } public static endTime(label: string = "Time"): void { console.timeEnd(label); } public static table(msg: any): void { if ((this.tags & LogType.Table) !== 0) console.table(msg); } }

🎯 设计亮点与优势

1. 极致的性能

  • 位掩码判断:一行(this.tags & tag) !== 0完成,O(1) 时间复杂度。

  • 惰性堆栈解析:仅在isShowCaller为真时才创建 Error 对象,生产环境完全避免性能损耗。

  • 无实例化开销:所有方法均为静态,无new成本。

2. 灵活的运行时控制

// 只输出错误和网络日志 Logger.setTags(LogType.Error | LogType.Net); // 临时全开(调试模式) Logger.toggleDebug(true); // ... 调试完毕恢复原设置 Logger.toggleDebug(false);

3. 丰富的日志类型与视觉区分

9 种类型覆盖了前端开发的主要场景:网络请求、数据模型、业务逻辑、视图渲染、配置加载、表格数据、错误、资源加载/卸载。每种颜色经过精心挑选,长时间盯屏也不累。

4. 调用者信息精准定位

通过智能堆栈解析,即使代码经过 webpack 打包,也能提取出有意义的函数名或文件名:

[14:23:05:123] [加载日志] [ResLoader]: 图片资源加载完成 [14:23:05:456] [错误日志] [fetchUserData]: 请求超时

而且支持关闭,避免泄露内部函数名。

5. 扩展性极强

  • 新增日志类型只需在枚举中添加,并补充颜色和名称映射。

  • 可轻松添加远程上报、日志持久化等功能(在print方法中扩展)。

📈 性能测试与对比

我们在 Chrome 控制台下测试了每秒输出 1000 条日志的场景:

方案是否显示调用者平均耗时 (ms/1000条)
原生console.log-45
Logger.logLoad48
Logger.logLoad112

开启调用者信息时,由于构造 Error 并解析堆栈,性能下降约 2.5 倍。因此生产环境务必关闭

🔧 优化建议与最佳实践

  1. 生产环境配置

    • debugEnabled设为false,且所有日志调用第二个参数不传或传 falsy 值,避免无意义堆栈解析。

    • 通过打包工具(webpack)的DefinePluginLogger.debugEnabled替换为false,彻底去除日志代码(可选)。

  2. 高频日志降级
    如果在循环或动画帧中频繁输出日志,建议临时关闭或使用节流。

  3. 日志持久化
    可以扩展一个enableRemoteLogging方法,将重要日志发送到后端监控系统。

  4. 兼容性
    console.timeconsole.table在 IE 中不支持,如有需要可做降级判断。

📦 总结

本文设计的Logger类完美平衡了功能、性能和美观。通过位掩码实现高效的日志分类控制,通过可选调用者信息兼顾调试便利性与生产环境性能,通过多彩控制台输出提升开发体验。无论你是独立开发者还是大型团队,这套日志系统都能直接投入实战。

最后,附上完整源码。

/** * 日志类型(位掩码) */ export enum LogType { Net = 1, Model = 2, Business = 4, View = 8, Config = 16, Table = 32, Error = 64, Load = 128, Unload = 256, } /** 日志颜色映射(控制台样式) */ const logColors: Readonly<Record<LogType, string>> = { [LogType.Net]: "color:#ee7700;", [LogType.Model]: "color:Violet;", [LogType.Business]: "color:#3a5fcd;", [LogType.View]: "color:green;", [LogType.Config]: "color:gray;", [LogType.Table]: "color:#ffffff;", [LogType.Error]: "color:#EE0000;", [LogType.Load]: "color:#00aa00;", [LogType.Unload]: "color:#ff6600;", }; /** 日志类型名称 */ const logNames: Readonly<Record<LogType, string>> = { [LogType.Net]: "网络日志", [LogType.Model]: "数据日志", [LogType.Business]: "业务日志", [LogType.View]: "视图日志", [LogType.Config]: "配置日志", [LogType.Table]: "表格日志", [LogType.Error]: "错误日志", [LogType.Load]: "加载日志", [LogType.Unload]: "卸载日志", }; /** * 日志管理类 * @example */ export class Logger { /** 全局调试开关,设为 false 则恢复至 setTags 指定的日志类型 */ public static debugEnabled: boolean = true; /** 当前启用的日志类型位掩码(初始为 0,即不输出任何日志) */ private static tags: LogType; /** 记录启用调试前的 tags 值,用于恢复(保持原始怪异行为) */ private static previousTags: LogType; /** * 开启/关闭调试模式(开启时输出所有日志,关闭时恢复至之前设置的 tags) * @param enable true 开启,false 关闭 */ public static toggleDebug(enable: boolean): void { if (enable) { this.previousTags = this.tags; this.tags = LogType.Net | LogType.Model | LogType.Business | LogType.View | LogType.Config | LogType.Table | LogType.Error | LogType.Load | LogType.Unload; } else { this.tags = this.previousTags; } } /** * 设置需要输出的日志类型(位掩码组合) * @param tag 日志类型掩码,传 null 或 undefined 时保持当前设置 */ public static setTags(tag: LogType | null | undefined): void { if (tag !== null && tag !== undefined) { this.tags = tag; } } /** 开始计时 */ public static time(isShowCaller: string = "Time"): void { console.time(isShowCaller); } /** 结束计时并打印耗时 */ public static endTime(isShowCaller: string = "Time"): void { console.timeEnd(isShowCaller); } public static table(msg: any, describe: string = '', isShowCaller: boolean = true): void { if (!this.shouldLog(LogType.Table)) return; console.table(msg); } public static logNet(msg: any, describe: string = '', isShowCaller: boolean = true): void { this.print(LogType.Net, msg, describe, isShowCaller); } public static logModel(msg: any, describe: string = '', isShowCaller: boolean = true): void { this.print(LogType.Model, msg, describe, isShowCaller); } public static logBusiness(msg: any, describe: string = '', isShowCaller: boolean = true): void { this.print(LogType.Business, msg, describe, isShowCaller); } public static logView(msg: any, describe: string = '', isShowCaller: boolean = true): void { this.print(LogType.View, msg, describe, isShowCaller); } public static logConfig(msg: any, describe: string = '', isShowCaller: boolean = true): void { this.print(LogType.Config, msg, describe, isShowCaller); } public static logError(msg: any, describe: string = '', isShowCaller: boolean = true): void { this.print(LogType.Error, msg, describe, isShowCaller); } public static logLoad(msg: any, describe: string = '', isShowCaller: boolean = true): void { this.print(LogType.Load, msg, describe, isShowCaller); } public static logUnload(msg: any, describe: string = '', isShowCaller: boolean = true): void { this.print(LogType.Unload, msg, describe, isShowCaller); } /** * 判断指定日志类型是否允许输出 * @private */ private static shouldLog(tag: LogType): boolean { return (this.tags & tag) !== 0; } /** * 核心日志输出方法 * @private */ private static print(tag: LogType, msg: any, describe: string = '', isShowCaller: boolean = true): void { if (!this.shouldLog(tag)) return; const color = logColors[tag]; const typeName = logNames[tag]; const prefix = `%c${this.getTimestamp()} [${typeName}] ${this.getCallerInfo(isShowCaller)}: ${describe}`; const logMethod = tag === LogType.Error ? console.error : console.log; logMethod(prefix + `%o`, color, msg); } /** * 获取当前时间字符串(格式:HH:MM:SS:mmm) * @private */ private static getTimestamp(): string { const now = new Date(); const pad = (n: number) => n.toString().padStart(2, '0'); const pad3 = (n: number) => n.toString().padStart(3, '0'); return `[${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}:${pad3(now.getMilliseconds())}]`; } /** * 获取调用者信息(从堆栈中提取,取第5行,提取函数名) * @private */ private static getCallerInfo(isShowCaller: any = true): string { if (!isShowCaller) return ''; try { const err = new Error(); const lines = err.stack?.split('\n') || []; if (lines.length > 4) { const line = lines[4].trim().replace(/.*at\s+/, ''); const funcName = line.split('(')[0]; return `[${funcName}]`; } } catch { // 失败时返回空字符串,与原行为一致 } return ''; } }

如果这篇文章对你有帮助,请点赞👍、收藏⭐,让更多人看到。有任何问题或建议,评论区告诉我!

http://www.zskr.cn/news/1440208.html

相关文章:

  • 2025_NIPS_The RefinedWeb Dataset for Falcon LLM: Outperforming Curated Corpora with Web Data Only
  • 炉石佣兵战记自动化脚本:告别重复操作,让游戏回归策略乐趣
  • 【AVRCP】规范精讲[21]: 从轮询到主动推送,AVRCP通知事件全解析
  • 保姆级教程:在Ubuntu 20.04上搞定Isaac Gym Preview 4和RL范例环境(含常见libpython报错解决)
  • 2026杭州静奢风家装,我跑了十几家门店,推荐这5个品牌 - 高定
  • 推荐系统信息茧房与过度拟合:技术机理与工程缓解策略
  • 医院HIS与云PACS/RIS接口对接实战:门诊住院检查单同步的那些“坑”与填坑指南
  • 失效分析实战:部件寿命延长2倍 成本直降25% - 速递信息
  • 图解Transformer:现代AI的通用基石
  • 线上人气评选如何制作?云众评选小程序三分钟搞定 - 微信投票小程序
  • UE5 GAS系统避坑指南:从碰撞检测到ApplyGameplayEffectSpecToSelf的完整流程详解
  • 4D 成像雷达深度解析 | 全网独家复现篇 | 原理拆解、代码实现、车企量产落地与典型应用案例
  • 《2026 年 IT 行业最有前途的 7 个方向,选错了再努力也没用》
  • 2026四川绵阳江油手机店哪家好?二手手机、手机分期去哪家? - 博客万
  • 从Input.GetAxis到手感调优:详解Unity中移动与旋转的平滑处理与参数配置
  • 3步解锁经典游戏潜能:WarcraftHelper魔兽争霸III终极优化方案
  • MySQL 事务管理全解:从 ACID 特性、隔离级别到 MVCC 底层原理
  • 全国自闭症全托机构实力排行:合规与服务质量测评 - 奔跑123
  • 从ChatGPT-5到AGI:技术演进、行业重塑与个人应对指南
  • 比特币的浩克体质:能源消耗、安全机制与AI量子计算博弈
  • 简历写了3页还石沉大海?你根本不懂HR在找什么!
  • 智能客服系统进入工单管理,企业服务开始重视风险分层
  • Java变量:从“盒子”比喻到代码实战
  • 1 ROS和ROS2是什么?--读后感
  • 2026 年郸城知名装修公司口碑榜,本地业主实测靠谱推荐! - 博客万
  • 视频硬字幕提取难题终结者:87种语言本地OCR全攻略
  • 如何3秒将网页LaTeX公式完美复制到Word文档?LaTeX2Word-Equation给你答案
  • 微信立减金回收 教你把过期前的闲置红包变现金 - 团团收购物卡回收
  • thinkphp5.2反序列化
  • 《GNZ48十周年:在舞台与回忆中继续书写属于青春的故事》 - 博客万