Guida introduttiva a Headless Chrome

TL;DR

Chrome Headless è disponibile in Chrome 59. Si tratta di un modo per eseguire il browser Chrome in un ambiente headless. In sostanza, puoi eseguire Chrome senza Chrome. Offre tutte le funzionalità moderne della piattaforma web fornite da Chromium e dal motore di rendering Blink alla riga di comando.

Perché è utile?

Un browser headless è un ottimo strumento per i test automatici e gli ambienti server in cui non è necessaria una shell dell'interfaccia utente visibile. Ad esempio, potresti voler eseguire alcuni test su una pagina web reale, crearne un PDF o semplicemente controllare il modo in cui il browser esegue il rendering di un URL.

Avvio headless (interfaccia a riga di comando)

Il modo più semplice per iniziare a utilizzare la modalità headless è aprire il file binario di Chrome dalla riga di comando. Se hai installato Chrome 59 o versioni successive, avvia Chrome con il flag --headless:

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 deve indicare l'installazione di Chrome. La posizione esatta varierà da una piattaforma all'altra. Dato che sono su Mac, ho creato dei comodi alias per ogni versione di Chrome che ho installato.

Se utilizzi il canale stabile di Chrome e non riesci a scaricare la versione beta, ti consiglio di utilizzare 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"

Scarica Chrome Canary qui.

Funzionalità della riga di comando

In alcuni casi potrebbe non essere necessario scrivere in modo programmatico lo script di Chrome headless. Esistono alcuni flag della riga di comando utili per eseguire attività comuni.

Stampa del DOM

La bandiera --dump-dom stampa document.body.innerHTML sullo stdout:

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

Crea un PDF

Il flag --print-to-pdf crea un PDF della pagina:

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

Acquisizione di screenshot

Per acquisire uno screenshot di una pagina, utilizza il flag --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/

L'esecuzione con --screenshot produrrà un file denominato screenshot.png nella directory di lavoro corrente. Se stai cercando screenshot a pagina intera, la procedura è un po' più complessa. C'è un ottimo post del blog di David Schnurr che fa al caso tuo. Dai un'occhiata a Utilizzare Chrome headless come strumento automatico per gli screenshot .

Modalità REPL (loop di lettura, valutazione e stampa)

Il flag --repl esegue headless in una modalità in cui puoi valutare le espressioni JS nel browser, direttamente dalla riga di comando:

$ 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
$

Eseguire il debug di Chrome senza un'interfaccia utente del browser?

Quando esegui Chrome con --remote-debugging-port=9222, viene avviata un'istanza con il protocollo DevTools abilitato. Il protocollo viene utilizzato per comunicare con Chrome e gestire l'istanza del browser headless. È anche ciò che strumenti come Sublime, VS Code e Node utilizzano per eseguire il debugging remoto di un'applicazione. #synergy

Poiché non disponi dell'interfaccia utente del browser per visualizzare la pagina, passa a http://localhost:9222 in un altro browser per verificare che tutto funzioni. Vedrai un elenco di pagine ispezionabili in cui puoi fare clic per vedere il rendering di headless:

DevTools Remote
UI di debug remoto di DevTools

Da qui, puoi utilizzare le funzionalità di DevTools che conosci per ispezionare, eseguire il debug e modificare la pagina come faresti normalmente. Se utilizzi Headless in modo programmatico, questa pagina è anche un potente strumento di debug per visualizzare tutti i comandi non elaborati del protocollo DevTools che vengono trasmessi e comunicano con il browser.

Utilizzo programmatico (nodo)

Burattinaio

Puppeteer è una libreria Node sviluppata dal team di Chrome. Fornisce un'API di alto livello per controllare Chrome headless (o completo). È simile ad altre librerie di test automatici come Phantom e NightmareJS, ma funziona solo con le versioni più recenti di Chrome.

Tra le altre cose, Puppeteer può essere utilizzato per acquisire facilmente screenshot, creare PDF, navigare nelle pagine e recuperare informazioni su queste pagine. Ti consiglio la libreria se vuoi automatizzare rapidamente i test del browser. Nasconde le complessità del protocollo DevTools e si occupa di attività ridondanti come l'avvio di un'istanza di debug di Chrome.

Installalo:

npm i --save puppeteer

Esempio: stampa dell'user agent

const puppeteer = require('puppeteer');

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

Esempio: acquisisci uno screenshot della pagina

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();
})();

Consulta la documentazione di Puppeteer per scoprire di più sull'API completa.

La libreria CRI

chrome-remote-interface è una libreria di livello inferiore rispetto all'API Puppeteer. Lo consiglio se vuoi essere vicino al metallo e usare direttamente il protocollo DevTools.

Avvio di Chrome

chrome-remote-interface non avvia Chrome per te, quindi dovrai occupartene personalmente.

Nella sezione interfaccia a riga di comando, abbiamo avviato Chrome manualmente utilizzando --headless --remote-debugging-port=9222. Tuttavia, per automatizzare completamente i test, probabilmente vorrai avviare Chrome da un'applicazione.

Un modo è utilizzare 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) => {
  ...
});

Le cose si complicano se vuoi una soluzione portatile che funzioni su più piattaforme. Guarda questo percorso hardcoded per Chrome :(

Utilizzo di ChromeLauncher

Lighthouse è un meraviglioso strumento per testare la qualità delle tue app web. All'interno di Lighthouse è stato sviluppato un modulo affidabile per l'avvio di Chrome, che ora è stato estratto per l'utilizzo autonomo. Il modulo NPM chrome-launcher individua dove è installato Chrome, configura un'istanza di debug, avvia il browser e lo chiude al termine del programma. L'aspetto migliore è che funziona multipiattaforma grazie a Node.

Per impostazione predefinita, chrome-launcher tenterà di avviare Chrome Canary (se è installato), ma puoi modificare questa impostazione per selezionare manualmente la versione di Chrome da utilizzare. Per usarlo, installalo da npm:

npm i --save chrome-launcher

Esempio: utilizzo di chrome-launcher per avviare Headless

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();
});

L'esecuzione di questo script non fa molto, ma dovresti vedere un'istanza di Chrome che si avvia nel Task Manager e carica about:blank. Ricorda che non ci sarà alcuna interfaccia utente del browser. Siamo headless.

Per controllare il browser, abbiamo bisogno del protocollo DevTools.

Recupero delle informazioni sulla pagina

Installa la libreria:

npm i --save chrome-remote-interface
Esempi

Esempio: stampa dell'user agent

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

...

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

Il risultato sarà simile a questo: HeadlessChrome/60.0.3082.0

Esempio: controlla se il sito ha un file manifest dell'app web

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.
});

})();

Esempio: estrai il <title> della pagina utilizzando le API DOM.

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.
});

})();

Utilizzo di Selenium, WebDriver e ChromeDriver

Al momento, Selenium apre un'istanza completa di Chrome. In altre parole, si tratta di una soluzione automatica, ma non completamente headless. Tuttavia, Selenium può essere configurato per eseguire Chrome headless con un po' di lavoro. Ti consiglio di consultare la pagina Eseguire Selenium con Chrome headless se vuoi istruzioni complete su come configurare tutto autonomamente, ma di seguito ho inserito alcuni esempi per iniziare.

Utilizzo di ChromeDriver

ChromeDriver 2.32 utilizza Chrome 61 e funziona bene con Chrome headless.

Installa:

npm i --save-dev selenium-webdriver chromedriver

Esempio:

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();

Utilizzo di WebDriverIO

WebDriverIO è un'API di livello superiore basata su Selenium WebDriver.

Installa:

npm i --save-dev webdriverio chromedriver

Esempio: filtrare le funzionalità CSS su chromestatus.com

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();

})();

Ulteriori risorse

Ecco alcune risorse utili per iniziare:

Documenti

Strumenti

  • chrome-remote-interface: modulo Node che avvolge il protocollo DevTools
  • Lighthouse: strumento automatizzato per il test della qualità delle app web; fa un uso intensivo del protocollo
  • chrome-launcher: modulo Node per l'avvio di Chrome, pronto per l'automazione

Demo

  • "The Headless Web": l'ottimo post del blog di Paul Kinlan sull'uso di Headless con api.ai.

Domande frequenti

Devo avere il flag --disable-gpu?

Solo su Windows. Le altre piattaforme non lo richiedono più. Il flag --disable-gpu è una soluzione temporanea per alcuni bug. Questo flag non sarà necessario nelle versioni future di Chrome. Per ulteriori informazioni, visita la pagina crbug.com/737678.

Quindi ho ancora bisogno di Xvfb?

No. Chrome headless non utilizza una finestra, quindi non è più necessario un server di visualizzazione come Xvfb. Puoi eseguire senza problemi i tuoi test automatici.

Che cos'è Xvfb? Xvfb è un server di visualizzazione in memoria per sistemi simili a Unix che consente di eseguire applicazioni grafiche (come Chrome) senza un display fisico collegato. Molte persone utilizzano Xvfb per eseguire versioni precedenti di Chrome ed eseguire test "headless".

Come faccio a creare un container Docker che esegue Chrome headless?

Dai un'occhiata a lighthouse-ci. Ha un Dockerfile di esempio che utilizza node:8-slim come immagine di base, installa ed esegue Lighthouse su App Engine Flex.

Posso utilizzarlo con Selenium / WebDriver / ChromeDriver?

Sì. Consulta Utilizzare Selenium, WebDriver e ChromeDriver.

Qual è la relazione con PhantomJS?

Headless Chrome è simile a strumenti come PhantomJS. Entrambe possono essere utilizzate per test automatici in un ambiente headless. La differenza principale tra i due è che Phantom utilizza una versione precedente di WebKit come motore di rendering, mentre Headless Chrome utilizza la versione più recente di Blink.

Al momento, Phantom fornisce anche un'API di livello superiore rispetto al protocollo DevTools.

Dove posso segnalare i bug?

I bug relativi a Chrome headless sono disponibili su crbug.com.

Per i bug nel protocollo DevTools, inviali all'indirizzo github.com/ChromeDevTools/devtools-protocol.