阅读 133

如何在 Docker 中设置 Headless Chrome Node.js 服务器

作者:Tigran Bayburtsyan

翻译:疯狂的技术宅

原文:blog.logrocket.com/how-to-set-…

未经允许严禁转载

How to Set Up a Headless Chrome Node.js Server in Docker

随着开发过程中自动 UI 测试的兴起,无头浏览器已变得非常流行。网站爬虫和基于 HTML 的内容分析也有无数的用例。

在 99% 的场合下,你实际上不需要浏览器 GUI,因为它是完全自动化的。运行 GUI 比发布基于 Linux 的服务器或在微服务集群(例如 Kubernetes)上扩展简单的Docker容器的代价要高得多。

但是我跑题了。简而言之,通过一个基于 Docker 容器的无头浏览器来拥有最大的化灵活性和可扩展性变得越来越重要。在本教程中,我们将演示如何创建 Dockerfile 以在 Node.js 中设置无头 Chrome 浏览器。

Headless Chrome 与 Node.js

Node.js 是 Google Chrome 开发团队使用的主要环境,它拥有用于与 Chrome 通信的原生集成库:Puppeteer.js。该库在 DevTools 接口上用 WebSocket 或基于系统管道的协议,可以执行各种操作,例如截屏、测量页面负载指标、连接速度和下载的内容大小等等。你可以在不同的设备模拟中测试 UI 并用其截屏。最重要的是,Puppeteer 不需要 GUI。所有这些都可以在无头模式下完成。

const puppeteer = require('puppeteer');
const fs = require('fs');

Screenshot('https://google.com');

async function Screenshot(url) {
   const browser = await puppeteer.launch({
       headless: true,
       args: [
       "--no-sandbox",
       "--disable-gpu",
       ]
   });

    const page = await browser.newPage();
    await page.goto(url, {
      timeout: 0,
      waitUntil: 'networkidle0',
    });
    const screenData = await page.screenshot({encoding: 'binary', type: 'jpeg', quality: 30});
    fs.writeFileSync('screenshot.jpg', screenData);

    await page.close();
    await browser.close();
}
复制代码

上面是用于在 Headless Chrome 上截图的简单可执行代码。请注意,我们未指定 Google Chrome 浏览器的可执行路径,因为 Puppeteer 的 NPM 模块内置了 Headless Chrome 版本。 Chrome 的开发团队不仅使库用起来很简单,而且在最小化设置方面做得非常好。这也使我们把代码嵌入 Docker 容器更加容易。

Docker 容器中的 Google Chrome

根据上面的代码,在容器内运行浏览器似乎很简单,但重要的是不要忽视安全性。默认情况下,容器中的所有内容都以 root 用户身份运行,浏览器会在本地执行 JavaScript 文件。

当然,Google Chrome 是安全的,它不允许用户从基于浏览器的脚本访问本地文件,但仍然存在潜在的安全风险。你可以通过创建新用户来执行浏览器本身的特定操作来最大大地降低这些风险。 Google 默认还启用了沙箱模式,该模式限制了外部脚本访问本地环境。

以下是负责 Google Chrome 设置的 Dockerfile 例子。我们将选择 Alpine Linux 作为基本容器,因为用它生成的 Docker 镜像占用的空间最小。

FROM alpine:3.6

RUN apk update && apk add --no-cache nmap && \
    echo @edge http://nl.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories && \
    echo @edge http://nl.alpinelinux.org/alpine/edge/main >> /etc/apk/repositories && \
    apk update && \
    apk add --no-cache \
      chromium \
      harfbuzz \
      "freetype>2.8" \
      ttf-freefont \
      nss

ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true

....
....
复制代码

run 命令处理用于获取 Chromium for Linux 的边缘存储库以及在 Alpine 上运行 chrome 所需的库。棘手的部分是要确保不会下载 Puppeteer 内嵌的 Chrome。这对于我们的容器镜像来说会白白的占用空间,这就是为什么我们要保留 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = true 这个环境变量的原因。

运行 Docker 构建后,我们会获得 Chromium 可执行文件:/usr/bin/chromium-browser。这是 Puppeteer Chrome 可执行文件的路径。

现在,让我们跳到 JavaScript 代码并完成一个 Dockerfile。

结合 Node.js 服务器和 Chromium 容器

在继续之前,我们需要修改一些代码,因为要作为微服务来获取给定网站的屏幕截图。为此,我们将用 Express.js 作为基本的 HTTP 服务器。

// server.js
const express = require('express');
const puppeteer = require('puppeteer');

const app = express();

// /?url=https://google.com
app.get('/', (req, res) => {
    const {url} = req.query;
    if (!url || url.length === 0) {
        return res.json({error: 'url query parameter is required'});
    }

    const imageData = await Screenshot(url);

    res.set('Content-Type', 'image/jpeg');
    res.set('Content-Length', imageData.length);
    res.send(imageData);
});

app.listen(process.env.PORT || 3000);

async function Screenshot(url) {
   const browser = await puppeteer.launch({
       headless: true,
       executablePath: '/usr/bin/chromium-browser',
       args: [
       "--no-sandbox",
       "--disable-gpu",
       ]
   });

    const page = await browser.newPage();
    await page.goto(url, {
      timeout: 0,
      waitUntil: 'networkidle0',
    });
    const screenData = await page.screenshot({encoding: 'binary', type: 'jpeg', quality: 30});

    await page.close();
    await browser.close();

    // Binary data of an image
    return screenData;
}
复制代码

这是完成 Dockerfile 的最后一步。运行 docker build -t headless:node后,我们将得到一个带有 Node.js 服务的镜像和一个 Headless Chrome 浏览器,用于截取屏幕截图。

截屏很有趣,但是还有许多其他的使用案例。幸运的是,上述过程几乎适用于所有案例。在大多数情况下,只需要对 Node.js 代码进行较小的更改。其余的是非常标准的环境设置。

Headless Chrome 的常见问题

Google Chrome 在执行时会占用大量内存,因此 Headless Chrome 在服务器端产生相同的情况也就不足为奇了。如果使同一浏览器打开多个实例,则服务最终将崩溃。

最好的解决方案是遵循同一种连接、同一种浏览器实例的原则。尽管这比多个浏览器管理多个页面的成本更高,但仅保留一个浏览器和一个页面会使你的系统更稳定。当然这取决于个人喜好和你特定的用例。根据独特的需求和目标,你也许可以找到最佳的权衡点。

以性能监控工具 Hexometer 的官方网站为例。该环境包括一个远程浏览器服务,其中包含几百个空闲浏览器池。它们用于在需要执行时通过 WebSocket 打开新连接,但严格遵循一个浏览器一个页面的原则。这使之成为一种稳定而有效的方法,不仅可以使运行中的浏览器保持空闲状态,而且还能使它们保持活动状态。

通过 WebSocket 进行伪造的连接非常稳定,你可以通过自定义服务(例如 browserless.io)来做类似的事情(也有开源版本)。

...
...

const browser = await puppeteer.launch({
    browserWSEndpoint: `ws://repo.treescale.com:6799`,
});

...
...
复制代码

这将使用相同的浏览器管理协议连接到 headless Chrome DevTools 套接字。

结论

在容器内运行浏览器可提供很多灵活性和可伸缩性。它也比传统的基于 VM 的实例便宜很多。现在,我们只需使用容器服务(例如 AWS Fargate 或 Google Cloud Run)就可以在需要时触发容器执行,并在一秒钟内扩展到数千个实例。

最常见的用例仍是使用 Jest和 UI automated tests。但是如果你认为可以在容器中用 Node.js 来操纵整个网页,则用例仅受到你想象力的限制。

欢迎关注前端公众号:前端先锋,免费领取前端工程化实用工具包。

关注下面的标签,发现更多相似文章
评论