浏览器原生的 console.log 虽然简单易用,但在大型应用中会遇到诸多问题:所有模块的日志混在一起、无法按需开关、生产环境难以调试。本文实现了一个基于模块命名空间和级别过滤的日志系统。
浏览器原生日志能力
浏览器的 Console API 提供了一系列强大的日志方法,这些方法在所有现代浏览器中都有良好的支持:
基础 API
// 基础日志输出
console.log('普通日志')
console.info('信息日志')
console.warn('警告日志')
console.error('错误日志')
console.debug('调试日志')
// 调用栈追踪
console.trace('追踪调用链')
// 分组显示
console.group('分组标题')
console.log('分组内容')
console.groupEnd()
console.groupCollapsed('折叠分组') // 默认折叠
// 表格展示
console.table([{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }])
// 计时器
console.time('操作耗时')
// ... 一些操作
console.timeEnd('操作耗时') // 输出: 操作耗时: 123.45ms
// 断言
console.assert(value > 0, 'Value must be positive')
// 清空控制台
console.clear()
浏览器日志的痛点
尽管浏览器提供了丰富的 API,但在实际开发中仍存在诸多问题:
1. 缺乏级别控制
// 问题:无法根据环境动态控制日志输出
console.log('开发时需要,生产环境不需要的调试信息')
// 只能通过手动注释或删除代码
// console.log('临时注释掉的日志')
生产环境中,大量 console.log 会:
- 影响性能(序列化对象、格式化输出)
- 暴露敏感信息(用户数据、业务逻辑)
- 产生噪音干扰(用户打开 DevTools 看到大量无关日志)
2. 缺乏模块隔离
console.log('来自认证模块')
console.log('来自网络模块')
console.log('来自渲染模块')
// 在控制台中:
// 来自认证模块
// 来自网络模块
// 来自渲染模块
// ❌ 无法快速定位特定模块的问题
当应用规模扩大时:
- 数百个模块的日志交织在一起
- 无法只查看特定模块的日志
- 难以追踪问题的来源
3. 生产环境难以调试
// 问题:生产环境出现问题时
// - 所有 console.log 都被删除了 → 无法排查
// - 所有 console.log 都保留了 → 找不到有用信息
// 希望能够:
// ✅ 动态开启特定模块的日志
我们需要什么
一个实用的日志系统需要两大核心能力:
1. 模块命名空间(Namespace)
- 每条日志都知道来自哪个模块
- 可以针对特定模块开启详细日志
- 支持模块级别的独立控制
2. 级别过滤(Level Filtering)
- 开发环境看所有日志,生产环境只看关键信息
- 运行时动态调整,无需重启应用
- 避免不必要的性能开销
核心实现
机制一:级别过滤
enum Level {
Debug = 'debug',
Info = 'info',
Warn = 'warn',
Error = 'error',
}
const LOG_LEVEL_PRIORITY = {
[Level.Debug]: 0,
[Level.Info]: 1,
[Level.Warn]: 2,
[Level.Error]: 3,
}
// 核心过滤逻辑
shouldLog(level: Level): boolean {
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[logger.level]
}
使用效果:
const log = createLogger('auth', Level.Info)
log.debug('不显示') // Debug < Info
log.info('用户登录', { userId: 123 }) // 显示
log.error('登录失败') // 显示
// 动态调整
log.level = Level.Debug
log.debug('现在显示了')
机制二:模块命名空间
const loggers: Record<string, Logger> = {}
function createLogger(moduleName: string, defaultLevel = Level.Info): Logger {
if (loggers[moduleName]) {
return loggers[moduleName]
}
const logger = {
level: defaultLevel,
debug: wrapMethod(console.log, Level.Debug),
info: wrapMethod(console.info, Level.Info),
warn: wrapMethod(console.warn, Level.Warn),
error: wrapMethod(console.error, Level.Error),
}
function wrapMethod(method: (...args: unknown[]) => void, level: Level) {
return (...args: unknown[]) => {
if (!logger.shouldLog(level)) return
const prefix = `[${level.toUpperCase()[0]}][${moduleName}]`
method(prefix, ...args)
}
}
loggers[moduleName] = logger
return logger
}
使用效果:
const authLog = createLogger('auth')
const networkLog = createLogger('network')
authLog.info('User login') // [I][auth] User login
networkLog.debug('API called') // [D][network] API called
机制三:全局控制接口
globalThis.__logger = {
list: () => Object.keys(loggers),
setLevel: (name: string, level: Level) => {
if (loggers[name]) {
loggers[name].level = level
}
},
}
控制级别
// 浏览器控制台
__logger.list() // ['auth', 'network']
__logger.setLevel('payment', 'debug') // 开启支付模块详细日志
源代码
enum Level {
Debug = 'debug',
Info = 'info',
Warn = 'warn',
Error = 'error',
}
const LOG_LEVEL_PRIORITY = {
[Level.Debug]: 0,
[Level.Info]: 1,
[Level.Warn]: 2,
[Level.Error]: 3,
}
interface Logger {
level: Level
debug: (...args: unknown[]) => void
info: (...args: unknown[]) => void
warn: (...args: unknown[]) => void
error: (...args: unknown[]) => void
debugGroup: (...args: unknown[]) => void
debugGroupEnd: () => void
shouldLog: (level: Level) => boolean
}
const loggers: Record<string, Logger> = {}
/**
* 创建或获取一个模块的日志记录器
* 解决痛点2:缺乏模块隔离
*/
function createLogger(moduleName: string, defaultLevel = Level.Info): Logger {
if (loggers[moduleName]) {
return loggers[moduleName]
}
const logger = {
level: defaultLevel,
shouldLog(level: Level): boolean {
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[logger.level]
},
debug: wrapMethod(console.log, Level.Debug),
info: wrapMethod(console.info, Level.Info),
warn: wrapMethod(console.warn, Level.Warn),
error: wrapMethod(console.error, Level.Error),
debugGroup: wrapMethod(console.group, Level.Debug),
debugGroupEnd: wrapMethod(console.groupEnd, Level.Debug),
}
function wrapMethod(method: (...args: unknown[]) => void, level: Level): (...args: unknown[]) => void {
return (...args: unknown[]) => {
// 解决痛点1:缺乏级别控制
if (!logger.shouldLog(level)) {
return
}
// 解决痛点3:格式不统一
// 部分解决痛点4:添加模块名上下文(但时间戳等仍需手动传入)
const prefix = `[${level.toUpperCase()[0]}][${moduleName}]`
method(prefix, ...args)
}
}
loggers[moduleName] = logger
return logger
}
globalThis.__logger = {
list: () => Object.keys(loggers),
get: (name: string) => loggers[name],
setLevel: (name: string, level: Level) => {
if (loggers[name]) {
loggers[name].level = level
console.log(`✅ ${name} 日志级别已设置为 ${level}`)
} else {
console.warn(`⚠️ 模块 ${name} 不存在`)
}
},
}