浏览器开发者工具

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: 1

3. 异步调试

// 使用 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('调试信息');
// #endif

3. 调试工具推荐

浏览器扩展:

  • 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