 详解:页面 JavaScript 执行覆盖率采集与实战)
Puppeteer Coverage.startJSCoverage() 详解页面 JavaScript 执行覆盖率采集与实战【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteerCoverage.startJSCoverage()是 Puppeteer 中用于开启页面 JavaScript 执行覆盖率收集的核心 API。它基于 Chrome DevTools ProtocolCDP的 Profiler 与 Debugger 域实现让开发者可以精确统计页面加载后哪些脚本被执行、哪些代码字节真正用上了从而支撑前端性能优化、无用代码剔除与自动化测试质量评估。读完本文你将掌握该方法的完整签名、全部可配置参数及其默认行为、匿名脚本eval / new Function的处理细节并能写出计算脚本利用率百分比的完整可运行代码。本文以仓库 API 文档 docs/api/puppeteer.coverage.startjscoverage.md 为核心骨架并结合 Coverage 类源码 与 coverage 测试套件 展开实现级讲解。方法签名与调用入口startJSCoverage()定义在Coverage类上其 TypeScript 签名如下class Coverage { startJSCoverage(options?: JSCoverageOptions): Promisevoid; }在真实 API 文档docs/api/puppeteer.coverage.md中Coverage类被描述为提供收集页面中被使用的 JavaScript 与 CSS 部分信息的方法。它通过page.coverage属性暴露Page 的抽象声明位于 packages/puppeteer-core/src/api/Page.ts#L1038如下abstract get coverage(): Coverage;也就是说日常用法是page.coverage.startJSCoverage(...)。该方法返回Promisevoid——Promise 在覆盖率收集真正启动后才会 resolve因此你可以安全地await之后再进行页面跳转确保不会漏掉首批脚本的执行信息。参数详解JSCoverageOptions 的四个可选项options的类型为 JSCoverageOptions 接口全部字段都是可选optional的。根据 startJSCoverage 的 API 文档 与源码注释各参数的默认值合计为默认值resetOnNavigation: truereportAnonymousScripts: falseincludeRawScriptCoverage: falseuseBlockCoverage: trueresetOnNavigation默认 true导航时是否重置采集状态接口定义见 docs/api/puppeteer.jscoverageoptions.mdWhether to reset coverage on every navigation.置为true时每当页面发生导航触发Runtime.executionContextsCleared就会清空已记录的脚本 URL 与源码表只统计当前页面生命周期内的执行情况置为false则可以跨页面SPA 内多次导航 / 多页跳转持续累积覆盖率。在源码 Coverage.ts 中清空逻辑由内部方法#onExecutionContextsCleared实现#onExecutionContextsCleared(): void { if (!this.#resetOnNavigation) { return; } this.#scriptURLs.clear(); this.#scriptSources.clear(); }对应测试可见 test/src/coverage.test.tsdescribe(resetOnNavigation)下验证了开启时不应跨导航上报脚本的行为。reportAnonymousScripts默认 false是否上报匿名脚本匿名脚本anonymous scripts指没有关联 URL 的脚本——即页面中通过eval或new Function动态创建的代码。当该选项为true时这类脚本会被纳入上报且其 URL 以debugger://VM开头如debugger://VM123其中 123 是 V8 的 scriptId除非脚本中存在 magic//# sourceURL注释此时将使用该注释声明的 URL 作为脚本标识。源码中 URL 兜底逻辑位于JSCoverage.stop()Coverage.tslet url this.#scriptURLs.get(entry.scriptId); if (!url this.#reportAnonymousScripts) { url debugger://VM entry.scriptId; }同时脚本解析阶段#onScriptParsedCoverage.ts会先丢弃无 URL 且未开启该选项的脚本并跳过 Puppeteer 注入的内部脚本// Ignore puppeteer-injected scripts if (PuppeteerURL.isPuppeteerURL(event.url)) { return; } // Ignore other anonymous scripts unless the reportAnonymousScripts option is true. if (!event.url !this.#reportAnonymousScripts) { return; }该行为的测试证据同样充分test 用例should ignore eval() scripts by default与should not ignore eval() scripts if reportAnonymousScripts is true以及should ignore pptr internal scripts if reportAnonymousScripts is truetest/src/coverage.test.ts。includeRawScriptCoverage默认 false是否附带 V8 原始覆盖条目置为true时每个 JSCoverageEntry 会额外携带rawScriptCoverage字段类型为Protocol.Profiler.ScriptCoverage其中包含 V8 给出的函数级原始覆盖数据如每个函数的 ranges 与执行计数便于做更深度的分析默认false时只输出 Puppeteer 整理好的{ url, ranges, text }结构。此开关同时决定启动时传给 CDP 的callCount参数——见下文底层实现小节。测试覆盖可见 test/src/coverage.test.ts开启/关闭该字段时的包含与否均被断言。useBlockCoverage默认 true块级还是函数级覆盖为true默认按代码块粒度收集覆盖信息能识别if分支、条件表达式等更细粒度的未执行路径为false退化为函数级粒度V8 只报告整个函数是否被调用粒度更粗、开销更低。测试中同样有对应验证如should report right ranges for per function scopetest/src/coverage.test.ts。底层实现startJSCoverage 内部发生了什么Coverage.startJSCoverage()的实现非常薄它把工作委托给内部类JSCoveragepackages/puppeteer-core/src/cdp/Coverage.tsasync startJSCoverage(options: JSCoverageOptions {}): Promisevoid { return await this.#jsCoverage.start(options); }而JSCoverage.start()Coverage.ts的核心步骤可以拆解为状态守卫通过assert(!this.#enabled, JSCoverage is already enabled)防止重复开启重复调用会直接抛出断言错误。相应地重复stop()会抛出JSCoverage is not enabled。解构默认值把四个选项分别落库为实例字段并将#enabled置为true、清空脚本 URL / 源码映射表、建立可自动清理的DisposableStack事件订阅。注册两个 CDP 事件监听Debugger.scriptParsed当页面解析出静态script、动态脚本、Web Worker 脚本等任何脚本时触发回调中调用Debugger.getScriptSource拉取源码并存表Runtime.executionContextsCleared导航/上下文销毁时触发与resetOnNavigation联动清空缓存。并发发送四条 CDP 指令完成能力开启await Promise.all([ this.#client.send(Profiler.enable), this.#client.send(Profiler.startPreciseCoverage, { callCount: this.#includeRawScriptCoverage, // 对应 includeRawScriptCoverage detailed: useBlockCoverage, // 对应 useBlockCoverage }), this.#client.send(Debugger.enable), this.#client.send(Debugger.setSkipAllPauses, {skip: true}), ]);这里值得注意的实现细节是callCount恰好映射到includeRawScriptCoverage而detailed映射到useBlockCoverage与上文选项含义一一对应。此外Debugger.setSkipAllPauses被置为skip: true用于防止页面中的debugger语句打断采集流程测试中专门有should not hang when there is a debugger statement用例见 test/src/coverage.test.ts。停止采集时的数据整理与分析配套的stopJSCoverage()详见 docs/api/puppeteer.coverage.stopjscoverage.md返回PromiseJSCoverageEntry[]。JSCoverage.stop()Coverage.ts会并发调用Profiler.takePreciseCoverage、Profiler.stopPreciseCoverage、Profiler.disable、Debugger.disable随后把 V8 返回的函数嵌套区间扁平化经convertToDisjointRangesCoverage.ts合并为互不相交的{ start, end }覆盖区间——该函数用括号序列排序 扫描线算法把多个函数/块的重叠范围规约为最终答案。每个结果条目JSCoverageEntrydocs/api/puppeteer.jscoverageentry.md继承自CoverageEntry含三个核心字段字段类型含义urlstring脚本的 URL匿名脚本为debugger://VMscriptId或 sourceURLtextstring脚本完整源码文本rangesArray{start: number; end: number}已执行代码覆盖的字节区间相对text的偏移rawScriptCoverage可选Protocol.Profiler.ScriptCoverage开启includeRawScriptCoverage后附带的 V8 原始数据关于 sourceURL 的一个反直觉点stopJSCoverage的 Remarksdocs/api/puppeteer.coverage.stopjscoverage.md特别说明JavaScript Coverage 默认不包含匿名脚本但带有 sourceURL 的脚本会被上报。也就是说即便reportAnonymousScripts: false只要动态脚本通过//# sourceURL...注释声明了标识它依然会进入报告V8 会将其视为有 URL 的脚本。测试should report sourceURLstest/src/coverage.test.ts正是对这一行为的验证。完整实战计算页面初始代码利用率API 文档docs/api/puppeteer.coverage.md给出了一个同时测量 JS 与 CSS 覆盖率并换算百分比的标准示例可直接运行// 同时开启 JavaScript 与 CSS 覆盖率采集 await Promise.all([ page.coverage.startJSCoverage(), page.coverage.startCSSCoverage(), ]); // 跳转到目标页面等待页面代码被执行 await page.goto(https://example.com); // 停止采集并取回报告 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()不传参数即等价于传入全部默认值resetOnNavigation: true等适合只测单页加载场景usedBytes的计算规则是区间长度减 1用于消除相邻区间合并时的重叠边界误差与官方示例保持一致想单独评估首页 JS 中有多少被真正执行可把 CSS 部分移除只保留jsCoverage的报告与计算逻辑。常见调参与典型场景建议针对不同需求可以按如下方式组合参数均可选填// 场景一SPA 应用连续导航后做整体统计跨导航累积 await page.coverage.startJSCoverage({ resetOnNavigation: false }); // 场景二希望把 eval/new Function 产生的动态代码也纳入统计 await page.coverage.startJSCoverage({ reportAnonymousScripts: true }); // 场景三需要拿到 V8 原始函数级数据做自定义分析 await page.coverage.startJSCoverage({ includeRawScriptCoverage: true }); // 场景四只关心函数是否被调用降低采集开销 await page.coverage.startJSCoverage({ useBlockCoverage: false }); // 也可以自由组合 await page.coverage.startJSCoverage({ resetOnNavigation: false, reportAnonymousScripts: true, includeRawScriptCoverage: true, });从测试套件看仓库对如下边界情况均有验证实战中可作为预期参考覆盖应报告多个脚本应报告无覆盖脚本与应报告正确区间test/src/coverage.test.ts条件表达式conditionals下的块级覆盖正确性test/src/coverage.test.ts页面存在debugger语句时不会导致采集挂起test/src/coverage.test.tsincludeRawScriptCoverage开/关时原始字段的包含与缺失test/src/coverage.test.ts。注意事项与易踩的坑结合 Coverage 类源码 的实现以下几点值得特别留意不可重复开启同一Coverage实例上连续调用两次startJSCoverage()会因assert抛出JSCoverage is already enabled必须先stopJSCoverage()再重新开启。结束后及时停止采集期间 Puppeteer 保持 Profiler/Debugger 域开启并有事件订阅会带来一定运行时开销采集完应立即调用stopJSCoverage()释放。脚本源码获取可能失败#onScriptParsed中若页面已跳走Debugger.getScriptSource可能失败此时会记入 error 日志并跳过该脚本Coverage.ts这是正常降级而非缺陷。匿名脚本的两套规则容易混淆reportAnonymousScripts: false默认时eval/new Function产生的无 URL 脚本一律不上报但只要动态脚本带//# sourceURL就会被视为有 URL两种开关下都会被上报reportAnonymousScripts: true时其余匿名脚本会以debugger://VMid形式进入报告。Puppeteer 自身注入的脚本会被忽略避免内部工具脚本污染统计结果。延伸阅读Coverage 类总览与综合示例startCSSCoverage / stopCSSCoverageCSS 覆盖率配合使用JSCoverageOptions 接口字段定义JSCoverageEntry 返回条目结构Coverage 类的 CDP 实现源码覆盖率的完整行为测试套件【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考