如何优雅地实现前端日志输出与性能监控

程序猿欣辰 移动 阅读 1,356
赞 12 收藏
二维码
手机扫码查看
反馈

先看代码,再聊别的

我最近在搞一个 H5 项目,移动端调试真是一言难尽。页面在安卓机上点不动,在 iOS 上又莫名其妙卡住。最后还是靠日志输出才定位到问题。今天就来聊聊我在移动端做日志输出的一些实战经验。

如何优雅地实现前端日志输出与性能监控

先上核心代码,这是我现在项目里直接用的:

// logger.js
class Logger {
  constructor(options = {}) {
    this.enabled = process.env.NODE_ENV !== 'production'; // 生产环境默认关掉
    this.prefix = options.prefix || '[App]';
    this.storageKey = options.storageKey || 'app_logs';
    this.maxLogs = options.maxLogs || 100;
  }

  log(...args) {
    if (!this.enabled) return;
    const timestamp = new Date().toISOString();
    console.log(${this.prefix} ${timestamp}, ...args);
    this._saveToStorage(${timestamp} LOG, args);
  }

  warn(...args) {
    if (!this.enabled) return;
    const timestamp = new Date().toISOString();
    console.warn(${this.prefix} ${timestamp}, ...args);
    this._saveToStorage(${timestamp} WARN, args);
  }

  error(...args) {
    const timestamp = new Date().toISOString();
    console.error(${this.prefix} ${timestamp}, ...args);
    this._saveToStorage(${timestamp} ERROR, args);
  }

  _saveToStorage(type, data) {
    try {
      const logs = JSON.parse(localStorage.getItem(this.storageKey) || '[]');
      logs.push({ type, data: data.map(d => typeof d === 'object' ? JSON.stringify(d) : String(d)) });
      if (logs.length > this.maxLogs) {
        logs.shift(); // 老日志踢掉
      }
      localStorage.setItem(this.storageKey, JSON.stringify(logs));
    } catch (e) {
      // 存不进去就算了,别影响主流程
      console.warn('Failed to save log to localStorage', e);
    }
  }

  clear() {
    localStorage.removeItem(this.storageKey);
  }

  getLogs() {
    const raw = localStorage.getItem(this.storageKey);
    return raw ? JSON.parse(raw) : [];
  }
}

// 全局实例
const logger = new Logger({ prefix: '[MobileApp]', maxLogs: 200 });
export default logger;

这个场景最好用:用户报错但你看不到控制台

最典型的场景就是 QA 或用户说“点按钮没反应”,你本地跑得好好的。这时候你就需要一套能回溯的日志系统。

我现在的做法是,在关键路径打日志,比如:

import logger from './logger';

function handleLogin() {
  logger.log('开始登录流程', { timestamp: Date.now() });

  const form = getLoginForm();
  if (!form.valid) {
    logger.warn('表单验证失败', form.errors);
    return;
  }

  logger.log('准备提交请求', { url: '/api/login', method: 'POST' });

  fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify(form.data),
    headers: { 'Content-Type': 'application/json' }
  })
  .then(res => {
    if (!res.ok) throw new Error(HTTP ${res.status});
    logger.log('登录请求成功', res);
  })
  .catch(err => {
    logger.error('登录失败', err.message, err.stack);
  });
}

然后让 QA 打开一个隐藏页面(一般是 /debug),把 logger.getLogs() 显示出来,复制发给你就行。亲测有效。

线上环境也要打日志?可以,但要小心

有些团队想在线上也保留日志输出,方便追踪用户行为。我试过,但踩了个大坑:频繁写 localStorage 会导致页面卡顿,尤其低端安卓机。

后来改成这样:

// 只在发生错误时才记录
if (err.code === 'NETWORK_ERROR' || err.code === 'AUTH_FAILED') {
  logger.error('关键错误需上报', err);
  // 再额外上报到服务器
  fetch('https://jztheme.com/api/logs', {
    method: 'POST',
    body: JSON.stringify({
      type: 'client_error',
      message: err.message,
      stack: err.stack,
      url: window.location.href,
      ua: navigator.userAgent,
      timestamp: Date.now()
    })
  }).catch(() => {}); // 上报失败也不影响用户体验
}

注意这里不是所有日志都上报,只报 error 级别的,并且加了 try-catch 防止上报逻辑拖垮页面。

踩坑提醒:这三点一定注意

  • 不要在循环里打日志。我之前在一个每帧执行的动画函数里写了 log,结果一打开调试工具就卡死。尤其是移动端,性能本来就紧张。
  • localStorage 有大小限制。一般 5-10MB,超出会抛错。所以我上面代码里做了最大条数限制(maxLogs),并且用 try-catch 包了一下。
  • console 方法在某些环境可能被重写或禁用。比如微信 WebView 有时候会拦截 console 输出。建议在初始化时做个检测:
function safeConsole(method, ...args) {
  if (console && typeof console[method] === 'function') {
    Function.prototype.apply.call(console[method], console, args);
  }
}

// 使用
safeConsole('log', '初始化完成');

高级技巧:用 URL 参数临时开启日志

不想每次改代码开关日志?我现在的项目里加了个小机制:

// 根据 URL 参数决定是否开启日志
const params = new URLSearchParams(window.location.search);
const enableLogging = params.has('debug') || params.get('log') === 'true';

const logger = new Logger({ enabled: enableLogging });

这样一来,只要访问 https://jztheme.com/page?debug,日志就自动打开了。QA 测试的时候特别方便,不用重新打包。

还可以更进一步,支持不同级别:

const logLevel = params.get('log'); // none | log | warn | error
let enabled = false;
let levels = ['log', 'warn', 'error'];

switch (logLevel) {
  case 'log': enabled = true; break;
  case 'warn': levels = ['warn', 'error']; enabled = true; break;
  case 'error': levels = ['error']; enabled = true; break;
  default: enabled = false;
}

// 然后在 logger 里根据 levels 过滤

移动端特有的问题:横竖屏切换时日志丢了?

这个问题我折腾了半天才发现。某些安卓浏览器在横竖屏切换时会 reload 页面,导致之前的日志全没了。解决方案是:不要完全依赖内存或 localStorage,关键时刻得上报。

我的做法是在页面 unload 前尝试上报未发送的日志:

window.addEventListener('beforeunload', () => {
  const logs = logger.getLogs();
  const unsent = logs.filter(l => l.type.includes('ERROR') && !l.sent);
  if (unsent.length > 0) {
    navigator.sendBeacon?.('https://jztheme.com/api/logs/batch', JSON.stringify(unsent));
  }
});

注意用了 navigator.sendBeacon,因为它能在页面关闭后继续发送请求,比 fetch 可靠多了。

不过这招也不是 100% 成功,有些浏览器根本不支持 sendBeacon。所以最终方案是:关键错误实时上报 + 日志本地留存供调试 + unload 补发作为兜底。

其实还有个更简单的办法

如果你只是想临时看看日志,没必要自己搞一套。推荐用 vConsole(腾讯出的那个轻量调试面板):

<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
  // 开发环境自动加载
  if (location.hostname === 'localhost' || /debug/.test(location.search)) {
    var vConsole = new window.VConsole();
  }
</script>

引入之后手机上就会出现一个小按钮,点开就能看 console、network、storage,特别方便。但我们项目里没用它,因为包体积+80KB,对首屏有影响。

总结一下我现在用的组合拳

  • 开发环境:完全开启日志,输出到 console 和 localStorage
  • 测试环境:通过 ?debug 参数手动开启
  • 生产环境:只记录 error 级别,关键错误实时上报服务器
  • 调试工具:提供 /debug 页面查看最近日志
  • 防卡顿:限制日志数量,避免频繁写 storage

这套方案上线两个月,帮我们定位了好几个偶现 bug,值了。

最后说两句

日志输出看起来是个小功能,但在移动端调试中真的能救命。尤其是那些“无法复现”的问题,有日志至少能知道用户走到哪一步卡住了。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如结合 sourcemap 解析堆栈、做日志聚合分析等,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论