如何优雅地实现前端日志输出与性能监控
先看代码,再聊别的
我最近在搞一个 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 解析堆栈、做日志聚合分析等,后续会继续分享这类博客。

暂无评论