news 2026/9/7 18:52:21

Puppeteer Coverage.startJSCoverage() 详解:JS 代码覆盖率采集的完整机制与源码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Puppeteer Coverage.startJSCoverage() 详解:JS 代码覆盖率采集的完整机制与源码解析

Puppeteer Coverage.startJSCoverage() 详解:JS 代码覆盖率采集的完整机制与源码解析

【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer

Puppeteer 的page.coverage.startJSCoverage()是采集浏览器中 JavaScript 实际执行情况(哪些代码被执行、哪些未执行)的核心入口,常用于统计站点首屏执行代码占比、检测未运行的 JS 死代码等场景。本文以官方 API 文档 puppeteer.coverage.startjscoverage 为主体,结合 Coverage.ts 的 CDP 实现与 coverage.test.ts 测试用例,完整讲清该方法的方法签名、JSCoverageOptions各选项的默认值与真实作用、底层Profiler/Debugger域调用链,以及结果条目JSCoverageEntry的结构和匿名脚本(eval/new Function)的处理规则,读完你可以直接复制可用的覆盖率统计代码并理解每一条 range 的来龙去脉。

一、方法签名与返回值

官方文档给出的签名如下(见 docs/api/puppeteer.coverage.startjscoverage.md):

class Coverage { startJSCoverage(options?: JSCoverageOptions): Promise<void>; }
  • 参数options:类型为 JSCoverageOptions,可选。文档明确给出的默认值为:resetOnNavigation: truereportAnonymousScripts: falseincludeRawScriptCoverage: falseuseBlockCoverage: true
  • 返回值Promise<void>,Promise 在覆盖率采集成功开启后 resolve。

在源码中,Coverage.startJSCoverage()是一个薄封装,直接把options透传给内部的JSCoverage类(见 Coverage.ts#L153-L155):

async startJSCoverage(options: JSCoverageOptions = {}): Promise<void> { return await this.#jsCoverage.start(options); }

配套的停止方法是stopJSCoverage(): Promise<JSCoverageEntry[]>,它返回所有脚本的覆盖率报告数组;文档备注:JavaScript Coverage 默认不包含匿名脚本,但带有sourceURL的脚本会被报告。

二、JSCoverageOptions 四个选项逐项详解

JSCoverageOptions接口定义在 Coverage.ts#L51-L70,与官方文档 puppeteer.jscoverageoptions 一一对应。结合 JSCoverage.start() 中的默认值解构,四项的默认值与源码作用如下:

属性类型默认值源码中的实际作用
resetOnNavigationbooleantrue监听Runtime.executionContextsCleared事件,导航时清空已记录的#scriptURLs/#scriptSources,即跨导航不累积脚本(Coverage.ts#L262-L268)
reportAnonymousScriptsbooleanfalse#onScriptParsed中过滤:if (!event.url && !this.#reportAnonymousScripts) return;,默认跳过无 URL 的脚本(Coverage.ts#L277-L280)
includeRawScriptCoveragebooleanfalse一方面作为Profiler.startPreciseCoveragecallCount参数(请求函数级调用计数),另一方面决定stop()结果条目是否附带rawScriptCoverage原始 V8 数据
useBlockCoveragebooleantrue作为Profiler.startPreciseCoveragedetailed参数:true时按块级(statement/block)收集,false时退化为函数级收集

注意assert(!this.#enabled, 'JSCoverage is already enabled')断言:覆盖率未stop之前重复调用startJSCoverage()会直接抛错,即同一页面同一时间只能有一轮 JS 覆盖率会话。

三、startJSCoverage() 底层执行了哪些 CDP 命令

这是理解该方法行为的关键。从 JSCoverage.start() 的实现看,开启采集共做三件事:

  1. 记录选项状态并清空旧数据:把四个选项写入私有字段,clear()两个Map(scriptId → url / 脚本源码文本),防止上一轮残留。
  2. 订阅两个 CDP 事件(通过DisposableStack托管,stop()时统一dispose()):
    • Debugger.scriptParsed:每解析出一个新脚本就触发#onScriptParsed,回调里先用PuppeteerURL.isPuppeteerURL(event.url)排除 Puppeteer 自身注入的pptr:前缀脚本,再按reportAnonymousScripts决定是否跳过无 URL 脚本;随后发送Debugger.getScriptSource拉取脚本源码文本并缓存。若页面已经导航走,拉取失败只记错误日志、不抛异常。
    • Runtime.executionContextsCleared:执行上下文被清空(通常是导航)时触发#onExecutionContextsCleared,按resetOnNavigation决定是否清空缓存。
  3. 并行发送四条 CDP 命令
await Promise.all([ this.#client.send('Profiler.enable'), this.#client.send('Profiler.startPreciseCoverage', { callCount: this.#includeRawScriptCoverage, detailed: useBlockCoverage, }), this.#client.send('Debugger.enable'), this.#client.send('Debugger.setSkipAllPauses', {skip: true}), ]);

其中Profiler.startPreciseCoverage是核心:detailed对应useBlockCoverage(块级 vs 函数级),callCount对应includeRawScriptCoverage。而Debugger.setSkipAllPauses保证页面上出现debugger语句或断点时不会挂起——这正是测试用例"should not hang when there is a debugger statement"所验证的行为。

四、stopJSCoverage():结果如何汇总为 JSCoverageEntry[]

stop() 先并行发送Profiler.takePreciseCoverageProfiler.stopPreciseCoverageProfiler.disableDebugger.disable,然后对takePreciseCoverage返回的每条 V8 覆盖记录做如下处理:

  • URL 解析:优先取#onScriptParsed阶段缓存的 URL;若脚本没有 URL 且reportAnonymousScriptstrue,则合成debugger://VM + scriptId作为占位 URL。
  • 过滤url或源码文本任意为undefined的条目直接跳过(例如页面导航后源码没拉到的情况)。
  • ranges 计算:把该脚本所有函数的functions[].ranges扁平化后交给convertToDisjointRanges()(Coverage.ts#L457-L516)——这是一个扫描线算法:把每个嵌套区间拆成 start/end 事件点、排序后用命中计数栈扫描,输出互不重叠的已覆盖区间{start, end}数组,并过滤空区间。
  • 是否附带原始数据includeRawScriptCoveragefalse时推入{url, ranges, text};为true时额外附带rawScriptCoverage: entry(完整的Protocol.Profiler.ScriptCoverage)。

返回的条目类型JSCoverageEntry继承自CoverageEntryurl/text/ranges三个必填字段,见 Coverage.ts#L21-L45)。文档中“带 sourceURL 的匿名脚本会被报告”的行为,正对应#onScriptParsed里只要event.url存在就缓存的逻辑——//# sourceURL=xxx注释会让 V8 给脚本赋上该 URL。

五、实战示例:统计页面初始执行代码占比

官方 Coverage 文档给出的标准用法即“启用 → 导航 → 停止 → 统计字节占比”,可直接复制运行:

// Enable both JavaScript and CSS coverage await Promise.all([ page.coverage.startJSCoverage(), page.coverage.startCSSCoverage(), ]); // Navigate to page await page.goto('https://example.com'); // Disable both JavaScript and CSS coverage const [jsCoverage, cssCoverage] = await Promise.all([ page.coverage.stopJSCoverage(), page.coverage.stopCSSCoverage(), ]); let totalBytes = 0; let usedBytes = 0; const coverage = [...jsCoverage, ...cssCoverage]; for (const entry of coverage) { totalBytes += entry.text.length; for (const range of entry.ranges) usedBytes += range.end - range.start - 1; } console.log(`Bytes used: ${(usedBytes / totalBytes) * 100}%`);

要点:

  • startJSCoverage()必须在page.goto()之前调用,否则采集窗口漏掉了首次导航加载的脚本;
  • stopJSCoverage()stopCSSCoverage()可以Promise.all并行停止;
  • 若只想跨多个导航累计覆盖率,需page.coverage.startJSCoverage({resetOnNavigation: false})
  • page.coverage属性由 Page 暴露,因此本方法属于 Page 级 API,通过 CDP 会话驱动,适用于 Chromium 系浏览器。

六、匿名脚本与 debugger://VM URL 的确切含义

原文档 Remarks 部分值得逐字理解:

匿名脚本是没有关联 URL 的脚本,即页面上通过evalnew Function动态创建的脚本。当reportAnonymousScriptstrue时,匿名脚本的 URL 会以debugger://VM开头(除非脚本中存在魔术注释//# sourceURL,此时该注释值即为 URL)。

对应源码逻辑:

  • 过滤发生在#onScriptParsedevent.url为空且未开启reportAnonymousScripts时不缓存(Coverage.ts#L277-L280);
  • URL 合成发生在stop()url = 'debugger://VM' + entry.scriptId(Coverage.ts#L310-L313)。

测试用例对此有明确断言(见 coverage.test.ts):

  • "should ignore eval() scripts by default":默认配置下访问含eval的页面,结果只有 1 条(页面自身脚本),eval内容不出现;
  • "should not ignore eval() scripts if reportAnonymousScripts is true":开启后,过滤掉debugger://前缀条目后仍剩 1 条,即eval脚本以debugger://VM...URL 形式被报告;
  • "should ignore pptr internal scripts if reportAnonymousScripts is true":即便开启了匿名报告,page.evaluate()注入的脚本因pptr:前缀 URL 被排除,结果为 0 条——这是PuppeteerURL.isPuppeteerURL()(util.ts#L69-L71)保证的。

七、用测试用例验证四个选项的行为

test/src/coverage.test.ts 中JSCoverage一节的用例与本文各节一一对应,可作为行为基准:

  1. 块级 ranges(useBlockCoverage: true:加载 test/assets/jscoverage/simple.html(内容是一个内联<script>),断言结果为 1 条且ranges精确等于[{start: 0, end: 17}, {start: 35, end: 61}]
  2. 函数级 ranges(useBlockCoverage: false:同一ranges.html页面下,函数级范围更大——console.log('unused!')所在的整个函数体被计入覆盖区间,与块级结果形成可验证的差异(coverage.test.ts#L99-L118)。
  3. resetOnNavigation: true(默认):连续两次gotostopJSCoverage()返回 0 条——第一次导航的脚本已随executionContextsCleared被清空。
  4. includeRawScriptCoverage:默认时entry.rawScriptCoverageundefined;传true后字段存在且非空(coverage.test.ts#L161-L184)。
  5. sourceURL报告:加载sourceurl.html后条目的urlnicename.js,印证//# sourceURL注释会作为 URL 被采用。
  6. 零覆盖脚本unused.html中脚本未被执行时仍会出现在结果里,只是ranges为空数组。
  7. debugger语句不挂起:得益于Debugger.setSkipAllPauses({skip: true})

八、小结与使用建议

  • startJSCoverage()本质是开启 V8 的Profiler.startPreciseCoverage精确覆盖采集,并借助Debugger域缓存每个脚本的 URL 与源码,最终把 V8 的嵌套区间归并成互不重叠的{start, end}覆盖区间;
  • 四个选项的默认值(resetOnNavigation: truereportAnonymousScripts: falseincludeRawScriptCoverage: falseuseBlockCoverage: true)覆盖了大多数“统计页面实际执行代码占比”的需求;需要调用计数或更细粒度分析时再开启includeRawScriptCoverage,需要评估eval/new Function代码时开启reportAnonymousScripts并自行识别debugger://VM前缀条目;
  • 覆盖率会话是独占的:再次startJSCoverage()前必须先stopJSCoverage()
  • 若需把结果交给 Istanbul 等工具链,官方文档在 Coverage 的 Remarks 中推荐配合puppeteer-to-istanbul转换;
  • 本方法依赖 CDP 的Profiler/Debugger域,属于 Chromium 系实现路径(实现位于 packages/puppeteer-core/src/cdp/Coverage.ts),使用前确认所用浏览器支持对应 CDP 命令即可。

【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 18:52:12

QQ机器人插件开发实战:从免费源码到二次开发全攻略

不需要什么花里胡哨的介绍&#xff0c;先说结论&#xff1a;QQ机器人插件开发这件事&#xff0c;在2025年的今天早就不是什么高门槛的黑科技了。你只要会一点Python基础&#xff0c;能照着文档复制粘贴&#xff0c;再找到一份靠谱的免费插件源码&#xff0c;几个小时就能跑起来…

作者头像 李华
网站建设 2026/9/7 18:51:32

四合一时间序列预测:ARIMA+LSTM+Transformer+门控融合

最近在做一套工业设备负荷预测时&#xff0c;我又一次被单模型的"偏科"打醒&#xff1a;同一组数据&#xff0c;LSTM训练时拟合得很漂亮&#xff0c;一到节假日就明显跑偏&#xff1b;ARIMA在平稳时段稳定得让人放心&#xff0c;碰到突发波动基本失灵&#xff1b;Tra…

作者头像 李华
网站建设 2026/9/7 18:51:04

电子行业PLM选型指南:五款国产系统核心差异化深度对比

1. 为什么电子行业需要一张PLM系统差异化对比表这两年国产PLM在电子行业的声量越来越大&#xff0c;我身边不少做研发管理、IT选型的朋友都在问同一个问题&#xff1a;国外那套巨头产品用得好好的&#xff0c;为什么还要折腾国产替代&#xff1f;答案其实不复杂——电子产品迭代…

作者头像 李华
网站建设 2026/9/7 18:48:35

MySQL函数实战指南:分类、性能陷阱与优化技巧

做后端开发这几年&#xff0c;MySQL是我每天都要打交道的东西&#xff0c;而在排查过的慢查询和错误SQL里&#xff0c;至少有三成问题出在函数使用上。MySQL函数用好了能让SQL简洁高效&#xff0c;用不好轻则结果不对、重则让索引失效直接全表扫描。这篇博文我想系统梳理一下My…

作者头像 李华
网站建设 2026/9/7 18:48:33

从2010年408真题看快速排序:手推一趟划分的避坑指南

如果你翻过408真题的排序部分&#xff0c;会发现快速排序几乎是选择题里的常驻嘉宾&#xff0c;2010年全国统考第10题就是典型代表。这类题看着简单&#xff0c;可我带过的学生里&#xff0c;能把“一趟划分”结果一次做对的不到一半。原因不是不懂原理&#xff0c;而是手推的时…

作者头像 李华
网站建设 2026/9/7 18:46:43

深入理解堆:从二叉堆到PriorityQueue的底层原理与工程实践

1. 先把“堆”这回事彻底掰开揉碎 聊PriorityQueue之前&#xff0c;必须先搞清楚一个特别容易被搞混的点&#xff1a;日常说的“堆”&#xff0c;和Java里那个 java.util.PriorityQueue &#xff0c;和C报错里“堆已损坏”的“堆”&#xff0c;以及JVM里“堆外内存”的“堆”…

作者头像 李华