Puppeteer CSSCoverage.start() 深度解析:如何用 DevTools 协议追踪页面实际使用过的 CSS
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
本篇聚焦 Puppeteer API 参考文档 CSSCoverage.start() 所描述的方法签名与参数,并结合 CSS 覆盖率核心实现 与 覆盖率测试用例 展开:讲清楚start(options)何时调用、resetOnNavigation的默认行为如何影响跨导航统计、方法内部通过 Chrome DevTools Protocol(CDP)启动了哪些能力,以及它与page.coverage.startCSSCoverage()公开入口、stop()结果消费之间的关系。读完后你可以独立实现一套"未使用 CSS 检测"流程,并理解其底层协议链路与边界行为。
方法签名与参数
官方文档给出的CSSCoverage.start()签名为:
class CSSCoverage { start(options?: {resetOnNavigation?: boolean}): Promise<void>; }| 参数 | 类型 | 说明 |
|---|---|---|
| options | { resetOnNavigation?: boolean } | (可选)每次导航时是否重置已收集的覆盖率数据 |
返回值为Promise<void>,当底层 CDP 命令全部下发成功后 resolve。
虽然签名来自 API 参考文档,但需要注意:CSSCoverage类本身是 Puppeteer 内部按页面对象封装的组件,开发者日常使用的公开入口是Page.coverage属性(见 Page 抽象定义),实际调用形式为:
// options 参数类型即 CSSCoverageOptions await page.coverage.startCSSCoverage({resetOnNavigation: false});Coverage类的 startCSSCoverage 实现 只是把options原样转发给内部的CSSCoverage.start(options),两者行为完全一致。
参数类型定义
对应 TypeScript 接口定义在 Coverage.ts 的 CSSCoverageOptions:
export interface CSSCoverageOptions { /** * Whether to reset coverage on every navigation. */ resetOnNavigation?: boolean; }resetOnNavigation 的默认值与生效机制
从源码看,start()中对选项做了展开默认值处理(Coverage.ts#L358-L361):
async start(options: {resetOnNavigation?: boolean} = {}): Promise<void> { assert(!this.#enabled, 'CSSCoverage is already enabled'); const {resetOnNavigation = true} = options; this.#resetOnNavigation = resetOnNavigation; ... }也就是说resetOnNavigation不传时默认为true,这与 API 参考文档中"可选参数"的表述互补:文档说明它可选,源码补充了它的默认值语义。
其生效时机是浏览器执行上下文被清空时(页面导航触发Runtime.executionContextsCleared事件),对应处理逻辑:
#onExecutionContextsCleared(): void { if (!this.#resetOnNavigation) { return; } this.#stylesheetURLs.clear(); this.#stylesheetSources.clear(); }resetOnNavigation: true(默认):每次导航后,此前页面收集的样式表记录全部清空,stop()只返回当前页面的覆盖率;resetOnNavigation: false:跨导航累计,适合测量 SPA 或多页应用中"整个会话"用到了哪些样式。
这一行为有明确的测试佐证(test/src/coverage.test.ts#L297-L316):
describe('resetOnNavigation', function () { it('should report stylesheets across navigations', async () => { await page.coverage.startCSSCoverage({resetOnNavigation: false}); await page.goto(server.PREFIX + '/csscoverage/multiple.html'); await page.goto(server.EMPTY_PAGE); const coverage = await page.coverage.stopCSSCoverage(); expect(coverage).toHaveLength(2); // 两次导航的样式表都被保留 }); it('should NOT report scripts across navigations', async () => { await page.coverage.startCSSCoverage(); // Enabled by default. await page.goto(server.PREFIX + '/csscoverage/multiple.html'); await page.goto(server.EMPTY_PAGE); const coverage = await page.coverage.stopCSSCoverage(); expect(coverage).toHaveLength(0); // 默认 true,导航后数据被重置 }); });start() 内部的协议链路
start()并不只是一句"开启开关",从 Coverage.ts#L358-L380 可以看到它依次完成四件事:
- 防重入断言:
assert(!this.#enabled, 'CSSCoverage is already enabled')。重复调用start会直接抛出CSSCoverage is already enabled断言错误,因此典型用法是"start 一次 → 操作页面 → stop 一次",而不是每步都 start。 - 状态复位与监听器注册:清空
#stylesheetURLs、#stylesheetSources两张映射表(样式表 ID 到 URL、ID 到源码文本),并通过DisposableStack管理两个 CDP 事件监听器:CSS.styleSheetAdded:页面每加载一张样式表都会触发,回调 onStyleSheet 会通过CSS.getStyleSheetText命令拉取完整样式文本并缓存;Runtime.executionContextsCleared:如上文所述,驱动resetOnNavigation逻辑。
- 并行下发三条 CDP 命令:
await Promise.all([ this.#client.send('DOM.enable'), this.#client.send('CSS.enable'), this.#client.send('CSS.startRuleUsageTracking'), ]);其中CSS.startRuleUsageTracking是关键——它让浏览器开始统计每条 CSS 规则是否被实际匹配应用(rule usage tracking)。stop()阶段正是通过CSS.stopRuleUsageTracking取回这份ruleUsage数据,逐条转换为{startOffset, endOffset, count: used ? 1 : 0}的区间记录(Coverage.ts#L408-L433)。 4. 由于start是async方法且末尾await Promise.all([...]),await start(...)返回即代表追踪已就绪,此时再导航不会遗漏样式表。
样式表收集的两条隐性规则
结合onStyleSheet实现,有两个容易踩坑的边界行为,均被 coverage.test.ts 中的用例固化:
- 没有
sourceURL的样式表会被忽略。回调开头即if (!header.sourceURL) return;,因此通过page.addStyleTag({content: '...'})注入的内联样式不会出现在覆盖率报告中(测试用例 "should ignore injected stylesheets" 断言coverage长度为 0)。需要按 URL 归组统计时,样式必须来自真实文件;不过源码支持sourceURL注释,测试用例 "should report sourceURLs" 验证了带@sourceURL的样式会以自定义名称(如nicename.css)上报。 - 样式文本拉取失败会被静默记录。若页面在拉取文本前已经导航离开,
CSS.getStyleSheetText可能抛错,此时仅通过 logger(DEBUG_PREFIXES.error)输出错误日志而不中断整体流程,保证覆盖率统计的健壮性。
与 stop() 配套使用:完整工作流
start()只负责"开始追踪",结果消费由stop()完成。stop()返回CoverageEntry[],每个条目的结构定义在 Coverage.ts#L21-L34:
export interface CoverageEntry { url: string; // 样式表或脚本的 URL text: string; // 样式表或脚本的完整内容 ranges: Array<{start: number; end: number}>; // 被使用的字符区间(半开区间) }ranges是由CSS.stopRuleUsageTracking返回的嵌套规则区间经 convertToDisjointRanges 函数做扫描线交集计算后得到的互不重叠区间,可以直接用text.substring(range.start, range.end)取回"被用到的 CSS 片段"。官方文档类注释(Coverage.ts#L95-L116)给出的标准用法如下,将 JS 与 CSS 覆盖率合并计算总使用率:
// 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}%`);注意usedBytes累加时用了range.end - range.start - 1,因为ranges区间是"起始包含、结束不包含"的偏移约定(与测试断言text.substring(range.start, range.end)一致),按字符数统计时要减 1。
行为边界一览(来自测试用例)
test/src/coverage.test.ts 的CSSCoverage用例集基本覆盖了start()后的所有关键场景:
| 场景 | 测试用例 | 断言结果 |
|---|---|---|
| 基础追踪 | simple.html | 1 个条目,区间{start:1, end:22}对应div { color: green; } |
| 多样式表 | multiple.html | 返回 2 个条目,分别对应 stylesheet1.css / stylesheet2.css |
| 零命中样式表 | unused.html | 条目存在但ranges为空数组(未使用的 CSS 以空区间体现) |
| 媒体查询 | media.html | 按规则分别统计,产生多个离散区间 |
| 空样式表 | empty.html | text为空字符串的条目仍然返回 |
| 延迟加载的样式表 | "recently loaded stylesheet" | start 之后动态<link>注入的样式表也会被纳入 |
| 动态注入内联样式 | addStyleTag 注入 | 不产生任何条目(无 sourceURL) |
| 复杂综合页面 | involved.html | 与 golden 文件 csscoverage-involved.txt 逐字比对 |
使用建议与限制
- 调用时序:
startCSSCoverage()必须在page.goto()等导航之前await完成,否则首屏加载的样式表不会被收集;start之后页面每加载一张样式表都会触发一次CSS.getStyleSheetText拉取,对大量样式表的页面会有一定的额外协议流量开销。 - CDP 专属能力:整条链路(
DOM.enable、CSS.enable、CSS.startRuleUsageTracking)都基于 Chrome DevTools Protocol,因此该功能只在走 CDP 连接的浏览器(Chrome/Chromium 系)下可用。 - 与 Istanbul 生态衔接:
Coverage类注释中说明,若需将结果转换为 Istanbul 可消费格式,可参考社区工具 puppeteer-to-istanbul;仓库内 browsers API 文档 与 coverage.startcsscoverage 文档、CSSCoverage 类文档、stop() 文档 可作为本主题的延伸阅读。
小结
CSSCoverage.start()是 Puppeteer CSS 覆盖率追踪链路的起点:一个可选的resetOnNavigation(默认true)决定跨导航是否累计数据;方法内部通过注册CSS.styleSheetAdded、Runtime.executionContextsCleared监听并下发CSS.startRuleUsageTracking等 CDP 命令,把"样式表收集"与"规则命中追踪"两条数据流同时挂起,最终由stop()汇聚成{url, text, ranges}报告。理解了 Coverage.ts 中这 30 行左右的start实现,再对照 coverage.test.ts 的边界用例,就能准确预测resetOnNavigation、内联样式、延迟加载样式等场景下的实际输出。
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考