告别 F12 与 Charles:如何打造一款「懂业务」的浏览器原生抓包利器?

0 阅读10分钟

图片

API Capture & YAML Exporter 源码与实战系列 · 第二篇

上一篇我们聊了接口自动化的「最后一公里」:抓到的请求是静态快照,而自动化需要的是可重复执行的动态关系。 这一篇换个角度:为什么我不建议你继续用 Charles 和 F12 做「抓包转自动化」?


系列索引

  • 第一篇:从「抓包两小时,写脚本一整天」说起:接口自动化的「最后一公里」痛点

  • 第二篇:告别 F12 与 Charles:如何打造一款「懂业务」的浏览器原生抓包利器?(本篇)

  • 第三篇:接口自动化的「灵魂」:如何让工具自动搞定上下游接口的参数关联?

  • 第四篇:一键生成 YAML 用例:从「抓包」到「自动化执行」的无缝闭环落地


01 传统抓包工具的「阿喀琉斯之踵」

上一篇我们吐槽了接口自动化推行不下去的核心痛点——参数关联太痛苦。

很多同事会问:

「抓包直接用 Charles 或者浏览器 F12 不就行了吗?为什么要自己写个 Chrome 插件?」

作为在测试行业摸爬滚打十多年的「老兵」,我当然知道 Charles 的强大,也理解 F12 的便捷。但在「抓包转自动化」这个特定场景下,传统工具存在几个致命的弱点——而这些弱点,恰恰是这款插件要解决的突破口。

痛点一:代理与证书地狱

图片

抓 HTTPS 必须配代理、下证书——这句话几乎是每个测试新人的「入学第一课」。

但现实往往比理论残酷:现代浏览器对证书校验越来越严格,企业内网经常有自己的根证书策略,App 端还有 SSL Pinning 保驾护航。

配置环境的过程,经常让新人崩溃,让老手皱眉。

痛点二:噪音太多,找核心 API 像大海捞针

图片

你打开一个业务系统页面,F12 打开 Network 面板,瞬间成百上千个请求刷屏——JS、CSS、图片、埋点、第三方统计……

真正核心的业务 API 被淹没在噪音海洋里,找起来费时费力。

痛点三:脱离浏览器上下文

图片

Charles 是独立于浏览器的代理工具,它不知道当前页面处于什么状态,也无法和 DOM 或 Storage 联动。

你抓完包导出的 .har 文件,还要手动清洗、格式化,才能导入 Postman 或 JMeter 使用。


所以我决定换一条路:利用 Chrome 的 Manifest V3 和 chrome.debugger API,打造一款长在浏览器侧边栏里的原生抓包利器。

它不需要配置代理,不需要安装证书,打开侧边栏就能抓——真正做到了「所见即所抓」。


02 架构总览:一个插件,三种角色

在深入代码之前,先看整体架构。它由四个核心文件组成,各司其职:

| 文件 | 角色 | 职责 | | --- | --- | --- | | manifest.json | 清单文件 | 声明权限、入口、配置页 | | background.js | 后台服务 | 核心抓包逻辑、Debugger 通信、Tab 生命周期管理 | | sidebar.html  + sidebar.js | 侧边栏 UI | 请求列表、详情面板、YAML 导出 | | options.html  + options.js | 配置页 | 多环境配置、正则测试工具 |

其中,background.js 是整个插件的「大脑」,负责与 Chrome 底层调试协议通信,完成请求的捕获和过滤。

侧边栏则是用户直接交互的界面,负责展示和管理抓到的数据。

接下来,逐行拆解 background.js 的核心逻辑,看看它是如何实现「精准、无侵入」的网络监听的。


03 核心揭秘:chrome.debugger 的网络监听魔法

打开 manifest.json,你会看到这样的权限声明:

{
  "manifest_version": 3,
  "name": "API Capture & YAML Exporter",
  "version": "1.0.0",
  "permissions": [
    "webRequest",
    "storage",
    "sidePanel",
    "tabs",
    "debugger",
    "activeTab"
  ],
  "host_permissions": ["<all_urls>"],
  "background": {
    "service_worker": "background.js"
  },
  "side_panel": {
    "default_path": "sidebar.html"
  },
  "options_page": "options.html"
}

这里有两个关键声明值得注意:

第一,debugger 权限。 这是 Chrome DevTools Protocol(CDP)的入口。通过它,插件可以获得和 F12 几乎同等深度的网络观测能力——不仅能拿到请求头,还能拿到完整的请求体和响应体。

第二,sidePanel 权限。 这是 Manifest V3 引入的新 API,允许扩展在浏览器侧边栏中嵌入页面。相比传统弹窗(popup),侧边栏空间更大,更适合做复杂的数据展示和交互。

3.1 为什么不用 chrome.webRequest?

图片

你可能会问:Chrome 不是提供了 chrome.webRequest 吗?为什么还要用更复杂的 debugger?

答案很简单:chrome.webRequest 拿不到响应体。

它只能在请求发送前拦截到请求头和部分请求体,对响应阶段的完整数据(尤其是 JSON 响应体)无能为力。而做接口自动化,响应体恰恰是断言和参数提取的核心数据来源。

chrome.debugger 则不同——它直接复用 DevTools 的底层能力,通过 Network.enable 开启监听,可以捕获完整的网络事件流:

  • Network.requestWillBeSent
  • Network.responseReceived
  • Network.loadingFinished

配合 Network.getResponseBody,可以拿到任意请求的完整响应数据。

3.2 抓包核心流程:三步走

图片

background.js 中 Debugger 事件监听的核心实现:

chrome.debugger.onEvent.addListener(async (source, method, params) => {
  if (!isCapturing) return;

  const config = getCurrentConfig();
  if (!config) return;

  // 第一步:请求发送时,记录请求信息
  if (method === 'Network.requestWillBeSent') {
    const { requestId, request, timestamp } = params;
    const url = request.url;

    if (!isUrlMatchConfig(url, config)) return;
    if (!isApiRequest(url)) return;

    requestMap.set(requestId, {
      requestId,
      url,
      method: request.method,
      requestHeaders: request.headers || {},
      requestBody: parseRequestBody(request),
      timestamp,
      status: null,
      responseHeaders: {},
      responseBody: null,
      config: config.id
    });
  }

  // 第二步:响应返回时,记录状态码和响应头
  if (method === 'Network.responseReceived') {
    const { requestId, response } = params;
    const entry = requestMap.get(requestId);
    if (!entry) return;

    entry.status = response.status;
    entry.responseHeaders = response.headers || {};
    entry.mimeType = response.mimeType;
  }

  // 第三步:请求完成时,获取响应体并持久化
  if (method === 'Network.loadingFinished') {
    const { requestId } = params;
    const entry = requestMap.get(requestId);
    if (!entry) return;

    try {
      const resp = await chrome.debugger.sendCommand(
        { tabId: source.tabId },
        'Network.getResponseBody',
        { requestId }
      );
      entry.responseBody = tryParseJson(resp.body);
    } catch {
      entry.responseBody = null;
    }

    capturedRequests.push({ ...entry });
    requestMap.delete(requestId);
    broadcastUpdate();
  }
});

这段逻辑可以概括为「请求记录 → 响应标记 → 完成归档」三步走:

  1. 请求发送时

    :拿到 URL、方法、Headers、Body,先过两道过滤(Host 匹配 + 静态资源排除),再以 requestId 为 key 暂存;

  2. 响应返回时

    :回填状态码和响应头;

  3. 请求完成时

    :通过 Network.getResponseBody 拿完整响应体,解析 JSON 后归档,并广播刷新侧边栏。

这种设计的好处是:请求和响应分阶段获取,避免单次事件数据量过大;即使响应体很大(比如文件下载),也不会阻塞主流程。


04 智能过滤:如何做到「只抓我想抓的」

抓包最怕噪音。如果每个 JS、CSS、图片都跑到侧边栏,那这个工具和 F12 就没区别了。

background.js 里设计了一套基于 Host 和正则的双层过滤引擎。

4.1 第一道滤网:静态资源排除

图片

function isApiRequest(url) {
  const skipExt = [
    '.js', '.css', '.png', '.jpg', '.jpeg', '.gif',
    '.ico', '.svg', '.woff', '.woff2', '.ttf', '.map', '.html'
  ];

  try {
    const p = new URL(url).pathname.toLowerCase();
    if (skipExt.some(e => p.endsWith(e))) return false;
    if (url.startsWith('chrome-extension://') || url.startsWith('chrome://')) {
      return false;
    }
  } catch {
    return false;
  }

  return true;
}

解析 URL 路径,检查后缀是否在跳过列表里,同时排除 Chrome 内部页面和扩展页面。

4.2 第二道滤网:Host 白名单 + 正则模块提取

图片

真正聪明的过滤在 isUrlMatchConfig:

function isUrlMatchConfig(url, config) {
  if (!config) return false;
  return parseHostFilter(config.hostFilter)
    .some(r => url.startsWith(r.host));
}

function parseHostFilter(hostFilterStr) {
  if (!hostFilterStr) return [];

  return hostFilterStr.trim().split('\n').map(line => {
    const parts = line.trim().split('|');
    return {
      host: parts[0]?.trim() || '',
      feature: parts[1]?.trim() || '',
      tenant: parts[2]?.trim() || '',
      role: parts[3]?.trim() || ''
    };
  }).filter(item => item.host);
}

用户的 hostFilter 配置格式是:

https://jsonplaceholder.typicode.com|jsonplaceholder||
https://jsonplaceholder.typicode.com/posts|posts||

每行用 | 分隔四个字段:主机地址、功能模块名、租户信息、角色信息。

这个设计很有讲究:插件在抓包的瞬间,就已经知道这个请求属于哪个业务系统、哪个模块、哪个租户环境——这些信息后面会直接用于 YAML 的结构化组织。

4.3 正则提取:让 URL「说话」

图片

除了 Host 白名单,配置里还有两个正则字段:

  • featureRule

    :从路径提取功能模块,例如 /api/([^/]+)/ 能从 /api/order/create 提取 order;

  • subdirRule

    :提取子目录,例如 /api/[^/]+/v\d+/([^/]+)/ 能从 /api/order/v1/user/list 提取 user。

这些结果,会在生成 YAML 时用于组织目录结构——让导出的用例天然具有清晰的模块层次。


05 生命周期管理:别让插件「拖垮」浏览器

做过 Chrome 扩展的同学都知道,chrome.debugger 有个巨坑:

用户关闭页面或切换 Tab 时,如果没有及时 detach,插件就会报错,甚至导致浏览器卡顿。

想象一下:你正在抓包,突然收到一条紧急消息,切到另一个 Tab 去回复。如果忘了停止抓包就关掉页面,后台的 Debugger 监听不会自动断开,后续这个 Tab 再被打开就会出问题。

所以 background.js 里加了严密的生命周期管理:

// Tab 关闭时自动分离 Debugger
chrome.tabs.onRemoved.addListener(tabId => detachDebugger(tabId));

async function detachDebugger(tabId) {
  if (!attachedTabs.has(tabId)) return;

  try {
    await chrome.debugger.detach({ tabId });
    attachedTabs.delete(tabId);
    console.log(`[Debugger] 已从 Tab ${tabId} 分离`);
  } catch (e) {
    console.error('[Debugger] 分离失败:', e);
  }
}

附加 Debugger 之前,还会做 Tab 合法性校验:

function isAttachableTab(tab) {
  if (!tab || !tab.url) return false;

  const blocked = [
    'chrome://', 'chrome-extension://', 'devtools://',
    'edge://', 'about:', 'data:', 'javascript://'
  ];

  for (const prefix of blocked) {
    if (tab.url.startsWith(prefix)) return false;
  }

  return tab.url.startsWith('http://') || tab.url.startsWith('https://');
}

这些「防御性编程」的细节,保证了一个测试工具最基本的素质:不添乱、不崩溃、不拖慢浏览器。


06 配置管理:让工具适应业务,而不是让人适应工具

一款好的测试工具,不应该让测试人员去适应工具的固定模式,而应该让工具去适应业务场景。

这就是 options.html + options.js 这套配置系统存在的理由。

6.1 多环境配置:一套工具,多个系统

图片

配置页里可以创建多个系统配置,每个包含:

  • 配置名称(如「测试演示(JSONPlaceholder)」)

  • Host 过滤规则

  • 功能模块正则

  • 子目录正则

  • 默认租户 / 角色

  • 断言成功值(响应体中表示成功的字段值)

插件启动时自动加载配置,首次使用还会创建一个默认测试配置:

// 默认配置(JSONPlaceholder - 无需真实后端)
{
  id: 'config_test_demo',
  name: '测试演示(JSONPlaceholder)',
  featureRule: '/([^/]+)/',
  subdirRule: '/[^/]+/(\\d+)',
  hostFilter: 'https://jsonplaceholder.typicode.com|jsonplaceholder||'
}

好处是:安装后无需任何配置就能立即体验抓包(用 JSONPlaceholder 这个公开测试 API),大大降低上手门槛。

6.2 正则测试器:配置不出错的安全网

图片

配置正则最怕什么?配错了不知道,抓不到包了也不知道原因。

所以配置页内置了一个正则测试工具:

<div id="regexTestCard">
  <h3>正则表达式测试</h3>
  <div class="form-grid">
    <div class="form-group">
      <label>测试 URL</label>
      <input type="text" id="testUrl"
             placeholder="https://jsonplaceholder.typicode.com/posts/1" />
    </div>
    <div class="form-group" style="justify-content:flex-end; padding-top:18px;">
      <button id="btnRunTest" class="btn btn-warning" onclick="runRegexTest()">
        执行测试
      </button>
    </div>
  </div>
  <div id="regexResult" style="margin-top:10px; font-size:12px;"></div>
</div>

输入一个真实 API URL,点「执行测试」,就能即时验证 featureRule 和 subdirRule 是否能正确提取模块名和子目录。

功能看似简单,但实际使用中极大地减少了配置调试时间。

6.3 配置导入导出:团队协作的基石

图片

支持 JSON 格式的导入导出:

function exportConfigs() {
  const json = JSON.stringify(
    { systemConfigs: configs, currentConfigId },
    null,
    2
  );

  const blob = new Blob([json], { type: 'application/json;charset=utf-8' });
  const url = URL.createObjectURL(blob);

  const a = document.createElement('a');
  a.href = url;
  a.download = `api_capture_configs_${Date.now()}.json`;
  a.click();

  URL.revokeObjectURL(url);
}

你可以把自己的配置导出分享给团队,大家导入后即使用相同的过滤规则和正则,保证团队内部的抓包配置一致性。


07 侧边栏体验:抓包也可以很优雅

最后看看用户直接交互的侧边栏。

sidebar.html 构建了一个完整的单页应用结构:

<!-- 顶部工具栏 -->
<div class="toolbar">
  <span class="app-title">API Capture</span>
  <button id="optionsBtn" class="btn btn-icon" title="系统配置">配置</button>
</div>

<!-- 配置选择栏 -->
<div class="config-bar">
  <label class="config-label">系统:</label>
  <select id="configSelect" class="config-select"></select>
  <span id="captureStatus" class="status-badge status-stopped">已停止</span>
</div>

<!-- 操作按钮栏 -->
<div class="action-bar">
  <button id="startBtn" class="btn btn-success">开始抓包</button>
  <button id="stopBtn" class="btn btn-danger" disabled>停止</button>
  <button id="clearBtn" class="btn btn-warning">清空</button>
  <button id="exportBtn" class="btn btn-primary">导出 YAML</button>
</div>

<!-- 过滤栏 -->
<div class="filter-bar">
  <input id="filterInput" type="text" placeholder="过滤 URL / 方法..." class="filter-input" />
  <select id="methodFilter" class="method-filter">
    <option value="">全部方法</option>
    <option value="GET">GET</option>
    <option value="POST">POST</option>
    <option value="PUT">PUT</option>
    <option value="DELETE">DELETE</option>
  </select>
</div>

<!-- 请求列表 -->
<div id="requestList" class="request-list">...</div>

<!-- 详情面板 -->
<div id="detailPanel" class="detail-panel hidden">
  <div class="detail-tabs">
    <button class="tab-btn active" data-tab="overview">概览</button>
    <button class="tab-btn" data-tab="request">请求体</button>
    <button class="tab-btn" data-tab="response">响应体</button>
    <button class="tab-btn" data-tab="yaml">YAML 预览</button>
  </div>
  <div id="detailContent" class="detail-content"></div>
</div>

侧边栏的设计哲学是「轻量、直观、不打断工作流」:

  • 配置切换

    :顶部下拉选择当前系统配置,切换后立即生效;

  • 一键抓包

    :点击开始,插件自动附加 Debugger,页面上的请求全部捕获;

  • 实时过滤

    :按 URL 关键词和方法类型实时筛选;

  • 详情面板

    :点请求卡片滑出,看概览、请求体、响应体、YAML 预览;

  • 拖拽排序

    :调整用例执行顺序。

sidebar.css 里用了大量 CSS 变量和现代特性(flex、过渡动画、自定义滚动条),在功能完整的同时也照顾了视觉体验。


08 小结:工具的价值在于「无感」

回顾一下,这篇从传统抓包工具的痛点出发,拆解了插件在抓包层面的技术实现:

  1. 基于 chrome.debugger 而非 chrome.webRequest,实现了全量请求体/响应体捕获;

  2. 通过 Host 白名单 + 静态资源过滤 + 正则模块提取的三层过滤,确保侧边栏只展示核心业务 API;

  3. 严密的 Tab 生命周期管理,保证插件不拖垮浏览器;

  4. 灵活的多环境配置,让一套工具适配多个业务系统;

  5. 内置正则测试和配置导入导出,降低配置门槛和团队协作成本。

但抓包只是第一步。

抓下来的数据如何变成自动化脚本?接口之间的参数关联如何自动搞定?

下一篇是本系列的高潮:《接口自动化的「灵魂」:如何让工具自动搞定上下游接口的参数关联?》——深入 sidebar.js 的核心算法,看插件如何像一位老测试一样,自动识别接口依赖并建立变量引用。


互动时间

你用抓包工具时,遇到过最抓狂的问题是什么?HTTPS 证书配置,还是噪音请求太多?

欢迎在评论区留言(注意隐去敏感信息),我会在后续文章里针对性解答。

如果这篇对你有帮助,也欢迎转给正在被抓包折磨的同事。