专栏首页京程一灯在 Docker 中配置 Headless Chrome Node.js 服务器

在 Docker 中配置 Headless Chrome Node.js 服务器

每日前端夜话第269篇

翻译:疯狂的技术宅

作者:Tigran Bayburtsyan

来源:logrocket

正文共:1773 字

预计阅读时间:8分钟

随着开发过程中自动 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 来操纵整个网页,则用例仅受到你想象力的限制。

原文:https://blog.logrocket.com/how-to-set-up-a-headless-chrome-node-js-server-in-docker/

本文分享自微信公众号 - 前端先锋(jingchengyideng),作者:疯狂的技术宅

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-01-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 深入解析 Node.js 的 console.log[每日前端夜话0x73]

    当你开始用 JavaScript 进行开发时,可能学到的第一件事就是如何用 console.log 将内容记录到控制台。如果你去搜索如何调试 JavaScrip...

    疯狂的技术宅
  • 如何正确使用Node.js事件[每日前端夜话0x4B]

    事件驱动的编程变得流行之前,在程序内部进行通信的标准方法非常简单:如果一个组件想要向另外一个发送消息,只是显式地调用了那个组件上的方法。但是在 react 中用...

    疯狂的技术宅
  • 用机器学习加速你的网站

    我一生中大约73%的时间都在思考网络性能:如何在慢速手机上能播放60FPS的画面,用完美的顺序加载资源,通过离线缓存能做的一切。等等等等。

    疯狂的技术宅
  • 图文实践 RabbitMQ 不同类型交换机消息投递机制

    生产者发布消息、消费者接收消息,但是这中间的消息是怎么传递的,就用到了一个很重要的概念 交换机(Exchange),RabbitMQ 消息投递到交换机上之后,通...

    五月君
  • Spring Boot 2.0.0参考手册_中文版_Part IV_26

    Spring Boot所有的内部日志都采用Commons Logging,但开放了底层的日志实现。提供了对Java Util Logging,Log4J2和Lo...

    Tyan
  • 另类追踪之——被“策反”的安全机制

    Web安全一直是互联网用户非常关心的话题,无论是国际互联网组织还是浏览器厂商,都在尽力使用各种策略和限制来保障用户的信息安全。然而,这种好的出发点,却极可能被心...

    FB客服
  • 神级程序员教你如何写代码——十年编程内功心法

    写代码就是学一门语言然后开始撸代码吗?看完了我一系列文章的同学或者本身已经就是老鸟的同学显然不会这么认为。编程是一项非常严谨的工作!虽然我们自嘲为码农,但是这工...

    企鹅号小编
  • 移动互联网思维十大法则

    在信息相对封闭和资源相对稀缺的工业时代,机器思维下的“成功学”与“科学管理”大行其道。然而一夜之间,底特律宣告破产,诺基亚被收购……一批批巨头轰然倒下,三维世界...

    非著名程序员
  • 第十三章 iptables 防火墙(一)

    防火墙(firewall)一词本是建筑用于,本意是为了保护建筑物不受火灾侵害的。被借鉴到了在网络通信领域中,表示保护局域网或主机不受网络攻击的侵害。

    晓天
  • 块状链表

    的复杂度,而如果将整个块状链表维护成有序的,它甚至可以实现平衡树的一些操作[1],毕竟平衡树也可以看作是一种维护序列的方法。 又因为块状链表只在每个分块记录一...

    radaren

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动