開始使用 Headless Chrome

TL;DR

無頭 Chrome 已在 Chrome 59 版推出。這是在無頭環境中執行 Chrome 瀏覽器的方式。基本上,就是在沒有 Chrome 的情況下執行 Chrome!它可將 Chromium 和 Blink 轉譯引擎提供的所有現代化網站平台功能帶入命令列。

這有什麼好處?

無頭瀏覽器是自動化測試和伺服器環境的絕佳工具,因為您不需要可見的 UI 殼層。舉例來說,您可能想針對實際網頁執行部分測試、建立網頁的 PDF 檔案,或檢查瀏覽器如何轉譯網址。

啟動無頭 (CLI)

如要開始使用無頭模式,最簡單的方法就是透過指令列開啟 Chrome 二進位檔。如果您已安裝 Chrome 59 以上版本,請使用 --headless 標記啟動 Chrome:

chrome \
--headless \                   # Runs Chrome in headless mode.
--disable-gpu \                # Temporarily needed if running on Windows.
--remote-debugging-port=9222 \
https://www.chromestatus.com   # URL to open. Defaults to about:blank.

chrome 應指向您安裝的 Chrome。確切位置會因平台而異。由於我使用的是 Mac,因此我為已安裝的每個 Chrome 版本建立了方便的別名。

如果您使用的是 Chrome 穩定版,但無法取得 Beta 版,建議您使用 chrome-canary

alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
alias chrome-canary="/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary"
alias chromium="/Applications/Chromium.app/Contents/MacOS/Chromium"

請按這裡下載 Chrome Canary。

指令列功能

在某些情況下,您可能不需要以程式輔助方式編寫指令碼來啟用無頭 Chrome。您可以使用一些實用的指令列旗標執行常見工作。

列印 DOM

--dump-dom 標記會將 document.body.innerHTML 列印至 stdout:

    chrome --headless --disable-gpu --dump-dom https://www.chromestatus.com/

建立 PDF

--print-to-pdf 標記會建立網頁的 PDF:

chrome --headless --disable-gpu --print-to-pdf https://www.chromestatus.com/

擷取螢幕畫面

如要擷取網頁的螢幕截圖,請使用 --screenshot 標記:

chrome --headless --disable-gpu --screenshot https://www.chromestatus.com/

# Size of a standard letterhead.
chrome --headless --disable-gpu --screenshot --window-size=1280,1696 https://www.chromestatus.com/

# Nexus 5x
chrome --headless --disable-gpu --screenshot --window-size=412,732 https://www.chromestatus.com/

使用 --screenshot 執行時,會在目前工作目錄中產生名為 screenshot.png 的檔案。如果您想擷取整個網頁的螢幕截圖,情況就會複雜一些。David Schnurr 撰寫了一篇很棒的網誌文章,提供相關資訊。請參閱「使用無頭 Chrome 做為自動螢幕截圖工具 」。

REPL 模式 (讀取-求值-顯示迴圈)

--repl 標記會在無頭模式下執行 Headless,讓您可以直接透過指令列在瀏覽器中評估 JS 運算式:

$ chrome --headless --disable-gpu --repl --crash-dumps-dir=./tmp https://www.chromestatus.com/
[0608/112805.245285:INFO:headless_shell.cc(278)] Type a Javascript expression to evaluate or "quit" to exit.
>>> location.href
{"result":{"type":"string","value":"https://www.chromestatus.com/features"}}
>>> quit
$

在沒有瀏覽器 UI 的情況下偵錯 Chrome?

使用 --remote-debugging-port=9222 執行 Chrome 時,系統會啟動已啟用開發人員工具通訊協定的例項。這個通訊協定可用於與 Chrome 進行通訊,並驅動無頭瀏覽器執行個體。這也是 Sublime、VS Code 和 Node 等工具用於遠端偵錯應用程式時所使用的工具。#synergy

由於您沒有瀏覽器 UI 可查看網頁,請在其他瀏覽器中前往 http://localhost:9222,確認一切運作正常。您會看到可檢查的頁面清單,點選後即可查看 Headless 正在算繪的內容:

開發人員工具遠端存取
開發人員工具遠端偵錯 UI

從這裡開始,您可以使用熟悉的開發人員工具功能,按照平常方式檢查、偵錯及調整網頁。如果您以程式輔助方式使用無頭瀏覽器,這個頁面也是強大的除錯工具,可查看透過網路傳輸的所有原始 DevTools 通訊協定指令,並與瀏覽器進行通訊。

以程式輔助方式使用 (Node)

布偶操作員

Puppeteer 是 Chrome 團隊開發的 Node 程式庫。這個 API 提供高階 API,可控制無頭 (或完整) Chrome。這類似於 Phantom 和 NightmareJS 等其他自動化測試程式庫,但只能搭配最新版本的 Chrome 使用。

除了其他功能外,Puppeteer 還可用來輕鬆擷取螢幕截圖、建立 PDF、瀏覽網頁,以及擷取這些網頁的相關資訊。如果您想快速自動執行瀏覽器測試,建議您使用這個程式庫。它會隱藏開發人員工具通訊協定的複雜性,並處理啟動 Chrome 偵錯執行個體等多餘的工作。

安裝方式如下:

npm i --save puppeteer

範例 - 列印使用者代理程式

const puppeteer = require('puppeteer');

(async() => {
  const browser = await puppeteer.launch();
  console.log(await browser.version());
  await browser.close();
})();

範例:擷取網頁的螢幕截圖

const puppeteer = require('puppeteer');

(async() => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://www.chromestatus.com', {waitUntil: 'networkidle2'});
  await page.pdf({path: 'page.pdf', format: 'A4'});

  await browser.close();
})();

請參閱 Puppeteer 說明文件,進一步瞭解完整的 API。

CRI 程式庫

chrome-remote-interface 是比 Puppeteer API 更低階的程式庫。如果您想直接使用 DevTools 通訊協定,我建議您採用這種做法。

啟動 Chrome

chrome-remote-interface 不會為您啟動 Chrome,因此您必須自行處理。

在 CLI 部分,我們使用 --headless --remote-debugging-port=9222 手動啟動 Chrome。不過,如要完全自動化測試,您可能需要應用程式產生 Chrome。

其中一種方法是使用 child_process

const execFile = require('child_process').execFile;

function launchHeadlessChrome(url, callback) {
  // Assuming MacOSx.
  const CHROME = '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome';
  execFile(CHROME, ['--headless', '--disable-gpu', '--remote-debugging-port=9222', url], callback);
}

launchHeadlessChrome('https://www.chromestatus.com', (err, stdout, stderr) => {
  ...
});

不過,如果您需要跨平台使用的可攜式解決方案,情況就會變得複雜。只要看看 Chrome 的硬式編碼路徑就知道了:(

使用 ChromeLauncher

Lighthouse 是一項用於測試網路應用程式品質的絕佳工具。我們在 Lighthouse 中開發了可用於啟動 Chrome 的強大模組,現在已擷取該模組供獨立使用。chrome-launcher NPM 模組會尋找 Chrome 安裝的位置、設定偵錯例項、啟動瀏覽器,並在程式完成後終止瀏覽器。最棒的是,Node 讓它支援跨平台!

根據預設,chrome-launcher 會嘗試啟動 Chrome Canary (如果已安裝),但您可以變更這項設定,手動選取要使用的 Chrome。如要使用,請先從 npm 安裝:

npm i --save chrome-launcher

範例:使用 chrome-launcher 啟動無頭模式

const chromeLauncher = require('chrome-launcher');

// Optional: set logging level of launcher to see its output.
// Install it using: npm i --save lighthouse-logger
// const log = require('lighthouse-logger');
// log.setLevel('info');

/**
 * Launches a debugging instance of Chrome.
 * @param {boolean=} headless True (default) launches Chrome in headless mode.
 *     False launches a full version of Chrome.
 * @return {Promise<ChromeLauncher>}
 */
function launchChrome(headless=true) {
  return chromeLauncher.launch({
    // port: 9222, // Uncomment to force a specific port of your choice.
    chromeFlags: [
      '--window-size=412,732',
      '--disable-gpu',
      headless ? '--headless' : ''
    ]
  });
}

launchChrome().then(chrome => {
  console.log(`Chrome debuggable on port: ${chrome.port}`);
  ...
  // chrome.kill();
});

執行這個指令碼不會有太大影響,但您應該會在載入 about:blank 的任務管理員中看到 Chrome 執行個體。請注意,這項功能不會提供任何瀏覽器 UI。我們是無頭。

如要控制瀏覽器,我們需要 DevTools 通訊協定!

擷取頁面相關資訊

讓我們安裝程式庫:

npm i --save chrome-remote-interface
範例

範例 - 列印使用者代理程式

const CDP = require('chrome-remote-interface');

...

launchChrome().then(async chrome => {
  const version = await CDP.Version({port: chrome.port});
  console.log(version['User-Agent']);
});

結果類似以下:HeadlessChrome/60.0.3082.0

範例:檢查網站是否有網路應用程式資訊清單

const CDP = require('chrome-remote-interface');

...

(async function() {

const chrome = await launchChrome();
const protocol = await CDP({port: chrome.port});

// Extract the DevTools protocol domains we need and enable them.
// See API docs: https://chromedevtools.github.io/devtools-protocol/
const {Page} = protocol;
await Page.enable();

Page.navigate({url: 'https://www.chromestatus.com/'});

// Wait for window.onload before doing stuff.
Page.loadEventFired(async () => {
  const manifest = await Page.getAppManifest();

  if (manifest.url) {
    console.log('Manifest: ' + manifest.url);
    console.log(manifest.data);
  } else {
    console.log('Site has no app manifest');
  }

  protocol.close();
  chrome.kill(); // Kill Chrome.
});

})();

範例:使用 DOM API 擷取網頁的 <title>

const CDP = require('chrome-remote-interface');

...

(async function() {

const chrome = await launchChrome();
const protocol = await CDP({port: chrome.port});

// Extract the DevTools protocol domains we need and enable them.
// See API docs: https://chromedevtools.github.io/devtools-protocol/
const {Page, Runtime} = protocol;
await Promise.all([Page.enable(), Runtime.enable()]);

Page.navigate({url: 'https://www.chromestatus.com/'});

// Wait for window.onload before doing stuff.
Page.loadEventFired(async () => {
  const js = "document.querySelector('title').textContent";
  // Evaluate the JS expression in the page.
  const result = await Runtime.evaluate({expression: js});

  console.log('Title of page: ' + result.result.value);

  protocol.close();
  chrome.kill(); // Kill Chrome.
});

})();

使用 Selenium、WebDriver 和 ChromeDriver

目前,Selenium 會開啟完整的 Chrome 例項。換句話說,這是自動化解決方案,但並非完全無頭。不過,只要稍微調整一下,就能將 Selenium 設定為執行無頭 Chrome。如需完整的操作說明,請參閱使用無頭 Chrome 執行 Selenium,不過我已在下方提供一些範例,協助您開始使用。

使用 ChromeDriver

ChromeDriver 2.32 使用 Chrome 61,並與無頭 Chrome 搭配運作良好。

安裝:

npm i --save-dev selenium-webdriver chromedriver

範例:

const fs = require('fs');
const webdriver = require('selenium-webdriver');
const chromedriver = require('chromedriver');

const chromeCapabilities = webdriver.Capabilities.chrome();
chromeCapabilities.set('chromeOptions', {args: ['--headless']});

const driver = new webdriver.Builder()
  .forBrowser('chrome')
  .withCapabilities(chromeCapabilities)
  .build();

// Navigate to google.com, enter a search.
driver.get('https://www.google.com/');
driver.findElement({name: 'q'}).sendKeys('webdriver');
driver.findElement({name: 'btnG'}).click();
driver.wait(webdriver.until.titleIs('webdriver - Google Search'), 1000);

// Take screenshot of results page. Save to disk.
driver.takeScreenshot().then(base64png => {
  fs.writeFileSync('screenshot.png', new Buffer(base64png, 'base64'));
});

driver.quit();

使用 WebDriverIO

WebDriverIO 是 Selenium WebDriver 之上的高階 API。

安裝:

npm i --save-dev webdriverio chromedriver

範例:在 chromestatus.com 上篩選 CSS 功能

const webdriverio = require('webdriverio');
const chromedriver = require('chromedriver');

const PORT = 9515;

chromedriver.start([
  '--url-base=wd/hub',
  `--port=${PORT}`,
  '--verbose'
]);

(async () => {

const opts = {
  port: PORT,
  desiredCapabilities: {
    browserName: 'chrome',
    chromeOptions: {args: ['--headless']}
  }
};

const browser = webdriverio.remote(opts).init();

await browser.url('https://www.chromestatus.com/features');

const title = await browser.getTitle();
console.log(`Title: ${title}`);

await browser.waitForText('.num-features', 3000);
let numFeatures = await browser.getText('.num-features');
console.log(`Chrome has ${numFeatures} total features`);

await browser.setValue('input[type="search"]', 'CSS');
console.log('Filtering features...');
await browser.pause(1000);

numFeatures = await browser.getText('.num-features');
console.log(`Chrome has ${numFeatures} CSS features`);

const buffer = await browser.saveScreenshot('screenshot.png');
console.log('Saved screenshot...');

chromedriver.stop();
browser.end();

})();

其他資源

以下提供一些實用資源,可協助您快速上手:

文件

工具

  • chrome-remote-interface:用於包裝開發人員工具通訊協定的節點模組
  • Lighthouse:用於測試網頁應用程式品質的自動化工具,可大量使用本通訊協定
  • chrome-launcher:用於啟動 Chrome 的節點模組,可用於自動化

示範

  • 無頭網頁」:Paul Kinlan 的部落格文章,說明如何將 Headless 與 api.ai 搭配使用。

常見問題

是否需要 --disable-gpu 旗標?

僅限 Windows。其他平台不再需要這項資訊。--disable-gpu 標記是針對部分錯誤的暫時解決方法。在日後的 Chrome 版本中,您就不需要這個標記。詳情請參閱 crbug.com/737678

那麼我還是需要 Xvfb 嗎?

否。無頭 Chrome 不會使用視窗,因此您不再需要 Xvfb 這類顯示伺服器。您可以放心執行自動化測試。

什麼是 Xvfb?Xvfb 是類 Unix 系統的記憶體內顯示伺服器,可讓您在不連接實體螢幕的情況下,執行圖形應用程式 (例如 Chrome)。許多人會使用 Xvfb 執行舊版 Chrome,以進行「無頭」測試。

如何建立可執行無頭 Chrome 的 Docker 容器?

請查看 lighthouse-ci。這個範例包含Dockerfile,會使用 node:8-slim 做為基礎映像檔,並在 App Engine Flex 上安裝 + 執行 Lighthouse

我可以將這個 API 與 Selenium / WebDriver / ChromeDriver 搭配使用嗎?

可以。請參閱「使用 Selenium、WebDriver 和 ChromeDriver」一文。

這與 PhantomJS 有何關聯?

Headless Chrome 與 PhantomJS 等工具類似。這兩種方法都能用於無頭環境中的自動化測試。兩者的主要差異在於,Phantom 使用舊版 WebKit 做為轉譯引擎,而無頭 Chrome 則使用最新版的 Blink。

目前,Phantom 也提供比 DevTools 通訊協定更高階的 API。

如何回報錯誤?

如果是針對 Headless Chrome 的錯誤,請在 crbug.com 提交。

如果是開發人員工具通訊協定中的錯誤,請前往 github.com/ChromeDevTools/devtools-protocol 提交。