用CDP实现自定义DevTools功能
本节使用 CDP 实现一个实际功能——代码覆盖率检测。
2024-2026 更新:chrome-remote-interface 已更新,支持 Pipe 模式连接。Puppeteer v23+ 默认使用 Pipe 而非 WebSocket,v24 延续该默认,并移除了
Browser.isConnected()方法(改用browser.connected属性)。
Chrome DevTools 的 Coverage 功能
Chrome DevTools 提供了覆盖率检测功能(Coverage 面板),可以检测 JS、CSS 代码中哪些已执行、哪些未执行。在 Sources 面板中,绿色的部分是已执行的,红色的部分是未执行的。
该功能可以帮助分析哪些代码是多余的,从而进行延后加载或删除等优化。
用 CDP 实现覆盖率检测
整体流程
图表渲染中…
使用 chrome-remote-interface
javascript
const CDP = require('chrome-remote-interface');
async function analyzeCoverage(url) {
let client;
try {
// 连接到 Chrome(默认端口 9222)
client = await CDP({ host: '127.0.0.1', port: 9222 });
const { Page, Debugger, Runtime, CSS, Profiler } = client;
// 启用相关 Domain
await Page.enable();
await Debugger.enable();
await DOM.enable();
await CSS.enable();
await Profiler.enable();
// 收集 JS 和 CSS 源码
const cssMap = new Map();
const jsMap = new Map();
CSS.on('styleSheetAdded', async (event) => {
const styleSheetId = event.header.styleSheetId;
const { text } = await CSS.getStyleSheetText({ styleSheetId });
cssMap.set(styleSheetId, {
meta: event.header,
content: text,
});
});
Debugger.on('scriptParsed', async (event) => {
const scriptId = event.scriptId;
const { scriptSource } = await Debugger.getScriptSource({ scriptId });
jsMap.set(scriptId, {
meta: event,
content: scriptSource,
});
});
// 开始跟踪执行情况
await CSS.startRuleUsageTracking();
await Profiler.startPreciseCoverage({ callCount: true, detailed: true });
// 导航到目标页面
await Page.navigate({ url });
// 等待页面执行完成
await new Promise(resolve => setTimeout(resolve, 3000));
// 获取覆盖率数据
const cssCoverage = await CSS.stopRuleUsageTracking();
const jsCoverage = await Profiler.takePreciseCoverage();
// 计算覆盖率
const report = calculateCoverage(cssMap, jsMap, cssCoverage, jsCoverage);
return report;
} catch (err) {
console.error(err);
} finally {
if (client) await client.close();
}
}覆盖率计算
javascript
function calculateCoverage(cssMap, jsMap, cssCoverage, jsCoverage) {
const result = {
css: [],
js: [],
totalUnusedBytes: 0,
totalBytes: 0,
};
// CSS 覆盖率
for (const [id, data] of cssMap) {
const total = data.content.length;
const used = cssCoverage.ruleUsage
.filter(rule => rule.styleSheetId === id)
.reduce((sum, rule) => {
const startOffset = rule.startOffset;
const endOffset = rule.endOffset;
return sum + (endOffset - startOffset);
}, 0);
result.css.push({
url: data.meta.sourceURL,
totalBytes: total,
usedBytes: used,
unusedBytes: total - used,
coverage: (used / total * 100).toFixed(2) + '%',
});
result.totalBytes += total;
result.totalUnusedBytes += (total - used);
}
// JS 覆盖率
for (const [id, data] of jsMap) {
const total = data.content.length;
const usedRanges = jsCoverage.result
.filter(entry => entry.scriptId === id)
.flatMap(entry => entry.functions);
const used = usedRanges
.filter(fn => fn.count > 0)
.reduce((sum, fn) => sum + (fn.endOffset - fn.startOffset), 0);
result.js.push({
url: data.meta.url,
totalBytes: total,
usedBytes: used,
unusedBytes: total - used,
coverage: (used / total * 100).toFixed(2) + '%',
});
result.totalBytes += total;
result.totalUnusedBytes += (total - used);
}
return result;
}输出覆盖率报告
javascript
async function main() {
const url = process.argv[2] || 'https://example.com';
// 先启动 Chrome
// chrome --remote-debugging-port=9222
const report = await analyzeCoverage(url);
console.log('=== CSS Coverage ===');
report.css.forEach(entry => {
console.log(`${entry.url}: ${entry.coverage} used (${entry.unusedBytes} bytes unused)`);
});
console.log('=== JS Coverage ===');
report.js.forEach(entry => {
console.log(`${entry.url}: ${entry.coverage} used (${entry.unusedBytes} bytes unused)`);
});
console.log(`\nTotal: ${report.totalBytes} bytes, ${report.totalUnusedBytes} unused`);
console.log(`Overall coverage: ${((report.totalBytes - report.totalUnusedBytes) / report.totalBytes * 100).toFixed(2)}%`);
}
main();CDP Domain 对照
| 功能 | CDP Domain | 方法/事件 |
|---|---|---|
| 收集 CSS 源码 | CSS | styleSheetAdded 事件 + getStyleSheetText |
| 收集 JS 源码 | Debugger | scriptParsed 事件 + getScriptSource |
| CSS 执行跟踪 | CSS | startRuleUsageTracking → stopRuleUsageTracking |
| JS 执行跟踪 | Profiler | startPreciseCoverage → takePreciseCoverage |
Lighthouse 的数据收集原理
Lighthouse 的 CLI 版本就是通过这种方式来收集数据的:
图表渲染中…
如果需要开发网页分析工具或自定义 DevTools 功能,都可以采用类似的思路:通过 CDP 收集数据 → 分析数据 → 输出报告。
CDP 的连接方式
2024-2026 更新:CDP 有三种连接方式:
图表渲染中…
Pipe 模式连接
2024-2026 新增:Puppeteer v23+ 默认使用 Pipe 模式(v24 延续),chrome-remote-interface 也支持 Pipe:
javascript
const CDP = require('chrome-remote-interface');
// Pipe 模式连接(需要 chrome --remote-debugging-pipe)
const client = await CDP({
target: pipeProcess, // 通过 Pipe 进程连接
});