前端调试技巧与工具大全
浏览器开发者工具
Chrome DevTools 核心功能
Console 面板
// 基础输出
console.log('普通日志');
console.info('信息日志');
console.warn('警告日志');
console.error('错误日志');
// 格式化输出
console.log('字符串: %s', 'hello');
console.log('数字: %d', 42);
console.log('对象: %o', { name: '张三' });
console.log('CSS 样式: %c自定义样式', 'color: red; font-size: 20px;');
// 表格输出
const users = [
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 },
{ id: 3, name: '王五', age: 28 },
];
console.table(users);
// 只输出特定列
console.table(users, ['name', 'age']);
// 分组输出
console.group('用户信息');
console.log('姓名: 张三');
console.log('年龄: 25');
console.groupCollapsed('详细信息');
console.log('邮箱: zhangsan@example.com');
console.log('电话: 1234567890');
console.groupEnd();
console.groupEnd();
// 计时
console.time('操作耗时');
// 执行一些操作
for (let i = 0; i < 1000000; i++) {
// 模拟耗时操作
}
console.timeEnd('操作耗时');
// 计时器
console.time('定时器');
setTimeout(() => {
console.timeLog('定时器'); // 输出中间时间
}, 1000);
setTimeout(() => {
console.timeEnd('定时器');
}, 2000);
// 计数
function processItem(item) {
console.count(`处理 ${item}`);
}
processItem('A'); // 处理 A: 1
processItem('B'); // 处理 B: 1
processItem('A'); // 处理 A: 2
// 断言
function divide(a, b) {
console.assert(b !== 0, '除数不能为零');
return a / b;
}
divide(10, 0); // 输出: Assertion failed: 除数不能为零
// 追踪调用栈
function trace() {
console.trace('调用栈追踪');
}
function a() {
trace();
}
function b() {
a();
}
b(); // 显示完整的调用栈
// 清除控制台
console.clear();
// 目录输出
console.dir(document.body);
console.dir(document.body, { depth: null }); // 展开所有层级
// XML/HTML 输出
console.dirxml(document.body);条件断点与日志断点
// 在 Sources 面板中设置断点
// 1. 普通断点
// 点击行号设置
// 2. 条件断点
// 右键行号 -> Add conditional breakpoint
// 输入条件: user.id === 123
// 3. 日志断点(Logpoint)
// 右键行号 -> Add logpoint
// 输入: User data: %o, user
// 不会暂停执行,只输出日志
// 4. DOM 断点
// 在 Elements 面板右键元素 -> Break on
// - subtree modifications(子树修改)
// - attribute modifications(属性修改)
// - node removal(节点移除)
// 5. XHR/Fetch 断点
// 在 Sources 面板 -> XHR/fetch Breakpoints
// 添加 URL 包含字符串,当请求匹配时暂停
// 6. 事件监听器断点
// 在 Sources 面板 -> Event Listener Breakpoints
// 勾选要监听的事件类型
// 7. 异常断点
// 在 Sources 面板点击暂停按钮
// - Pause on caught exceptions(捕获的异常)
// - Pause on uncaught exceptions(未捕获的异常)网络面板技巧
// 1. 筛选请求
// 类型筛选: Fetch/XHR, JS, CSS, Img, Media, Font, Doc, WS, Manifest, Other
// 状态筛选: All, 2xx, 3xx, 4xx, 5xx
// 2. 搜索请求
// Ctrl+F 或 Cmd+F 打开搜索框
// 支持正则表达式
// 3. 复制请求
// 右键请求 -> Copy
// - Copy as cURL
// - Copy as fetch
// - Copy as PowerShell
// - Copy all as cURL
// 4. 重放请求
// 右键请求 -> Replay XHR
// 5. 阻止请求
// 右键请求 -> Block request URL
// 右键请求 -> Block request domain
// 6. 覆盖响应
// 右键请求 -> Override content
// 可以修改响应内容进行测试
// 7. 节流网络
// 在网络面板选择 "Slow 3G"、"Fast 3G" 等
// 模拟慢速网络环境
// 8. 离线模式
// 勾选 Offline 模拟离线状态
// 9. 查看请求时间线
// 勾选 Timing 标签查看请求各阶段耗时
// 10. 性能分析
// 点击性能面板按钮,录制网络活动Performance 面板
// 1. 录制性能
// 点击录制按钮,执行操作,停止录制
// 2. 火焰图分析
// - 黄色:脚本执行
// - 紫色:渲染
// - 绿色:绘制
// - 灰色:其他
// 3. 主线程分析
// 查看主线程上的任务,找出长任务
// 4. 调用树
// 查看函数调用关系和耗时
// 5. 事件列表
// 按耗时排序的事件列表
// 6. 内存时间线
// 查看内存使用情况
// 7. 性能指标
// - FPS(帧率)
// - CPU 使用率
// - 网络活动
// 8. 性能标记
console.time('自定义标记');
// 执行操作
console.timeEnd('自定义标记');
// 在 Performance 面板中会显示标记
// 使用 Performance API
performance.mark('start');
// 执行操作
performance.mark('end');
performance.measure('操作', 'start', 'end');
// 9. 录制堆栈
// 在录制设置中勾选 "Enable advanced paint instrumentation"
// 可以查看详细的渲染信息Memory 面板
// 1. 堆快照(Heap Snapshot)
// 拍摄当前内存快照,分析对象占用
// 2. 分配仪表(Allocation Instrumentation)
// 实时跟踪内存分配
// 3. 分配时间线(Allocation Timeline)
// 按时间顺序记录内存分配
// 4. 分离检测
// 查找分离的 DOM 节点(内存泄漏)
// 5. 内存泄漏排查步骤
// - 拍摄初始堆快照
// - 执行可疑操作
// - 强制垃圾回收
// - 拍摄第二个快照
// - 对比两个快照,查找增长的对象
// 6. 常见内存泄漏场景
// - 未清理的定时器
// - 未移除的事件监听器
// - 全局变量
// - 闭包引用
// - 分离的 DOM 节点
// - 控制台日志引用
// 7. 使用 WeakRef 和 FinalizationRegistry
const weakRef = new WeakRef(obj);
const deref = weakRef.deref();
if (deref) {
console.log('对象还存在');
} else {
console.log('对象已被回收');
}
const registry = new FinalizationRegistry((id) => {
console.log(`对象 ${id} 已被回收`);
});
registry.register(obj, 'obj-1');Firefox Developer Tools
Firefox 的开发者工具在某些方面比 Chrome 更强大。
// Firefox 特有功能
// 1. 网格检查器
// 在 Elements 面板中,点击 grid 图标查看 CSS Grid 布局
// 2. Flexbox 检查器
// 点击 flex 图标查看 Flexbox 布局
// 3. 动画检查器
// 在 Inspector 面板中查看和调试 CSS 动画
// 4. 字体编辑器
// 在 Fonts 面板中实时调整字体属性
// 5. 无障碍检查
// 在 Accessibility 面板中检查无障碍属性
// 6. 网络监控
// Firefox 的网络面板更详细地显示请求信息
// 7. 性能工具
// Firefox 的性能工具提供更详细的分析
// 8. 内存工具
// Firefox 的内存工具更易于使用
// 9. 响应式设计模式
// Ctrl+Shift+M 或 Cmd+Opt+M 进入响应式设计模式
// 10. 取色器
// 在样式面板中点击颜色值可以打开取色器调试技巧
1. 条件调试
// 使用 debugger 语句
function processData(data) {
if (data.length > 100) {
debugger; // 当数据量大于 100 时暂停
}
return data.map(item => transform(item));
}
// 条件调试器
function debugIf(condition) {
if (condition) {
debugger;
}
}
// 使用示例
function complexFunction(user) {
debugIf(user.id === 123); // 只在特定用户时调试
// 函数逻辑
}2. 时间旅行调试
// 使用 Immutable 数据结构实现时间旅行
import { produce } from 'immer';
class TimeTravelDebugger {
private history: any[] = [];
private currentIndex = -1;
record(state: any) {
// 移除当前索引之后的历史
this.history = this.history.slice(0, this.currentIndex + 1);
this.history.push(JSON.parse(JSON.stringify(state)));
this.currentIndex++;
}
undo() {
if (this.currentIndex > 0) {
this.currentIndex--;
return this.history[this.currentIndex];
}
return null;
}
redo() {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
return this.history[this.currentIndex];
}
return null;
}
goTo(index: number) {
if (index >= 0 && index < this.history.length) {
this.currentIndex = index;
return this.history[this.currentIndex];
}
return null;
}
}
const debugger = new TimeTravelDebugger();
// 记录状态变化
let state = { count: 0 };
debugger.record(state);
state = produce(state, draft => {
draft.count = 1;
});
debugger.record(state);
state = produce(state, draft => {
draft.count = 2;
});
debugger.record(state);
// 时间旅行
console.log(debugger.undo()); // count: 1
console.log(debugger.undo()); // count: 0
console.log(debugger.redo()); // count: 13. 异步调试
// 使用 async/await 调试
async function debugAsync() {
try {
console.log('开始');
const result1 = await fetchData1();
debugger; // 在第一个请求后暂停
const result2 = await fetchData2(result1);
debugger; // 在第二个请求后暂停
return result2;
} catch (error) {
debugger; // 在错误处暂停
throw error;
}
}
// 使用 Promise 链调试
function debugPromiseChain() {
return fetchData1()
.then(result1 => {
debugger; // 第一个 Promise 完成后暂停
return fetchData2(result1);
})
.then(result2 => {
debugger; // 第二个 Promise 完成后暂停
return result2;
})
.catch(error => {
debugger; // 错误时暂停
throw error;
});
}
// 使用 async stacks(Chrome 支持)
// 在 Settings -> Preferences -> JavaScript 中启用 "Async stacks"
// 可以在异步调用之间保持调用栈
// 追踪未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', event => {
console.error('未处理的 Promise 拒绝:', event.reason);
debugger; // 在错误处暂停
});4. 网络请求调试
// 拦截 XMLHttpRequest
const originalXHR = window.XMLHttpRequest;
function debugXHR() {
const xhr = new originalXHR();
const originalOpen = xhr.open;
const originalSend = xhr.send;
xhr.open = function(method, url, ...args) {
console.log(`XHR 请求: ${method} ${url}`);
return originalOpen.apply(this, [method, url, ...args]);
};
xhr.send = function(data) {
console.log('XHR 发送数据:', data);
this.addEventListener('load', () => {
console.log(`XHR 响应: ${this.status}`, this.responseText);
});
this.addEventListener('error', () => {
console.error('XHR 错误');
debugger;
});
return originalSend.apply(this, [data]);
};
return xhr;
}
// 拦截 Fetch
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const [url, options] = args;
console.log(`Fetch 请求: ${url}`, options);
try {
const response = await originalFetch.apply(this, args);
const clone = response.clone();
clone.text().then(text => {
console.log(`Fetch 响应: ${url}`, text);
});
return response;
} catch (error) {
console.error(`Fetch 错误: ${url}`, error);
debugger;
throw error;
}
};
// 模拟网络延迟
async function fetchWithDelay(url: string, delay: number = 1000) {
await new Promise(resolve => setTimeout(resolve, delay));
return fetch(url);
}
// 模拟网络失败
function fetchWithError(url: string, shouldFail: boolean = false) {
if (shouldFail) {
return Promise.reject(new Error('模拟网络错误'));
}
return fetch(url);
}5. DOM 调试
// 监控 DOM 变化
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
console.log('DOM 变化:', mutation);
console.log('类型:', mutation.type);
console.log('目标:', mutation.target);
if (mutation.type === 'childList') {
console.log('添加的节点:', mutation.addedNodes);
console.log('移除的节点:', mutation.removedNodes);
}
if (mutation.type === 'attributes') {
console.log('属性名:', mutation.attributeName);
}
debugger; // 在 DOM 变化时暂停
});
});
observer.observe(document.body, {
childList: true,
attributes: true,
subtree: true,
characterData: true,
});
// 停止监控
// observer.disconnect();
// 查找元素
function findElements() {
// 使用选择器
const elements = document.querySelectorAll('.my-class');
// 使用 XPath
const xpathResult = document.evaluate(
'//div[@class="my-class"]',
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
// 使用 getElementsByClassName(更快)
const fastElements = document.getElementsByClassName('my-class');
// 检查元素是否可见
function isVisible(element: HTMLElement) {
const style = window.getComputedStyle(element);
return (
style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0' &&
element.offsetWidth > 0 &&
element.offsetHeight > 0
);
}
// 检查元素是否在视口中
function isInViewport(element: HTMLElement) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
}
}
// 高亮元素
function highlightElement(element: HTMLElement, color: string = 'red') {
const originalOutline = element.style.outline;
element.style.outline = `3px solid ${color}`;
setTimeout(() => {
element.style.outline = originalOutline;
}, 2000);
}
// 在控制台选择元素
// $0 返回当前选中的元素
// $1 返回上一个选中的元素
// $$('selector') 返回所有匹配的元素
// $('selector') 返回第一个匹配的元素
// $x('xpath') 返回 XPath 匹配的元素错误处理与追踪
全局错误捕获
// 捕获 JavaScript 错误
window.addEventListener('error', (event) => {
console.error('全局错误:', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error,
});
// 发送到错误追踪服务
sendToErrorTracking({
type: 'javascript',
message: event.message,
stack: event.error?.stack,
url: event.filename,
line: event.lineno,
column: event.colno,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href,
});
});
// 捕获 Promise 错误
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 拒绝:', event.reason);
sendToErrorTracking({
type: 'promise',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: Date.now(),
});
// 防止默认处理
// event.preventDefault();
});
// 捕获资源加载错误
window.addEventListener('error', (event) => {
const target = event.target as HTMLElement;
if (target.tagName) {
console.error('资源加载失败:', {
tag: target.tagName,
src: (target as HTMLImageElement).src || (target as HTMLLinkElement).href,
});
}
}, true); // 使用捕获阶段
// 发送错误数据
function sendToErrorTracking(data: any) {
// 使用 sendBeacon 确保数据发送成功
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon('/api/error-tracking', blob);
}Source Maps
// vite.config.ts
export default defineConfig({
build: {
sourcemap: true, // 启用 source map
// 或更详细的配置
sourcemap: 'hidden', // 生成 source map 但不引用
// sourcemap: 'inline', // 内联 source map
},
});
// 在代码中使用
// # sourceMappingURL=path/to/source.map.js
// 隐藏 source map(生产环境)
// 通过服务器配置只允许特定 IP 访问
// 使用 Sentry 等工具
// npm install @sentry/browser
import * as Sentry from '@sentry/browser';
Sentry.init({
dsn: 'your-dsn-here',
environment: process.env.NODE_ENV,
release: '1.0.0',
// 配置 source map 上传
// 使用 @sentry/webpack-plugin 或 @sentry/vite-plugin
});错误追踪服务
// Sentry 集成
import * as Sentry from '@sentry/browser';
import { BrowserTracing } from '@sentry/tracing';
Sentry.init({
dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
integrations: [new BrowserTracing()],
// 采样率
tracesSampleRate: 1.0,
// 环境
environment: process.env.NODE_ENV,
// 发布版本
release: '1.0.0',
// beforeSend 钩子
beforeSend(event) {
// 过滤敏感信息
if (event.request?.headers) {
delete event.request.headers['Authorization'];
}
return event;
},
// 用户信息
setUser(user) {
Sentry.setUser({
id: user.id,
username: user.username,
email: user.email,
});
},
// 自定义上下文
setTag(key: string, value: string) {
Sentry.setTag(key, value);
},
// 自定义面包屑
addBreadcrumb(breadcrumb) {
Sentry.addBreadcrumb({
message: breadcrumb.message,
category: breadcrumb.category,
level: breadcrumb.level,
data: breadcrumb.data,
});
},
});
// 手动发送错误
try {
// 可能出错的代码
} catch (error) {
Sentry.captureException(error);
}
// 发送消息
Sentry.captureMessage('这是一条消息', 'info');
// 性能追踪
const transaction = Sentry.startTransaction({
name: '自定义事务',
});
const span = transaction.startChild({
op: 'http',
description: 'GET /api/users',
});
// 执行操作
await fetch('/api/users');
span.finish();
transaction.finish();日志管理
分级日志系统
enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}
class Logger {
private level: LogLevel;
private prefix: string;
constructor(prefix: string = '', level: LogLevel = LogLevel.INFO) {
this.prefix = prefix;
this.level = level;
}
private shouldLog(level: LogLevel): boolean {
return level >= this.level;
}
private formatMessage(level: string, message: string): string {
const timestamp = new Date().toISOString();
return `[${timestamp}] [${level}] [${this.prefix}] ${message}`;
}
debug(message: string, ...args: any[]) {
if (this.shouldLog(LogLevel.DEBUG)) {
console.debug(this.formatMessage('DEBUG', message), ...args);
}
}
info(message: string, ...args: any[]) {
if (this.shouldLog(LogLevel.INFO)) {
console.info(this.formatMessage('INFO', message), ...args);
}
}
warn(message: string, ...args: any[]) {
if (this.shouldLog(LogLevel.WARN)) {
console.warn(this.formatMessage('WARN', message), ...args);
}
}
error(message: string, ...args: any[]) {
if (this.shouldLog(LogLevel.ERROR)) {
console.error(this.formatMessage('ERROR', message), ...args);
}
}
setLevel(level: LogLevel) {
this.level = level;
}
}
// 使用示例
const logger = new Logger('App', LogLevel.DEBUG);
logger.debug('调试信息');
logger.info('一般信息');
logger.warn('警告信息');
logger.error('错误信息');
// 不同模块使用不同的 logger
const apiLogger = new Logger('API');
const uiLogger = new Logger('UI');
apiLogger.info('发送请求');
uiLogger.info('渲染组件');远程日志
class RemoteLogger {
private buffer: any[] = [];
private flushInterval: number = 5000; // 5 秒
private maxBufferSize: number = 100;
private timer: NodeJS.Timeout | null = null;
constructor() {
this.startFlushTimer();
}
private startFlushTimer() {
this.timer = setInterval(() => {
this.flush();
}, this.flushInterval);
}
private addLog(level: string, message: string, data?: any) {
const log = {
timestamp: Date.now(),
level,
message,
data,
url: window.location.href,
userAgent: navigator.userAgent,
};
this.buffer.push(log);
if (this.buffer.length >= this.maxBufferSize) {
this.flush();
}
}
private async flush() {
if (this.buffer.length === 0) return;
const logs = [...this.buffer];
this.buffer = [];
try {
await fetch('/api/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ logs }),
});
} catch (error) {
// 失败时放回缓冲区
this.buffer.unshift(...logs);
// 使用 sendBeacon 作为后备
const blob = new Blob([JSON.stringify({ logs })], {
type: 'application/json',
});
navigator.sendBeacon('/api/logs', blob);
}
}
info(message: string, data?: any) {
this.addLog('info', message, data);
}
warn(message: string, data?: any) {
this.addLog('warn', message, data);
}
error(message: string, data?: any) {
this.addLog('error', message, data);
}
destroy() {
if (this.timer) {
clearInterval(this.timer);
}
this.flush();
}
}
const remoteLogger = new RemoteLogger();
// 页面卸载时发送剩余日志
window.addEventListener('beforeunload', () => {
remoteLogger.destroy();
});性能分析工具
Lighthouse CI
# .lighthouserc.yml
ci:
collect:
url:
- 'http://localhost:3000'
- 'http://localhost:3000/about'
numberOfRuns: 3
settings:
preset: 'desktop'
assert:
assertMatrix:
- matchingUrlPattern: '.*'
assertions:
'categories:performance': ['error', { minScore: 0.9 }]
'categories:accessibility': ['error', { minScore: 0.9 }]
'categories:best-practices': ['error', { minScore: 0.9 }]
'categories:seo': ['error', { minScore: 0.9 }]
upload:
target: 'lhci'
serverBaseUrl: 'http://localhost:9001'
token: 'your-token'Web Vitals 监控
import { onCLS, onFID, onLCP, onINP, onTTFB } from 'web-vitals';
function sendToAnalytics(name: string, value: number, id: string) {
// 使用 Google Analytics
if (typeof gtag !== 'undefined') {
gtag('event', name, {
event_category: 'Web Vitals',
event_value: Math.round(name === 'CLS' ? value * 1000 : value),
event_label: id,
non_interaction: true,
});
}
// 或发送到自己
}
onCLS((metric) => sendToAnalytics('CLS', metric.value, metric.id));
onFID((metric) => sendToAnalytics('FID', metric.value, metric.id));
onLCP((metric) => sendToAnalytics('LCP', metric.value, metric.id));
onINP((metric) => sendToAnalytics('INP', metric.value, metric.id));
onTTFB((metric) => sendToAnalytics('TTFB', metric.value, metric.id));调试最佳实践
1. 结构化调试流程
// 1. 复现问题
// 确保能够稳定复现问题
// 2. 隔离问题
// 使用二分法找出问题的代码范围
// 3. 理解上下文
// 查看相关代码、日志、网络请求
// 4. 形成假设
// 基于观察提出可能的原因
// 5. 验证假设
// 通过调试验证每个假设
// 6. 修复问题
// 实施修复并验证
// 7. 防止回归
// 添加测试用例,防止问题再次出现2. 有效的调试注释
// 标记需要调试的代码
// TODO: 调试 - 检查为什么这里会出错
// DEBUG: 临时调试代码,记得删除
// FIXME: 这里有个 bug,需要修复
// 使用条件编译(如果支持)
// #if DEBUG
console.log('调试信息');
// #endif3. 调试工具推荐
浏览器扩展:
- React Developer Tools
- Vue.js DevTools
- Redux DevTools
- ColorZilla
- WhatFont
- Page Ruler
独立工具:
- Charles Proxy - 网络调试代理
- Fiddler - Windows 网络调试工具
- Postman - API 测试
- Insomnia - API 测试
性能分析:
- Lighthouse
- WebPageTest
- GTmetrix
- PageSpeed Insights
错误追踪:
- Sentry
- Bugsnag
- Rollbar
- LogRocket
