告别console.log!UniApp中打造一个媲美专业框架的日志系统(支持Vue3/小程序)
告别console.log!UniApp中打造一个媲美专业框架的日志系统(支持Vue3/小程序)
在开发中大型跨端应用时,日志系统的重要性往往被低估。许多开发者习惯性地使用console.log进行调试,但当应用上线后,面对用户反馈的"无法复现"的问题时,却束手无策。一个完善的日志系统不仅能帮助开发者快速定位问题,还能提供用户行为分析、性能监控等额外价值。
本文将带你从零构建一个支持多平台(App、小程序、H5)、具备日志分级、环境感知、异步写入等特性的完整日志解决方案。不同于简单的文件读写,这套系统借鉴了主流框架的设计理念,同时针对UniApp的跨端特性进行了深度优化。
1. 为什么需要专业的日志系统
console.log在开发阶段确实方便,但在生产环境却存在诸多局限:
- 无法持久化:控制台日志在页面刷新后即消失
- 缺乏结构化:难以区分日志级别和来源
- 跨平台差异:小程序和H5端的表现与App端不一致
- 性能隐患:同步写入可能阻塞UI线程
一个专业的日志系统应该具备以下核心能力:
| 特性 | 说明 | 必要性 |
|---|---|---|
| 分级日志 | DEBUG/INFO/WARN/ERROR等级别 | 便于过滤重要信息 |
| 环境感知 | 开发环境输出到控制台,生产环境持久化 | 兼顾调试与生产需求 |
| 异步写入 | 避免日志操作阻塞主线程 | 保障应用流畅性 |
| 跨端兼容 | 一套代码适配App、小程序、H5 | 减少维护成本 |
| 日志轮转 | 自动清理过期日志 | 防止存储空间耗尽 |
2. 核心架构设计
2.1 模块划分
我们采用分层设计,将系统划分为以下几个模块:
// 类型定义 interface LogEntry { level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' message: string timestamp: number context?: Record<string, unknown> } class Logger { private transports: Transport[] = [] log(entry: LogEntry): void { // 异步处理日志 Promise.resolve().then(() => { this.transports.forEach(t => t.write(entry)) }) } addTransport(transport: Transport): void { this.transports.push(transport) } } interface Transport { write(entry: LogEntry): Promise<void> }2.2 平台适配策略
针对不同平台,我们采用条件编译和策略模式实现无缝适配:
// 根据不同平台初始化不同的传输器 function createTransport() { // #ifdef APP-PLUS return new AppFileTransport() // #endif // #ifdef MP-WEIXIN return new MiniProgramTransport() // #endif // #ifdef H5 return new BrowserTransport() // #endif }3. 实现关键功能
3.1 日志分级管理
我们实现四个标准日志级别,并允许自定义级别:
const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 } class Logger { private level: number = LEVELS.DEBUG setLevel(level: keyof typeof LEVELS): void { this.level = LEVELS[level] } debug(...args: any[]): void { if (this.level <= LEVELS.DEBUG) { this._log('DEBUG', args) } } // 其他级别方法类似... }3.2 高性能异步写入
为避免阻塞UI,我们采用微任务队列+批量写入策略:
class AppFileTransport implements Transport { private queue: LogEntry[] = [] private isWriting = false async write(entry: LogEntry): Promise<void> { this.queue.push(entry) if (!this.isWriting) { this.isWriting = true await this.flush() } } private async flush(): Promise<void> { while (this.queue.length > 0) { const batch = this.queue.splice(0, 10) await this.writeToFile(batch) } this.isWriting = false } }3.3 智能日志轮转
自动清理过期日志,防止存储空间耗尽:
class LogRotator { constructor( private maxDays: number = 7, private maxSize: number = 1024 * 1024 * 5 // 5MB ) {} async rotate(): Promise<void> { const files = await this.listLogFiles() const now = Date.now() for (const file of files) { if (this.shouldRotate(file, now)) { await this.compressAndRemove(file) } } } private shouldRotate(file: LogFile, now: number): boolean { return ( file.createTime < now - this.maxDays * 86400000 || file.size > this.maxSize ) } }4. 跨平台实现细节
4.1 App端实现
利用5+ API实现文件存储,注意权限处理:
class AppFileTransport { private getLogDir(): Promise<DirectoryEntry> { return new Promise((resolve, reject) => { plus.io.requestFileSystem(plus.io.PRIVATE_DOC, fs => { fs.root.getDirectory( 'logs', { create: true }, dir => resolve(dir), reject ) }, reject) }) } }4.2 小程序端适配
小程序使用wx.getLogManager,但需要注意:
- 日志缓存有大小限制
- 无法直接访问原始日志文件
- 需要特殊处理日志上传
class MiniProgramTransport implements Transport { private logger = wx.getLogManager({ level: 0 }) write(entry: LogEntry): Promise<void> { return new Promise(resolve => { switch (entry.level) { case 'DEBUG': this.logger.debug(entry.message) break case 'INFO': this.logger.info(entry.message) break // 其他级别... } resolve() }) } }4.3 H5端策略
浏览器端可采用IndexedDB存储,并实现日志上传:
class BrowserTransport implements Transport { private db: IDBDatabase async init(): Promise<void> { this.db = await this.openDatabase() } async write(entry: LogEntry): Promise<void> { await this.db .transaction('logs', 'readwrite') .objectStore('logs') .add(entry) } }5. 高级功能扩展
5.1 性能监控集成
将日志系统与性能监控结合:
class PerformanceMonitor { constructor(private logger: Logger) { this.setup() } private setup(): void { uni.onNetworkStatusChange(res => { this.logger.info('Network changed', { isConnected: res.isConnected, networkType: res.networkType }) }) // 监控页面加载性能 uni.onAppRoute(res => { const timing = performance.now() this.logger.debug(`Route changed: ${res.path}`, { timing }) }) } }5.2 日志可视化分析
实现一个简单的日志查看器组件:
<template> <view class="log-viewer"> <view v-for="(log, index) in logs" :key="index" :class="['log-entry', log.level.toLowerCase()]"> [{{ formatTime(log.timestamp) }}] {{ log.message }} </view> </view> </template> <script> export default { data() { return { logs: [] } }, async mounted() { this.logs = await logger.getLogs() } } </script>5.3 安全日志处理
敏感信息过滤和日志加密:
class SecureLogger { constructor(private logger: Logger) {} log(entry: LogEntry): void { this.logger.log({ ...entry, message: this.filterSensitiveInfo(entry.message) }) } private filterSensitiveInfo(message: string): string { return message .replace(/(password|token)=[^&\s]+/g, '$1=***') .replace(/\d{4}-\d{4}-\d{4}-\d{4}/g, '****-****-****-****') } }在实际项目中,这套日志系统已经帮���我快速定位了多个难以复现的边界条件问题。特别是在处理小程序端的网络异常时,详细的日志记录让原本需要数天才能解决的问题在几小时内就找到了根源。
