对于我的一些性能审计,我需要一份网页的精确副本,因为它是由我的客户基础设施提供的。在某些情况下,可能很难接触到实际的人工制品。所以从网络上获取它比较容易。
我发现用周围的一些工具来保存一个网站特别困难。curl 和wget 在处理SPA的时候会有麻烦。解析的JavaScript会获取新的资源。而且你需要一个浏览器上下文来记录每个请求和响应。
这就是为什么我决定使用一个带有puppeteer的无头Chrome实例来存储一个精确的副本。让我们看看这是如何工作的
环境#
我使用Node v9,只需要几个额外的包。puppeteer,版本为1.1.0。我还在使用fs-extra,版本为5.0。如果你想在一行中创建文件夹和文件,它有几个不错的快捷方式。
const puppeteer = require('puppeteer'); // v 1.1.0
const { URL } = require('url');
const fse = require('fs-extra'); // v 5.0.0
const path = require('path');
就这样吧!url 和path 包是来自核心。我需要这两个包来提取文件名,并创建一个适当的路径来存储我磁盘上的文件。
刮取网站#
下面是刮取和保存网站的完整代码。让它先沉淀一下,我之后会详细解释每一点。
async function start(urlToFetch) {
/* 1 */
const browser = await puppeteer.launch();
const page = await browser.newPage();
/* 2 */
page.on('response', async (response) => {
const url = new URL(response.url());
let filePath = path.resolve(`./output${url.pathname}`);
if (path.extname(url.pathname).trim() === '') {
filePath = `${filePath}/index.html`;
}
await fse.outputFile(filePath, await response.buffer());
});
/* 3 */
await page.goto(urlToFetch, {
waitUntil: 'networkidle2'
});
/* 4 */
setTimeout(async () => {
await browser.close();
}, 60000 * 4);
}
start('https://fettblog.eu');
让我们深入了解一下代码。
1.创建一个浏览器上下文#
我们要做的第一件事。启动浏览器!
const browser = await puppeteer.launch();
const page = await browser.newPage();
puppeteer.launch() 创建一个新的浏览器上下文。这就像从Dock或工具栏上启动你的浏览器。它启动了一个无头的Chromium实例,但你也可以指向你机器上的Chrome/Chromium浏览器。
浏览器启动后,我们用browser.newPage ,打开一个新标签。然后我们就准备好了!
2.记录所有的反应#
在我们导航到要搜刮的URL之前,我们需要告诉puppeteer如何处理我们浏览器标签中的所有响应。Puppeteer有一个用于此的事件接口。
page.on('response', async (response) => {
const url = new URL(response.url());
let filePath = path.resolve(`./output${url.pathname}`);
if (path.extname(url.pathname).trim() === '') {
filePath = `${filePath}/index.html`;
}
await fse.outputFile(filePath, await response.buffer());
});
对于我们页面上下文中的每个响应,我们执行一个回调。这个回调访问了几个属性,以在我们的硬盘上存储文件的精确拷贝。
- 来自
url包的URL类帮助我们访问响应的URL的一部分。我们利用pathname属性来获取不含主机名的URL,并通过path.resolve方法在本地磁盘上创建一个路径。 - 如果URL没有指定扩展名,我们将文件转化为目录,并添加一个
index.html文件。这就是静态网站生成器如何为你不能直接访问路由的服务器创建漂亮的URL。对我们来说也是如此。 response.buffer()包含响应中的所有内容,以正确的格式。我们把它存储为文本、图像、字体,任何需要的东西。
在导航到一个URL之前定义这个响应处理程序是很重要的。但是导航是我们的下一步。
3.导航到URL#
page.goto 方法是开始导航的正确工具。
await page.goto(urlToFetch, {
waitUntil: 'networkidle2'
});
很直接,但注意到我传递了一个配置对象,在那里我要求等待哪个事件。我把它设置为networkidle2 ,这意味着在过去500ms内没有超过2个开放的网络连接。其他选项是networkidle0 ,或事件load 和domcontentloaded 。最后一个事件反映了浏览器中的导航事件。由于一些SPA在load 之后开始执行,我更想监听网络连接。
在这个事件之后,异步函数调用解决了,我们又回到了我们的同步流程。
4.等待一下#
setTimeout(async () => {
await browser.close();
}, 60000 * 4);
为了结束执行和清理事情,我们需要用browser.close() 关闭浏览器窗口。在这个特殊的情况下,我等待了4分钟。原因是我抓取的这个特定的SPA有一些延迟的获取,我没能用networkidle 事件来记录。响应处理程序仍然是活动的。所以所有的响应都被记录下来。
底线#
这就是我所需要的获取我的客户的网络应用程序的副本。有一个真实的浏览器上下文是一个很大的帮助。然而,puppeteer 是更强大的。