Lighthouse 报告生成器Report Generator解析从 LHR 到 HTML / JSON / CSV 的完整链路【免费下载链接】lighthouseAutomated auditing, performance metrics, and best practices for the web.项目地址: https://gitcode.com/GitHub_Trending/lig/lighthouseLighthouse 在完成采集、评分之后最终产物是一个结构化的LHRLighthouse Result对象而 report/generator/README.md 所描述的报告生成器Report Generator正是把这个对象翻译成用户可读报告的入口它接受一个 LHR或 User Flow 的 FlowResult输出 HTML、JSON、CSV 三种格式。本文将结合当前仓库的 report-generator.js、report-assets.js、file-namer.js 及对应测试讲清楚报告生成的调用链、模板注入机制、CSV 格式约定、浏览器端编译原理并给出 CLI 层面的实际用法。报告生成器在整个 Lighthouse 中的定位从职责上看报告生成器位于审计结果产出与结果交付之间core计算出 LHR 后并不直接关心用户要的是网页报告还是 CSV 表格而是把格式化工作交给ReportGenerator。这一点从 core/index.js 的导出可以看出function generateReport(result, format html) { const reportOutput ReportGenerator.generateReport(result, format); // ... }ReportGenerator被core复用而core又同时服务于 CLI、DevTools、Lightrider 等多种前端因此 README 明确了一条工程约束该模块在 core 与 report 之间共享代码与类型依赖都应保持最小。从目录结构看report/generator 下只有report-generator.js、report-assets.js、flow-report-assets.js、file-namer.js和tsconfig.json五个文件且tsconfig.json只引入了types/lhr与shared两个引用正是这一约束的落地。输入LHR 与 FlowResult 的统一判别生成器支持两种输入对象LHRLighthouse Result单次运行navigation / timespan / snapshot 模式的完整结果包含requestedUrl、fetchTime、categories、audits等顶层字段FlowResultUser Flow 结果由多个步骤step组成每个步骤内嵌一个lhr用于用户流式场景。generateReport内部通过isFlowResult做类型收窄其实现非常轻量report-generator.jsstatic isFlowResult(result) { return steps in result; }只要对象上存在steps字段即判定为 FlowResult随后在 HTML 分支中分发到generateFlowReportHtml在 CSV 分支中则直接抛错——因为 CSV 输出不支持用户流详见下文。统一入口generateReport三种格式的分发所有输出格式的调度都收敛在ReportGenerator.generateReport[report-generator.js](https://link.gitcode.com/i/e8a6fc3bda2674abbf4285ac08391f91#L157-L191。它的第二个参数outputModes既可以是单个字符串也可以是字符串数组对应LH.OutputMode枚举合法值即json | html | csvstatic generateReport(result, outputModes) { const outputAsArray Array.isArray(outputModes); if (typeof outputModes string) outputModes [outputModes]; const output outputModes.map(outputMode { if (outputMode html) { if (ReportGenerator.isFlowResult(result)) { return ReportGenerator.generateFlowReportHtml(result); } return ReportGenerator.generateReportHtml(result); } if (outputMode csv) { if (ReportGenerator.isFlowResult(result)) { throw new Error(CSV output is not support for user flows); } return ReportGenerator.generateReportCSV(result); } if (outputMode json) { return JSON.stringify(result, null, 2); } throw new Error(Invalid output mode: outputMode); }); return outputAsArray ? output : output[0]; }要点多格式一次生成传入[json, html]会返回一个数组两个元素分别是对应格式的字符串传入单个字符串则返回单个字符串。测试 report-generator-test.js 专门覆盖了数组模式。JSON 即序列化JSON 分支就是对结果做JSON.stringify(result, null, 2)的格式化输出没有任何信息损耗。非法模式抛错Invalid output mode: outputMode保证未知字符串不会静默通过。HTML 报告模板 JSON 注入的安全机制HTML 是 Lighthouse 最常用的输出形态。generateReportHtmlreport-generator.js的核心思路是把一份独立的模板字符串拿过来将两个占位符替换为真实内容static generateReportHtml(lhr) { const sanitizedJson ReportGenerator.sanitizeJson(lhr); const sanitizedJavascript reportAssets.REPORT_JAVASCRIPT.replace(/\//g, \\u003c/); return ReportGenerator.replaceStrings(reportAssets.REPORT_TEMPLATE, [ {search: %%LIGHTHOUSE_JSON%%, replacement: sanitizedJson}, {search: %%LIGHTHOUSE_JAVASCRIPT%%, replacement: sanitizedJavascript}, ]); }这里有两个值得展开的安全细节1.sanitizeJson防 XSS 的关键转义报告是单文件自包含的 HTML审计数据可能来自不可信的第三方站点内容会被原样写进script标签内的 JSON 中。sanitizeJsonreport-generator.js做三件事static sanitizeJson(object) { return JSON.stringify(object) .replace(//g, \\u003c) // 替换开标签防止 /script 提前闭合 .replace(/\u2028/g, \\u2028) // 行分隔符 .replace(/\u2029/g, \\u2029); // 段落分隔符 }被统一替换为\u003c即使审计数据里出现/scriptscript...也无法破坏外层脚本\u2028/\u2029在 ES2019 之前的 JavaScript 中属于非法的字符串字面量字符直接内嵌会导致解析错误因此也被显式转义。对应的攻击场景在测试 report-generator-test.js 中被精确验证传入hax\u2028hax/scriptscriptconsole.log(pwned);...生成结果必须同时满足JSON 已转义与渲染脚本未被替换两个断言。2.replaceStrings无串行替换的模板引擎replaceStringsreport-generator.js是一个刻意保持朴素的替换实现它按顺序取出每一对{search, replacement}用split map join递归处理从而保证后一个替换不会作用于前一个替换刚产生的结果。测试中的用例很能说明问题ReportGenerator.replaceStrings(%1, [ {search: %1, replacement: %2}, {search: %2, replacement: pwnd}, ]); // 结果是 %2而不是 pwnd如果使用String.prototype.replace配合回调用户数据里出现$、$等特殊模式时会被当作替换引用产生意外结果——测试pre$post与cannot be tricked两个断言report-generator-test.js就是为了防止这类注入。自定义模板引擎只用split/join天然免疫该问题。3. 资产从哪来report-assets.js的运行时读盘模板与渲染脚本并不硬编码在源码中而是在运行时通过fs.readFileSync读取report-assets.jsconst REPORT_TEMPLATE fs.readFileSync(moduleDir /../assets/standalone-template.html, utf8); const REPORT_JAVASCRIPT fs.readFileSync(moduleDir /../../dist/report/standalone.js, utf8); export const reportAssets { REPORT_TEMPLATE, REPORT_JAVASCRIPT, ...flowReportAssets, };REPORT_TEMPLATE来自 report/assets/standalone-template.html内含%%LIGHTHOUSE_JSON%%、%%LIGHTHOUSE_JAVASCRIPT%%占位符REPORT_JAVASCRIPT来自构建产物dist/report/standalone.js即渲染器report/renderer 目录下的report-renderer.js等打包压缩后的结果之所以用fs.readFileSync读文件而非直接 import 文本正是为了配合下文要讲的inline-fs 编译。浏览器端也能运行inline-fs 编译原理README 特别强调生成器原生运行在 Node.js 中但经过打包管线的一次编译后也能在浏览器中运行。这次编译使用inline-fs它会找到源码里所有的fs.readFileSync()调用把调用替换成字面量的文件内容字符串stringified file content。也就是说report-assets.js里的两个readFileSync在浏览器构建产物中会变成类似const REPORT_TEMPLATE !doctype html...的形式运行时不再依赖文件系统。这样 devtools、extension浏览器插件等无法访问 Node fs 的场景也能在端上直接调用生成器把 LHR 即时渲染成 HTML。Flow Report用户流报告的 HTML 生成当输入是 FlowResult 时走generateFlowReportHtmlreport-generator.js逻辑与单次报告一致但注入位点更多static generateFlowReportHtml(flow) { const sanitizedJson ReportGenerator.sanitizeJson(flow); const sanitizedJavascript reportAssets.FLOW_REPORT_JAVASCRIPT.replace(/\//g, \\u003c/); return ReportGenerator.replaceStrings(reportAssets.FLOW_REPORT_TEMPLATE, [ {search: %%LIGHTHOUSE_FLOW_JSON%%, replacement: sanitizedJson}, {search: %%LIGHTHOUSE_FLOW_JAVASCRIPT%%, replacement: sanitizedJavascript}, {search: /*%%LIGHTHOUSE_FLOW_CSS%%*/, replacement: reportAssets.FLOW_REPORT_CSS}, ]); }Flow 报告额外注入 CSS 占位符/*%%LIGHTHOUSE_FLOW_CSS%%*/。对应的资产在 flow-report-assets.js 中定义模板来自 flow-report/assets/standalone-flow-template.htmlJS 来自dist/report/flow.jsCSS 由普通报告样式report/assets/styles.css与 Flow 样式flow-report/assets/styles.css拼接而成。另外注意report-assets.js中的注释flowReportAssets通过展开运算符挂在reportAssets下但不是每个 bundle 都需要 Flow 资产——构建时通过rollupPlugins.shim等插件替换/忽略flow-report-assets.js即可把 Flow 部分从产物中剔除保持单次报告场景的包体精简。CSV 输出遵循 RFC 4180 的审计明细表generateReportCSVreport-generator.js把 LHR 摊平成一张表格其设计原则是遵循 RFC 4180 规范。整体行结构分为四段元数据段首行是列头requestedUrl, finalDisplayedUrl, fetchTime, gatherMode第二行是对应取值缺失字段写null空行分隔分类段列头category, score随后每个分类一行如performance,0.32空行分隔审计明细段列头category, audit, score, displayValue, description逐分类遍历其auditRefs按审计 ID 取lhr.audits中的详情输出。转义规则同样照搬 RFC 4180每个单元格用双引号包裹内部双引号翻倍转义→行分隔使用 CRLF\r\n列分隔使用逗号。测试通过真实的csv-validator对输出做解析校验report-generator-test.js并快照了前 15 行其中能看到典型输出形如requestedUrl,finalDisplayedUrl,fetchTime,gatherMode http://localhost:10200/dobetterweb/dbw_tester.html,http://localhost:10200/dobetterweb/dbw_tester.html,2026-04-23T16:18:53.230Z,navigation category,score performance,0.32 accessibility,0.75 category,audit,score,displayValue,description performance,first-contentful-paint,0.02,6.8 s,First Contentful Paint marks the time at which...需要强调的边界User Flow 结果不支持 CSV调用会抛出CSV output is not support for user flows测试 report-generator-test.js 专门断言了这一点。原因从结构上很好理解CSV 是单层扁平表无法表达多步骤用户流的嵌套关系。报告文件命名file-namer.jsCLI 在写盘前需要为报告生成文件名前缀这个职责独立在 file-namer.js 中。命名约定是name_YYYY-MM-DD_HH-MM-SS并保证不含文件名非法字符function getFilenamePrefix(name, fetchTime) { const date fetchTime ? new Date(fetchTime) : new Date(); // 手工拼接 YYYY-MM-DD 与 HH-MM-SSNode 的 ICU 支持不可靠不能依赖 toLocaleString 直接格式化日期 const filenamePrefix ${name}_${dateStr}_${timeStr}; // 把对文件名不友好的字符替换为 - return filenamePrefix.replace(/[/?\\:*|]/g, -); }它导出三个函数getFilenamePrefix(name, fetchTime)基础实现getLhrFilenamePrefix(lhr)从lhr.finalDisplayedUrl取 hostname 作为前缀例如对https://example.com生成example.com_2026-09-09_10-30-45getFlowResultFilenamePrefix(flowResult)以用户流的name空格转-为前缀时间取自第一个 step 的lhr.fetchTime。CLI 侧的实际调用在 cli/run.js普通 LHR 用getLhrFilenamePrefixFlowResult 用getFlowResultFilenamePrefix。测试 file-namer-test.js 用正则hostname_\d{4}-...-SS校验了前缀的完整格式。文件命名遵循本地时区toLocaleTimeString/toLocaleDateString均为本地时间这是 CLI 输出到用户磁盘时的直观预期也解释了为什么测试只断言格式而不断言具体时刻。与 CLI 的集成--output与 PrinterCLI 层面--output参数支持多个值choices: json, html, csv见 cli/cli-flags.js例如一次运行同时产出 HTML 与 JSONnpx lighthouse https://example.com --outputhtml --outputjson --output-path./reportcli/printer.js 维护了与生成器一致的OutputMode枚举并提供两段式写盘能力未指定--output-path时输出到 stdout带 50ms 延迟以避免与调试日志竞争cli/printer.js指定路径时自动mkdir -p创建目录后写入文件cli/printer.js并在成功后打印xxx output written to path。因此完整的调用链是CLI 解析--output→core的generateReport调用ReportGenerator.generateReport→ 返回字符串 →Printer.write落盘或打印。报告生成器本身不关心输出到哪里只负责把 LHR 变成指定格式的字符串职责边界非常清晰。小结Lighthouse 报告生成器是一个小而专的模块其设计值得借鉴的点可以归纳为三条输入输出契约简单统一接受 LHR / FlowResult按outputMode分发到 HTML含 Flow、JSON、CSV 三个生成器多格式通过数组参数一次产出安全性内建sanitizeJson的\u003c/\u2028/\u2029转义与replaceStrings的无串行替换从根上堵住了/script注入和String.replace特殊模式注入环境无关Node 端直接readFileSync读资产浏览器端由inline-fs编译为内联字符串同一份源码两端通用同时通过 spread 结构与构建插件支持按需剔除 Flow 资产。若想深入验证上述行为可以阅读 report-generator-test.js含 XSS 注入、RFC 4180 校验、数组输出等 13 个用例与 file-namer-test.js若要在此基础上二次开发自定义输出格式建议直接扩展 report-generator.js 中的分发分支并保持依赖最小化的约束。【免费下载链接】lighthouseAutomated auditing, performance metrics, and best practices for the web.项目地址: https://gitcode.com/GitHub_Trending/lig/lighthouse创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考