小程序调试技巧:从console.log到三通道日志系统的演进

0 阅读6分钟

前言

在开发小程序的过程中,调试是一个永恒的话题。最初我只会用console.log,后来遇到了各种线上问题,才意识到一个好的调试系统有多重要。

这篇文章将分享我如何从简单的console.log演进到一套完整的三通道日志系统,以及这套系统如何帮助我快速定位和解决线上问题。

一、调试的痛点

1.1 传统调试方式的局限

问题1:console.log的局限

// 这是我最初的调试方式
console.log('请求开始', { name: 'poemEngine' });
console.log('返回结果', { data: [...] });

局限

  • 线上环境看不到
  • 无法导出给他人
  • 日志分散,难以串联

问题2:断点调试的局限

  • 真机调试时无法设置断点
  • 复现问题困难(特别是偶发问题)
  • 性能开销大

问题3:云函数调试的困难

  • 本地调试正常,云端必挂
  • 日志在云端,查看不便
  • 冷启动问题难以定位

1.2 我的需求

  1. 多环境支持:开发、测试、生产环境都能用
  2. 可导出:能把日志导出给他人或保存到文件
  3. 零性能开销:生产环境完全静默
  4. 结构化:日志可搜索、可过滤
  5. 线上可用:用户环境也能收集日志

二、三通道日志系统设计

2.1 架构设计

输入 → createLogger → 三通道输出
                    ↓
            1. console(控制台)
            2. 内存缓冲(环形队列)
            3. 文件输出(可选)

2.2 核心实现

// utils/debug.js

class Logger {
  constructor(module) {
    this.module = module;
    this.buffer = new CircularBuffer(200);  // 200条环形缓冲
  }
  
  log(...args) {
    this.output('log', args);
  }
  
  warn(...args) {
    this.output('warn', args);
  }
  
  error(...args) {
    this.output('error', args);
  }
  
  output(level, args) {
    const entry = {
      timestamp: Date.now(),
      level,
      module: this.module,
      args
    };
    
    // 通道1:console输出
    if (globalThis.__JIA_DEBUG__) {
      console.log(`[${this.module}]`, ...args);
    }
    
    // 通道2:内存缓冲
    this.buffer.push(entry);
    
    // 通道3:文件输出
    if (globalThis.__debugWriteFile__) {
      globalThis.__debugWriteFile__(entry);
    }
  }
}

// 环形缓冲(固定大小,先进先出)
class CircularBuffer {
  constructor(size) {
    this.size = size;
    this.buffer = [];
  }
  
  push(item) {
    this.buffer.push(item);
    if (this.buffer.length > this.size) {
      this.buffer.shift();
    }
  }
  
  tail(n) {
    return this.buffer.slice(-n);
  }
  
  dump() {
    return this.buffer.splice(0, this.buffer.length);
  }
}

// 全局API
const loggers = new Map();

export function createLogger(module) {
  if (!loggers.has(module)) {
    loggers.set(module, new Logger(module));
  }
  return loggers.get(module);
}

// 全局调试API
export function __debugTail(n = 20) {
  const allLogs = [];
  loggers.forEach(logger => {
    allLogs.push(...logger.buffer.tail(n));
  });
  return allLogs.sort((a, b) => a.timestamp - b.timestamp).slice(-n);
}

export function __debugDump() {
  const allLogs = [];
  loggers.forEach(logger => {
    allLogs.push(...logger.buffer.dump());
  });
  return allLogs.sort((a, b) => a.timestamp - b.timestamp);
}

2.3 关键设计点

  1. 变量名禁止用debug:会和debug.js模块名冲突,统一用dbg
  2. 环形缓冲:固定大小(200条),防止内存泄漏
  3. 开关控制__JIA_DEBUG__控制console输出
  4. 文件输出回调__debugWriteFile__由外部环境注入

三、使用方式

3.1 基础使用

import { createLogger } from '@/utils/debug';
const dbg = createLogger('api');

dbg.log('请求开始', { name: 'poemEngine' });
dbg.warn('警告信息', { ... });
dbg.error('错误信息', { ... });

3.2 开发环境

// tests/setup.js
// 开启console输出
globalThis.__JIA_DEBUG__ = true;

3.3 测试环境

# 带文件日志的测试
export DEBUG_LOG_FILE=./debug_output.log
npm run test:debug
// package.json
{
  "scripts": {
    "test:debug": "DEBUG_LOG_FILE=./debug_output.log node --import ./tests/setup.js --test tests/**/*.test.js"
  }
}

3.4 生产环境

// 默认全静默,零性能开销
// __JIA_DEBUG__ = false(默认)
// __debugWriteFile__ = undefined(默认)

3.5 线上问题排查

// 用户遇到问题时,引导导出日志
// 在页面中添加一个隐藏按钮,长按触发导出

onLongPressExport() {
  const logs = __debugTail__(20);
  // 保存到本地文件或复制到剪贴板
  uni.setClipboardData({
    data: JSON.stringify(logs, null, 2)
  });
}

四、实际案例

4.1 案例1:云函数超时问题

问题:用户反馈点击生成后一直转圈,最后提示"请求超时"。

排查步骤

  1. 让用户导出日志:__debugTail__(20)
  2. 查看日志发现:
[
  { "timestamp": 1690000000000, "level": "log", "module": "api", "args": ["请求开始", { "name": "poemEngine" }] },
  { "timestamp": 1690000015000, "level": "warn", "module": "api", "args": ["请求超时"] },
  { "timestamp": 1690000015001, "level": "error", "module": "api", "args": ["Error: 请求超时"] }
]
  1. 分析:从请求开始到超时正好15秒,说明是云函数挂起
  2. 解决:检查云函数日志,发现checkContentSecurity调用微信安全检测API hang住
  3. 修复:添加超时保护 + 降级策略

4.2 案例2:收藏功能401错误

问题:用户反馈收藏功能报错"401"。

排查步骤

  1. 导出日志
  2. 查看日志发现:
[
  { "timestamp": 1690000000000, "level": "log", "module": "api", "args": ["收藏请求", { "name": "张清扬" }] },
  { "timestamp": 1690000000100, "level": "error", "module": "api", "args": ["Error: 401", "未登录"] }
]
  1. 分析:401是未登录错误,但用户说已登录
  2. 深入查看:发现登录态过期
  3. 修复:添加登录态刷新逻辑

4.3 案例3:生僻字显示问题

问题:用户反馈名字显示为"□"。

排查步骤

  1. 导出日志
  2. 查看生成结果的日志
  3. 发现名字中包含生僻字"龘"
  4. 修复:添加生僻字过滤规则
const RARE_WORDS = new Set(['龘', '靐', '齉', '爩']);

function filterRareWords(name) {
  return !name.split('').some(char => RARE_WORDS.has(char));
}

五、进阶功能

5.1 日志分级

const LEVELS = {
  debug: 0,
  log: 1,
  warn: 2,
  error: 3
};

class Logger {
  constructor(module, level = 'log') {
    this.module = module;
    this.level = LEVELS[level];
  }
  
  shouldOutput(level) {
    return LEVELS[level] >= this.level;
  }
  
  debug(...args) {
    if (this.shouldOutput('debug')) {
      this.output('debug', args);
    }
  }
}

5.2 日志搜索

function searchLogs(keyword) {
  const allLogs = __debugDump();
  return allLogs.filter(log => 
    JSON.stringify(log.args).includes(keyword)
  );
}

// 使用
const errorLogs = searchLogs('error');
const apiLogs = searchLogs('poemEngine');

5.3 日志上报

// 严重错误自动上报到云端
function reportError(error) {
  const logs = __debugTail__(20);
  uniCloud.callFunction({
    name: 'errorReporter',
    data: {
      error: error.message,
      stack: error.stack,
      logs
    }
  });
}

六、与云函数日志的配合

6.1 云函数日志

// cloudfunctions/poemEngine/index.js
exports.main = async (event, context) => {
  console.log('云函数开始', { event });
  
  // 业务逻辑...
  
  console.log('云函数结束', { result });
  return result;
};

6.2 前后端日志关联

// 前端:生成requestId
const requestId = Date.now().toString();
dbg.log('请求开始', { requestId, ... });

// 云函数:接收并打印requestId
exports.main = async (event, context) => {
  console.log('云函数开始', { requestId: event.requestId });
  // ...
};

七、最佳实践

7.1 日志规范

  1. 结构化日志:用对象而不是字符串
  2. 关键节点必打日志:请求开始/结束、错误、重要状态变化
  3. 敏感信息脱敏:不要打印密码、token等
  4. 日志级别合理:debug(开发)、log(常规)、warn(警告)、error(错误)

7.2 性能考虑

  1. 生产环境静默__JIA_DEBUG__ = false
  2. 缓冲大小限制:200条防止内存泄漏
  3. 避免频繁输出:循环内不要打日志

7.3 调试技巧

  1. 复现问题前先清空__debugDump()
  2. 导出日志__debugTail__(20)
  3. 搜索关键词searchLogs('error')
  4. 关联前后端:用requestId串联

八、总结

8.1 三通道日志的优势

  1. console:开发调试,实时查看
  2. 内存缓冲:线上排查,导出日志
  3. 文件输出:测试环境,持久化存储

8.2 关键设计要点

  1. 零性能开销:生产环境完全静默
  2. 可导出:线上问题也能收集日志
  3. 结构化:可搜索、可过滤
  4. 模块化:按模块划分,便于定位

8.3 经验教训

  1. 不要只用console.log:线上看不到
  2. 不要用debug变量名:会和模块名冲突
  3. 缓冲大小要限制:防止内存泄漏
  4. 关键节点必打日志:否则排查时两眼一抹黑