在本教程中,我将逐步向您展示如何使用 Node.js 创建一个简单的工具,通过命令行运行 Google Lighthouse 审核,以 JSON 格式保存他们生成的报告,然后进行比较,以便在网站增长和发展时监控网站性能。
我希望这可以作为对任何有兴趣了解如何以编程方式使用 Google Lighthouse 的开发人员的良好介绍。
但首先,对于那些不了解的人……
什么是 Google Lighthouse?
Google Lighthouse 是网络开发人员工具集中可用的最佳自动化工具之一。它允许您快速审核网站的几个关键领域,这些领域可以共同构成衡量其整体质量的指标。这些是
- 性能
- 无障碍性
- 最佳实践
- SEO
- 渐进式 Web 应用程序
审核完成后,将生成一份关于您的网站做得好的地方和做得不好的地方的报告,后者旨在作为指示下一步改进页面的指标。
这是一个完整的报告示例。
除了其他一般诊断和网络性能指标外,报告的一个非常有用的功能是,每个关键领域都被汇总到 0-100 之间的颜色编码分数。
这不仅允许开发人员在无需进一步分析的情况下快速评估网站的质量,而且还允许非技术人员(例如利益相关者或客户)也理解。
例如,这意味着在花费时间改进网站无障碍性后,更容易与市场部的希瑟分享胜利,因为她在看到 Lighthouse 无障碍性得分上升 50 分进入绿色后,能够更好地理解这一努力。
但同样,项目经理西蒙可能不明白“速度指数”或“首屏绘制时间”的含义,但当他看到 Lighthouse 报告显示网站性能得分深陷红色时,他就知道你还有工作要做。

如果您使用的是 Chrome 或最新版本的 Edge,您可以立即使用 DevTools 为自己运行 Lighthouse 审核。方法如下
您还可以通过 PageSpeed Insights 或流行的性能工具(如 WebPageTest)在线运行 Lighthouse 审核。
但是,今天,我们只对 Lighthouse 作为 Node 模块感兴趣,因为这使我们能够以编程方式使用该工具来审核、记录和比较 Web 性能指标。
让我们看看如何做。
设置
首先,如果您还没有安装 Node.js,则需要安装它。有无数种方法可以安装它。我使用 Homebrew 包管理器,但如果您愿意,也可以直接从 Node.js 网站 下载安装程序。本教程是在 Node.js v10.17.0 的基础上编写的,但在过去几年发布的大多数版本上也很有可能正常工作。
您还需要安装 Chrome,因为我们将使用它来运行 Lighthouse 审核。
接下来,为项目创建一个新目录,然后在控制台中 cd
到该目录。然后运行 npm init
开始创建 package.json
文件。此时,我建议您一直按 Enter 键跳过尽可能多的步骤,直到文件创建完成。
现在,让我们在项目目录中创建一个新文件。我将其命名为 lh.js
,但您可以随意命名。它将包含工具的所有 JavaScript 代码。在您选择的文本编辑器中打开它,现在,写入一个 console.log
语句。
console.log('Hello world');
然后在控制台中,确保您的 CWD(当前工作目录)是您的项目目录,然后运行 node lh.js
(用您使用的文件名替换我的文件名)。
您应该看到
$ node lh.js
Hello world
如果没有,请检查您的 Node 安装是否正常,以及您是否确实位于正确的项目目录中。
现在,我们已经完成了这些步骤,可以继续开发工具本身。
使用 Node.js 打开 Chrome
让我们安装项目的第一个依赖项:Lighthouse 本身。
npm install lighthouse --save-dev
这将创建一个包含所有包文件的 node_modules
目录。如果您使用的是 Git,您唯一需要做的就是将其添加到 .gitignore
文件中。
在 lh.js
中,您接下来需要删除测试 console.log()
并导入 Lighthouse 模块,以便您可以在代码中使用它。如下所示
const lighthouse = require('lighthouse');
在它下面,您还需要导入一个名为 chrome-launcher 的模块,它是 Lighthouse 的依赖项之一,允许 Node 自行启动 Chrome,以便可以运行审核。
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
现在我们已经可以使用这两个模块了,让我们创建一个简单的脚本,该脚本只打开 Chrome、运行 Lighthouse 审核,然后将报告打印到控制台。
创建一个接受 URL 作为参数的新函数。由于我们将使用 Node.js 运行它,因此我们可以安全地使用 ES6 语法,因为我们不必担心那些令人讨厌的 Internet Explorer 用户。
const launchChrome = (url) => {
}
在函数中,我们要做的第一件事是使用我们导入的 chrome-launcher 模块打开 Chrome,并将其发送到通过 url
参数传递的任何参数。
我们可以使用它的 launch()
方法和它的 startingUrl
选项来实现这一点。
const launchChrome = url => {
chromeLauncher.launch({
startingUrl: url
});
};
在下面调用该函数并传递您选择的 URL 会导致在运行 Node 脚本时在该 URL 上打开 Chrome。
launchChrome('https://www.lukeharrison.dev');
launch 函数实际上返回一个 promise,它使我们能够访问包含一些有用方法和属性的对象。
例如,使用下面的代码,我们可以打开 Chrome,将对象打印到控制台,然后使用它的 kill()
方法在三秒钟后关闭 Chrome。
const launchChrome = url => {
chromeLauncher
.launch({
startingUrl: url
})
.then(chrome => {
console.log(chrome);
setTimeout(() => chrome.kill(), 3000);
});
};
launchChrome("https://www.lukeharrison.dev");
现在我们已经搞定了 Chrome,让我们继续使用 Lighthouse。
以编程方式运行 Lighthouse
首先,让我们将 launchChrome()
函数重命名为更能反映其最终功能的名称:launchChromeAndRunLighthouse()
。完成困难的部分后,我们现在可以使用本教程前面导入的 Lighthouse 模块了。
在 Chrome 启动器的 then 函数中(它只在浏览器打开后执行),我们将向 Lighthouse 传递该函数的 url
参数,并触发对该网站的审核。
const launchChromeAndRunLighthouse = url => {
chromeLauncher
.launch({
startingUrl: url
})
.then(chrome => {
const opts = {
port: chrome.port
};
lighthouse(url, opts);
});
};
launchChromeAndRunLighthouse("https://www.lukeharrison.dev");
要将 Lighthouse 实例链接到我们的 Chrome 浏览器窗口,我们必须传递它的端口以及 URL。
如果您现在运行此脚本,您将在控制台中遇到错误
(node:47714) UnhandledPromiseRejectionWarning: Error: You probably have multiple tabs open to the same origin.
要解决此问题,我们只需要从 Chrome Launcher 中删除 startingUrl
选项,并让 Lighthouse 从这里开始处理 URL 导航。
const launchChromeAndRunLighthouse = url => {
chromeLauncher.launch().then(chrome => {
const opts = {
port: chrome.port
};
lighthouse(url, opts);
});
};
如果您执行此代码,您会注意到似乎正在发生一些事情。我们只是没有在控制台中收到任何反馈来确认 Lighthouse 审核确实已经运行,Chrome 实例也没有像以前那样自动关闭。
幸运的是,lighthouse()
函数返回一个 promise,它使我们能够访问审核结果。
让我们关闭 Chrome,然后通过 results 对象的 report 属性以 JSON 格式将这些结果打印到终端。
const launchChromeAndRunLighthouse = url => {
chromeLauncher.launch().then(chrome => {
const opts = {
port: chrome.port
};
lighthouse(url, opts).then(results => {
chrome.kill();
console.log(results.report);
});
});
};
虽然控制台不是显示这些结果的最佳方式,但如果您要将其复制到剪贴板并访问 Lighthouse 报告查看器,将结果粘贴到这里将显示完整的报告。
此时,重要的是整理一下代码,使 launchChromeAndRunLighthouse()
函数在执行完毕后返回报告。这使我们能够稍后处理报告,而不会导致混乱的 JavaScript 金字塔。
const lighthouse = require("lighthouse");
const chromeLauncher = require("chrome-launcher");
const launchChromeAndRunLighthouse = url => {
return chromeLauncher.launch().then(chrome => {
const opts = {
port: chrome.port
};
return lighthouse(url, opts).then(results => {
return chrome.kill().then(() => results.report);
});
});
};
launchChromeAndRunLighthouse("https://www.lukeharrison.dev").then(results => {
console.log(results);
});
您可能已经注意到,我们的工具目前只能审核一个网站。让我们更改它,以便您可以通过命令行传递 URL 作为参数。
为了消除处理命令行参数的麻烦,我们将使用一个名为 yargs 的包来处理它们。
npm install --save-dev yargs
然后在脚本顶部导入它,以及 Chrome Launcher 和 Lighthouse。我们在这里只需要它的 argv
函数。
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;
这意味着如果您要在终端中传递一个命令行参数,如下所示
node lh.js --url https://www.google.co.uk
…您可以在脚本中访问该参数,如下所示
const url = argv.url // https://www.google.co.uk
让我们编辑我们的脚本,将命令行 URL 参数传递给该函数的 url
参数。重要的是,要通过 if
语句和错误消息添加一个安全网,以防没有传递任何参数。
if (argv.url) {
launchChromeAndRunLighthouse(argv.url).then(results => {
console.log(results);
});
} else {
throw "You haven't passed a URL to Lighthouse";
}
好了!我们现在拥有一个可以以编程方式启动 Chrome 并运行 Lighthouse 审核,然后以 JSON 格式将报告打印到终端的工具。
保存 Lighthouse 报告
将报告打印到控制台并不是很有用,因为您无法轻松阅读其内容,它们也不会保存以备将来使用。在本教程的这一部分中,我们将更改此行为,以便每个报告都保存到自己的 JSON 文件中。
为了防止来自不同网站的报告混淆,我们将按如下方式组织它们
- lukeharrison.dev
- 2020-01-31T18:18:12.648Z.json
- 2020-01-31T19:10:24.110Z.json
- cnn.com
- 2020-01-14T22:15:10.396Z.json
- lh.js
我们将使用时间戳命名报告,时间戳指示报告生成的日期/时间。这意味着不会出现两个报告文件名相同的情况,并且它将帮助我们轻松区分不同的报告。
Windows 中存在一个需要我们注意的问题:冒号(:
)是文件名中的非法字符。为了缓解这个问题,我们将用下划线(_
)替换所有冒号,因此典型的报告文件名将类似于
- 2020-01-31T18_18_12.648Z.json
创建目录
首先,我们需要操作命令行 URL 参数,以便将其用于目录名称。
这不仅仅是删除 www
,还需要考虑对不在根目录的网页(例如:www.foo.com/bar
)运行的审计,因为斜杠对于目录名称来说是无效字符。
对于这些 URL,我们将再次用下划线替换无效字符。这样,如果您对 https://www.foo.com/bar
运行审计,则包含报告的结果目录名称将为 foo.com_bar.
。
为了更轻松地处理 URL,我们将使用一个名为 url 的原生 Node.js 模块。可以像任何其他包一样导入它,无需将其添加到 package.json
中并通过 npm 获取它。
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;
const url = require('url');
接下来,让我们使用它来实例化一个新的 URL 对象。
if (argv.url) {
const urlObj = new URL(argv.url);
launchChromeAndRunLighthouse(argv.url).then(results => {
console.log(results);
});
}
如果您要将 urlObj
打印到控制台,您将看到我们可以使用的大量有用的 URL 数据。
$ node lh.js --url https://www.foo.com/bar
URL {
href: 'https://www.foo.com/bar',
origin: 'https://www.foo.com',
protocol: 'https:',
username: '',
password: '',
host: 'www.foo.com',
hostname: 'www.foo.com',
port: '',
pathname: '/bar',
search: '',
searchParams: URLSearchParams {},
hash: ''
}
创建一个名为 dirName
的新变量,并使用字符串 replace()
方法在 URL 的 host 属性上,以删除 www
以及 https
协议
const urlObj = new URL(argv.url);
let dirName = urlObj.host.replace('www.','');
我们在这里使用了 let
,与 const
不同的是,它可以重新赋值,因为如果 URL 有 pathname,我们需要更新引用,用下划线替换斜杠。这可以通过正则表达式模式完成,如下所示
const urlObj = new URL(argv.url);
let dirName = urlObj.host.replace("www.", "");
if (urlObj.pathname !== "/") {
dirName = dirName + urlObj.pathname.replace(/\//g, "_");
}
现在我们可以创建目录本身。这可以通过使用另一个名为 fs(“文件系统”的缩写)的原生 Node.js 模块来完成。
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;
const url = require('url');
const fs = require('fs');
我们可以使用它的 mkdir()
方法来创建目录,但首先要使用它的 existsSync()
方法来检查目录是否已存在,否则 Node.js 会抛出错误
const urlObj = new URL(argv.url);
let dirName = urlObj.host.replace("www.", "");
if (urlObj.pathname !== "/") {
dirName = dirName + urlObj.pathname.replace(/\//g, "_");
}
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName);
}
在这一点上测试脚本应该会创建一个新目录。将 https://www.bbc.co.uk/news
作为 URL 参数传递将创建一个名为 bbc.co.uk_news
的目录。
保存报告
在 launchChromeAndRunLighthouse()
的 then
函数中,我们希望用写入报告到磁盘的逻辑替换现有的 console.log
。这可以使用 fs 模块的 writeFile()
方法来完成。
launchChromeAndRunLighthouse(argv.url).then(results => {
fs.writeFile("report.json", results, err => {
if (err) throw err;
});
});
第一个参数表示文件名,第二个是文件的内容,第三个是回调,包含在写入过程中出现错误时的错误对象。这将创建一个名为 report.json
的新文件,其中包含返回的 Lighthouse 报告 JSON 对象。
我们仍然需要将其发送到正确的目录,并使用时间戳作为其文件名。 前者很简单——我们传递之前创建的 dirName
变量,如下所示
launchChromeAndRunLighthouse(argv.url).then(results => {
fs.writeFile(`${dirName}/report.json`, results, err => {
if (err) throw err;
});
});
但后者要求我们以某种方式检索报告生成的时间戳。值得庆幸的是,报告本身捕获了此数据点,并将其存储为 fetchTime
属性。
我们只需要记住将任何冒号(:
)替换为下划线(_
),这样它就可以与 Windows 文件系统完美配合。
launchChromeAndRunLighthouse(argv.url).then(results => {
fs.writeFile(
`${dirName}/${results["fetchTime"].replace(/:/g, "_")}.json`,
results,
err => {
if (err) throw err;
}
);
});
如果您现在运行它,而不是 timestamped.json
文件名,您可能会看到类似于以下内容的错误
UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'replace' of undefined
这是因为 Lighthouse 当前以 JSON 格式返回报告,而不是 JavaScript 可以使用的对象。
值得庆幸的是,我们可以直接让 Lighthouse 返回一个常规的 JavaScript 对象,而不是自己解析 JSON。
这要求将以下行从
return chrome.kill().then(() => results.report);
…修改为
return chrome.kill().then(() => results.lhr);
现在,如果您重新运行脚本,文件将被正确命名。但是,当打开时,它的唯一内容不幸地是…
[object Object]
这是因为我们现在遇到了与之前相反的问题。我们正在尝试渲染一个 JavaScript 对象,而没有先将其字符串化为 JSON 对象。
解决方案很简单。为了避免在解析或字符串化这个巨大的对象上浪费资源,我们可以从 Lighthouse 返回两种类型
return lighthouse(url, opts).then(results => {
return chrome.kill().then(() => {
return {
js: results.lhr,
json: results.report
};
});
});
然后我们可以将 writeFile
实例修改为
fs.writeFile(
`${dirName}/${results.js["fetchTime"].replace(/:/g, "_")}.json`,
results.json,
err => {
if (err) throw err;
}
);
搞定!在 Lighthouse 审计完成后,我们的工具现在应该将报告保存到一个文件中,该文件使用唯一的带时间戳的文件名存储在以网站 URL 命名的目录中。
这意味着报告现在可以更有效地组织起来,并且无论保存了多少报告,它们都不会相互覆盖。
比较 Lighthouse 报告
在日常开发中,当我专注于提高性能时,能够非常快速地在控制台中直接比较报告并查看我是否朝着正确的方向前进将非常有用。考虑到这一点,这种比较功能的要求应该是
- 如果 Lighthouse 审计完成后,同一网站的先前报告已存在,则自动与之进行比较,并显示关键性能指标的任何更改。
- 我还应该能够比较来自任何两个网站的任何两个报告的关键性能指标,而无需生成可能不需要的新 Lighthouse 报告。
报告的哪些部分应该进行比较?这些是作为任何 Lighthouse 报告的一部分收集的数值关键性能指标。它们提供了对网站客观和感知性能的洞察。

此外,Lighthouse 还收集了其他指标,这些指标未列在此部分报告中,但仍然以适当的格式包含在比较中。这些是
- 第一个字节时间 - 第一个字节时间标识服务器发送响应的时间。
- 总阻塞时间 - 从 FCP 到交互时间的所有时间段的总和,其中任务长度超过 50 毫秒,以毫秒为单位表示。
- 估计的输入延迟 - 估计的输入延迟是对应用程序在页面加载最繁忙的 5 秒窗口内响应用户输入所需时间的估计,以毫秒为单位表示。如果您的延迟高于 50 毫秒,用户可能会认为您的应用程序很卡顿。
指标比较应该如何输出到控制台?我们将使用旧指标和新指标创建一个简单的基于百分比的比较,以查看它们从一个报告到另一个报告的更改情况。
为了便于快速扫描,我们还将根据指标是更快、更慢还是不变来对各个指标进行颜色编码。
我们将努力实现以下输出

将新报告与先前报告进行比较
让我们从在 launchChromeAndRunLighthouse()
函数下方创建一个名为 compareReports()
的新函数开始,该函数将包含所有比较逻辑。我们将为它提供两个参数——from
和 to
— 来接受用于比较的两个报告。
现在,作为占位符,我们将只将来自每个报告的一些数据打印到控制台,以验证它是否正确接收了它们。
const compareReports = (from, to) => {
console.log(from["finalUrl"] + " " + from["fetchTime"]);
console.log(to["finalUrl"] + " " + to["fetchTime"]);
};
由于这种比较将在创建新报告后开始,因此执行此函数的逻辑应该位于 launchChromeAndRunLighthouse()
的 then
函数中。
例如,如果您在目录中放置了 30 个报告,我们需要确定哪个是最新的报告,并将其设置为将与新报告进行比较的先前报告。值得庆幸的是,我们已经决定使用时间戳作为报告的文件名,因此这给了我们一些可以利用的东西。
首先,我们需要收集所有现有报告。为了简化此过程,我们将安装一个名为 glob 的新依赖项,该依赖项允许在搜索文件时进行模式匹配。这至关重要,因为我们无法预测将存在多少个报告或它们将被命名为什么。
像任何其他依赖项一样安装它
npm install glob --save-dev
然后像往常一样在文件顶部导入它
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const argv = require('yargs').argv;
const url = require('url');
const fs = require('fs');
const glob = require('glob');
我们将使用 glob
收集目录中的所有报告,我们已经通过 dirName
变量知道了目录的名称。将它的 sync
选项设置为 true
非常重要,因为我们不想让 JavaScript 执行继续,直到我们知道存在多少个其他报告。
launchChromeAndRunLighthouse(argv.url).then(results => {
const prevReports = glob(`${dirName}/*.json`, {
sync: true
});
// et al
});
此过程返回一个路径数组。因此,如果报告目录看起来像这样
- lukeharrison.dev
- 2020-01-31T10_18_12.648Z.json
- 2020-01-31T10_18_24.110Z.json
…那么结果数组将如下所示
[
'lukeharrison.dev/2020-01-31T10_18_12.648Z.json',
'lukeharrison.dev/2020-01-31T10_18_24.110Z.json'
]
因为只有在先前报告存在的情况下才能执行比较,所以让我们使用此数组作为比较逻辑的条件
const prevReports = glob(`${dirName}/*.json`, {
sync: true
});
if (prevReports.length) {
}
我们有一个报告文件路径列表,我们需要比较它们的时间戳文件名,以确定哪个是最新的。
这意味着我们首先需要收集所有文件名的列表,修剪任何不相关的数据(例如目录名称),并注意将下划线(_
)替换回冒号(:
),以将其重新转换为有效的日期。最简单的方法是使用 path
,另一个 Node.js 原生模块。
const path = require('path');
将路径作为参数传递给它的 parse
方法,如下所示
path.parse('lukeharrison.dev/2020-01-31T10_18_24.110Z.json');
返回此有用的对象
{
root: '',
dir: 'lukeharrison.dev',
base: '2020-01-31T10_18_24.110Z.json',
ext: '.json',
name: '2020-01-31T10_18_24.110Z'
}
因此,要获取所有时间戳文件名的列表,我们可以这样做
if (prevReports.length) {
dates = [];
for (report in prevReports) {
dates.push(
new Date(path.parse(prevReports[report]).name.replace(/_/g, ":"))
);
}
}
如果我们的目录看起来像这样
- lukeharrison.dev
- 2020-01-31T10_18_12.648Z.json
- 2020-01-31T10_18_24.110Z.json
将导致
[
'2020-01-31T10:18:12.648Z',
'2020-01-31T10:18:24.110Z'
]
日期的一个有用之处在于它们默认情况下是可比较的。
const alpha = new Date('2020-01-31');
const bravo = new Date('2020-02-15');
console.log(alpha > bravo); // false
console.log(bravo > alpha); // true
因此,通过使用 `reduce` 函数,我们可以将日期数组缩减到只剩下最新的日期。
dates = [];
for (report in prevReports) {
dates.push(new Date(path.parse(prevReports[report]).name.replace(/_/g, ":")));
}
const max = dates.reduce(function(a, b) {
return Math.max(a, b);
});
如果将 `max` 的内容打印到控制台,它会显示一个 UNIX 时间戳,因此现在,我们只需要添加另一行将最新的日期转换回正确的 ISO 格式。
const max = dates.reduce(function(a, b) {
return Math.max(a, b);
});
const recentReport = new Date(max).toISOString();
假设这些是报告列表:
- 2020-01-31T23_24_41.786Z.json
- 2020-01-31T23_25_36.827Z.json
- 2020-01-31T23_37_56.856Z.json
- 2020-01-31T23_39_20.459Z.json
- 2020-01-31T23_56_50.959Z.json
`recentReport` 的值为 `2020-01-31T23:56:50.959Z`。
现在我们知道了最新的报告,接下来需要提取其内容。在 `recentReport` 变量下方创建一个名为 `recentReportContents` 的新变量,并将其赋值为一个空函数。
由于我们知道此函数始终需要执行,而不是手动调用它,因此将其转换为 IFFE(立即调用函数表达式)是有意义的,该表达式将在 JavaScript 解析器到达它时自行运行。这由额外的括号表示。
const recentReportContents = (() => {
})();
在此函数中,我们可以使用原生 `fs` 模块的 `readFileSync()` 方法返回最新报告的内容。由于这将是 JSON 格式,因此将其解析为常规 JavaScript 对象很重要。
const recentReportContents = (() => {
const output = fs.readFileSync(
dirName + "/" + recentReport.replace(/:/g, "_") + ".json",
"utf8",
(err, results) => {
return results;
}
);
return JSON.parse(output);
})();
然后,只需调用 `compareReports()` 函数并传递当前报告和最新报告作为参数即可。
compareReports(recentReportContents, results.js);
目前这只是将一些详细信息打印到控制台,以便我们可以测试报告数据是否正常传递。
https://www.lukeharrison.dev/ 2020-02-01T00:25:06.918Z
https://www.lukeharrison.dev/ 2020-02-01T00:25:42.169Z
如果您在此处遇到任何错误,请尝试删除教程前面任何 `report.json` 文件或内容无效的报告。
比较任意两个报告
剩下的关键要求是能够比较来自任意两个网站的任意两个报告。实现这一点的最简单方法是允许用户将完整的报告文件路径作为命令行参数传递,然后我们将这些参数发送到 `compareReports()` 函数。
在命令行中,它看起来像这样
node lh.js --from lukeharrison.dev/2020-02-01T00:25:06.918Z --to cnn.com/2019-12-16T15:12:07.169Z
实现这一点需要编辑条件 `if` 语句,该语句检查是否存在 URL 命令行参数。我们将添加一个额外的检查,以查看用户是否只传递了 `from` 和 `to` 路径,否则检查 URL,就像以前一样。这样,我们将阻止新的 Lighthouse 审核。
if (argv.from && argv.to) {
} else if (argv.url) {
// et al
}
让我们提取这些 JSON 文件的内容,将它们解析为 JavaScript 对象,然后将它们传递给 `compareReports()` 函数。
在检索最新报告时,我们已经解析过 JSON。我们可以将此功能提取到自己的辅助函数中,并在两个位置使用它。
使用 `recentReportContents()` 函数作为基础,创建一个名为 `getContents()` 的新函数,该函数接受文件路径作为参数。确保这只是一个常规函数,而不是 IFFE,因为我们不希望它在 JavaScript 解析器找到它时立即执行。
const getContents = pathStr => {
const output = fs.readFileSync(pathStr, "utf8", (err, results) => {
return results;
});
return JSON.parse(output);
};
const compareReports = (from, to) => {
console.log(from["finalUrl"] + " " + from["fetchTime"]);
console.log(to["finalUrl"] + " " + to["fetchTime"]);
};
然后更新 `recentReportContents()` 函数以使用此提取的辅助函数。
const recentReportContents = getContents(dirName + '/' + recentReport.replace(/:/g, '_') + '.json');
回到我们的新条件中,我们需要将比较报告的内容传递给 `compareReports()` 函数。
if (argv.from && argv.to) {
compareReports(
getContents(argv.from + ".json"),
getContents(argv.to + ".json")
);
}
与以前一样,这应该在控制台中打印出有关报告的一些基本信息,让我们知道一切都正常工作。
node lh.js --from lukeharrison.dev/2020-01-31T23_24_41.786Z --to lukeharrison.dev/2020-02-01T11_16_25.221Z
将导致
https://www.lukeharrison.dev/ 2020-01-31T23_24_41.786Z
https://www.lukeharrison.dev/ 2020-02-01T11_16_25.221Z
比较逻辑
开发的这一部分涉及构建比较逻辑,以比较 `compareReports()` 函数接收的两个报告。
在 Lighthouse 返回的对象中,有一个名为 `audits` 的属性,该属性包含另一个对象,其中列出了性能指标、机会和信息。这里有很多信息,其中大部分对于此工具的目的来说并不重要。
以下是第一个内容绘制 (First Contentful Paint) 的条目,它是我们要比较的九个性能指标之一。
"first-contentful-paint": {
"id": "first-contentful-paint",
"title": "First Contentful Paint",
"description": "First Contentful Paint marks the time at which the first text or image is painted. [Learn more](https://webdev.ac.cn/first-contentful-paint).",
"score": 1,
"scoreDisplayMode": "numeric",
"numericValue": 1081.661,
"displayValue": "1.1 s"
}
创建一个包含这九个性能指标键的数组。我们可以使用它来过滤审核对象。
const compareReports = (from, to) => {
const metricFilter = [
"first-contentful-paint",
"first-meaningful-paint",
"speed-index",
"estimated-input-latency",
"total-blocking-time",
"max-potential-fid",
"time-to-first-byte",
"first-cpu-idle",
"interactive"
];
};
然后,我们将遍历其中一个报告的 `audits` 对象,然后将其名称与我们的过滤器列表进行交叉引用。(哪个审核对象并不重要,因为它们都具有相同的结构。)
如果它在里面,那么太好了,我们想使用它。
const metricFilter = [
"first-contentful-paint",
"first-meaningful-paint",
"speed-index",
"estimated-input-latency",
"total-blocking-time",
"max-potential-fid",
"time-to-first-byte",
"first-cpu-idle",
"interactive"
];
for (let auditObj in from["audits"]) {
if (metricFilter.includes(auditObj)) {
console.log(auditObj);
}
}
此 `console.log()` 将将以下键打印到控制台
first-contentful-paint
first-meaningful-paint
speed-index
estimated-input-latency
total-blocking-time
max-potential-fid
time-to-first-byte
first-cpu-idle
interactive
这意味着我们将分别在 `from['audits'][auditObj].numericValue` 和 `to['audits'][auditObj].numericValue` 中使用此循环来访问指标本身。
如果我们将这些内容与键一起打印到控制台,它将产生如下输出
first-contentful-paint 1081.661 890.774
first-meaningful-paint 1081.661 954.774
speed-index 15576.70313351777 1098.622294504341
estimated-input-latency 12.8 12.8
total-blocking-time 59 31.5
max-potential-fid 153 102
time-to-first-byte 16.859999999999985 16.096000000000004
first-cpu-idle 1704.8490000000002 1918.774
interactive 2266.2835 2374.3615
现在我们有了所有需要的数据。我们只需要计算这两个值之间的百分比差异,然后使用前面概述的颜色编码格式将其记录到控制台。
你知道如何计算两个值之间的百分比变化吗?我也不知道。谢天谢地,每个人最喜欢的巨石搜索引擎前来救援。
公式为
((From - To) / From) x 100
所以,假设第一个报告 (from) 的速度指数为 5.7 秒,然后第二个报告 (to) 的值为 2.1 秒。计算将是
5.7 - 2.1 = 3.6
3.6 / 5.7 = 0.63157895
0.63157895 * 100 = 63.157895
四舍五入到小数点后两位,速度指数下降了 63.16%。
让我们将此放入 `compareReports()` 函数中的辅助函数中,位于 `metricFilter` 数组下方。
const calcPercentageDiff = (from, to) => {
const per = ((to - from) / from) * 100;
return Math.round(per * 100) / 100;
};
回到我们的 `auditObj` 条件中,我们可以开始整理最终的报告比较输出。
首先,使用辅助函数为每个指标生成百分比差异。
for (let auditObj in from["audits"]) {
if (metricFilter.includes(auditObj)) {
const percentageDiff = calcPercentageDiff(
from["audits"][auditObj].numericValue,
to["audits"][auditObj].numericValue
);
}
}
接下来,我们需要以这种格式输出值到控制台

这需要向控制台输出添加颜色。在 Node.js 中,这可以通过 将颜色代码作为参数传递 给 `console.log()` 函数,如下所示
console.log('\x1b[36m', 'hello') // Would print 'hello' in cyan

您可以在此 Stackoverflow 问题中获得 颜色代码的完整参考。我们需要绿色和红色,因此分别为 `\x1b[32m` 和 `\x1b[31m`。对于值保持不变的指标,我们将只使用白色。这将是 `\x1b[37m`。
根据百分比增加是正数还是负数,需要发生以下事情
- 日志颜色需要更改(负数为绿色,正数为红色,不变为白色)
- 日志文本内容更改。
- ‘[Name] 的速度慢 X%’ 对于正数
- ‘[Name] 的速度快 X%’ 对于负数
- ‘[Name] 没有变化’ 对于没有百分比差异的数字。
- 如果数字为负数,我们希望删除减号/负号,否则,你将得到一个类似于 “速度指数快 -92.95%” 的句子,这毫无意义。
有很多方法可以做到这一点。在这里,我们将使用 `Math.sign()` 函数,如果其参数为正数,则返回 1,如果为 0,则返回 0,如果为负数,则返回 -1。这就足够了。
for (let auditObj in from["audits"]) {
if (metricFilter.includes(auditObj)) {
const percentageDiff = calcPercentageDiff(
from["audits"][auditObj].numericValue,
to["audits"][auditObj].numericValue
);
let logColor = "\x1b[37m";
const log = (() => {
if (Math.sign(percentageDiff) === 1) {
logColor = "\x1b[31m";
return `${percentageDiff + "%"} slower`;
} else if (Math.sign(percentageDiff) === 0) {
return "unchanged";
} else {
logColor = "\x1b[32m";
return `${percentageDiff + "%"} faster`;
}
})();
console.log(logColor, `${from["audits"][auditObj].title} is ${log}`);
}
}
所以,我们有了它。
您可以创建新的 Lighthouse 报告,如果存在以前的报告,则会进行比较。
您还可以比较来自任意两个网站的任意两个报告。
完整源代码
以下是该工具的完整源代码,您也可以通过以下链接在 Gist 中查看。
const lighthouse = require("lighthouse");
const chromeLauncher = require("chrome-launcher");
const argv = require("yargs").argv;
const url = require("url");
const fs = require("fs");
const glob = require("glob");
const path = require("path");
const launchChromeAndRunLighthouse = url => {
return chromeLauncher.launch().then(chrome => {
const opts = {
port: chrome.port
};
return lighthouse(url, opts).then(results => {
return chrome.kill().then(() => {
return {
js: results.lhr,
json: results.report
};
});
});
});
};
const getContents = pathStr => {
const output = fs.readFileSync(pathStr, "utf8", (err, results) => {
return results;
});
return JSON.parse(output);
};
const compareReports = (from, to) => {
const metricFilter = [
"first-contentful-paint",
"first-meaningful-paint",
"speed-index",
"estimated-input-latency",
"total-blocking-time",
"max-potential-fid",
"time-to-first-byte",
"first-cpu-idle",
"interactive"
];
const calcPercentageDiff = (from, to) => {
const per = ((to - from) / from) * 100;
return Math.round(per * 100) / 100;
};
for (let auditObj in from["audits"]) {
if (metricFilter.includes(auditObj)) {
const percentageDiff = calcPercentageDiff(
from["audits"][auditObj].numericValue,
to["audits"][auditObj].numericValue
);
let logColor = "\x1b[37m";
const log = (() => {
if (Math.sign(percentageDiff) === 1) {
logColor = "\x1b[31m";
return `${percentageDiff.toString().replace("-", "") + "%"} slower`;
} else if (Math.sign(percentageDiff) === 0) {
return "unchanged";
} else {
logColor = "\x1b[32m";
return `${percentageDiff.toString().replace("-", "") + "%"} faster`;
}
})();
console.log(logColor, `${from["audits"][auditObj].title} is ${log}`);
}
}
};
if (argv.from && argv.to) {
compareReports(
getContents(argv.from + ".json"),
getContents(argv.to + ".json")
);
} else if (argv.url) {
const urlObj = new URL(argv.url);
let dirName = urlObj.host.replace("www.", "");
if (urlObj.pathname !== "/") {
dirName = dirName + urlObj.pathname.replace(/\//g, "_");
}
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName);
}
launchChromeAndRunLighthouse(argv.url).then(results => {
const prevReports = glob(`${dirName}/*.json`, {
sync: true
});
if (prevReports.length) {
dates = [];
for (report in prevReports) {
dates.push(
new Date(path.parse(prevReports[report]).name.replace(/_/g, ":"))
);
}
const max = dates.reduce(function(a, b) {
return Math.max(a, b);
});
const recentReport = new Date(max).toISOString();
const recentReportContents = getContents(
dirName + "/" + recentReport.replace(/:/g, "_") + ".json"
);
compareReports(recentReportContents, results.js);
}
fs.writeFile(
`${dirName}/${results.js["fetchTime"].replace(/:/g, "_")}.json`,
results.json,
err => {
if (err) throw err;
}
);
});
} else {
throw "You haven't passed a URL to Lighthouse";
}
下一步
随着这个基本的 Google Lighthouse 工具的完成,有很多方法可以进一步开发它。例如
- 某种简单的在线仪表板,允许非技术用户运行 Lighthouse 审核并查看指标随时间的变化。让利益相关者支持网站性能可能很困难,因此,他们可以自己感兴趣的一些切实的东西可以激发他们的兴趣。
- 构建对性能预算的支持,因此,如果生成报告并且性能指标比预期慢,则该工具会输出有关如何改进它们的实用建议(或者称你为名字)。
祝您好运!
本教程很棒,可以根据您的需要进行调整。感谢您展示如何有效地使用 lighthouse。
很棒。谢谢! `npm init -y` 是使用默认值并避免敲击 enter 的好方法。
但这没那么有趣 :D
文章很棒,内容也很详细。你还能提供一些步骤,比如当网站需要凭据时该怎么办?另外,有没有可能不是新开一个 Chrome 窗口,而是直接在已打开的 Chrome 中打开一个新标签页,这样就可以完成身份验证?
非常棒,类似于我们在 Blnq Studio 上的做法。每次保存都会生成一个可视化 Lighthouse 报告,并添加相应的版本标签。
这个工具能在 WSL 下运行吗?
我还没用过,但如果它与 Linux/MacOS 上的 bash 相同,那肯定可以!
哇,非常感谢你分享这个教程。
我一直都在努力提升网站性能,但很难跟踪这些改进的进度。这个应用非常实用。
好文章,内容很详细,一步一步地讲解。
我想确认一下,你是否成功地构建了仪表盘?
你好,感谢你一直以来提供的精彩资源 :)
我尝试了这个教程,但使用网址 “https://www.bbc.co.uk/news” 似乎无法正常运行。
Chromium 窗口打开了,并且创建了 bbc.co.uk_news 目录,但 Lighthouse 似乎没有运行,也没有生成报告文件。
看起来这个网站需要进行 cookie 验证,这个过程会阻止 Lighthouse 在 node 打开的新 Chrome 窗口中运行吗?
抱歉,我之前的问题可能太着急了…
如果等待足够长的时间,一切都会正常工作。
只是使用网址 “https://www.bbc.co.uk/news” 比以前使用的任何网址都要花费更长的时间。
再次感谢你的教程以及你在 css-trick 上做的所有其他工作。
抱歉造成困扰…
你好,很棒的文章。我想问一下,在 Lighthouse 中如何传递 –extra-headers。请帮忙提供一些 –extra-headers 选项的示例。