如何在 Docker 中設置 Headless Chrome Node.js 服務器

做者:Tigran Bayburtsyanjavascript

翻譯:瘋狂的技術宅前端

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

未經容許嚴禁轉載node

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

隨着開發過程當中自動 UI 測試的興起,無頭瀏覽器已變得很是流行。網站爬蟲和基於 HTML 的內容分析也有無數的用例。react

在 99% 的場合下,你實際上不須要瀏覽器 GUI,由於它是徹底自動化的。運行 GUI 比發佈基於 Linux 的服務器或在微服務集羣(例如 Kubernetes)上擴展簡單的Docker容器的代價要高得多。linux

可是我跑題了。簡而言之,經過一個基於 Docker 容器的無頭瀏覽器來擁有最大的化靈活性和可擴展性變得愈來愈重要。在本教程中,咱們將演示如何建立 Dockerfile 以在 Node.js 中設置無頭 Chrome 瀏覽器。chrome

Headless Chrome 與 Node.js

Node.js 是 Google Chrome 開發團隊使用的主要環境,它擁有用於與 Chrome 通訊的原生集成庫:Puppeteer.js。該庫在 DevTools 接口上用 WebSocket 或基於系統管道的協議,能夠執行各類操做,例如截屏、測量頁面負載指標、鏈接速度和下載的內容大小等等。你能夠在不一樣的設備模擬中測試 UI 並用其截屏。最重要的是,Puppeteer 不須要 GUI。全部這些均可以在無頭模式下完成。docker

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 容器更加容易。express

Docker 容器中的 Google Chrome

根據上面的代碼,在容器內運行瀏覽器彷佛很簡單,但重要的是不要忽視安全性。默認狀況下,容器中的全部內容都以 root 用戶身份運行,瀏覽器會在本地執行 JavaScript 文件。json

固然,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 來操縱整個網頁,則用例僅受到你想象力的限制。

歡迎關注前端公衆號:前端先鋒,免費領取前端工程化實用工具包。

相關文章
相關標籤/搜索