Guida introduttiva a Headless Chrome

Eric Bidelman

TL;DR

Headless Chrome è disponibile in Chrome 59. È un modo per eseguire il browser Chrome in un ambiente headless. In pratica, eseguire Chrome senza Chrome! Porta nella riga di comando tutte le moderne funzionalità della piattaforma web fornite da Chromium e dal motore di rendering Blink.

Perché è utile?

Un browser headless è un ottimo strumento per i test automatizzati e gli ambienti server in cui non è necessaria una shell UI visibile. Ad esempio, potresti eseguire alcuni test su una pagina web reale, creare un PDF della pagina o semplicemente controllare il modo in cui il browser esegue il rendering di un URL.

Avvio senza testa (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 dovrebbe rimandare alla tua installazione di Chrome. La posizione esatta varia da piattaforma a piattaforma. Dato che sono su Mac, ho creato alias pratici 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à a riga di comando

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

Stampa del DOM

Il flag --dump-dom viene stampato da document.body.innerHTML a standard:

    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 lo 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. Per gli screenshot a pagina intera, le cose sono un po' più complesse. C'è un ottimo post del blog di David Schnurr che ti riguarda. Consulta l'articolo Utilizzare Chrome headless come strumento automatizzato per gli screenshot .

Modalità REPL (read-eval-print loop)

Il flag --repl esegue Headless in una modalità che consente di 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 una UI 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. Inoltre, consente di utilizzare strumenti come Sublime, VS Code e Node per il debug remoto di un'applicazione. #synergy

Poiché l'interfaccia utente del browser non è disponibile per vedere la pagina, vai 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 cosa viene visualizzato senza testa:

Remoto DevTools
UI di debug remoto di DevTools

Da qui, puoi utilizzare le familiari funzionalità DevTools per esaminare, 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 passano attraverso la rete e che comunicano con il browser.

Utilizzo programmatico (nodo)

Burattinaio

Puppeteer è una libreria di nodi 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 tra le pagine e recuperare le relative informazioni. Consiglio la libreria se vuoi automatizzare rapidamente i test del browser. Nasconde le complessità del protocollo DevTools e si occupa delle attività ridondanti come l'avvio di un'istanza di debug di Chrome.

Installala:

npm i --save puppeteer

Esempio: stampa lo user agent

const puppeteer = require('puppeteer');

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

Esempio: acquisizione di 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 Pupeteer per scoprire di più sull'API completa.

La libreria CRI

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

Avvio di Chrome

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

Nella sezione dell'interfaccia a riga di comando, abbiamo avviato Chrome manualmente utilizzando --headless --remote-debugging-port=9222. Tuttavia, per automatizzare completamente i test, probabilmente vorrai generare Chrome dalla tua 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) => {
  ...
});

Tuttavia, se si cerca una soluzione portabile che funzioni su più piattaforme, le cose si complicano. Osserva il percorso hardcoded di Chrome :(

Utilizzo di ChromeLauncher

Lighthouse è un ottimo strumento per verificare la qualità delle tue app web. In Lighthouse è stato sviluppato un modulo affidabile per il lancio di Chrome, che ora è stato estratto per l'uso autonomo. Il modulo chrome-launcher Gestione dei partner di rete individuerà la posizione in cui Chrome viene installato, configurerà un'istanza di debug, avvia il browser e lo termina al termine del programma. La parte 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 il Chrome da utilizzare. Per utilizzarlo, esegui prima l'installazione 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 richiede molto, ma dovresti vedere un'istanza di Chrome attivarsi nel Task Manager che ha caricato about:blank. Ricordate che non ci sarà alcuna UI del browser. Siamo senza testa.

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 lo 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 è simile a: HeadlessChrome/60.0.3082.0

Esempio: controlla se il sito ha un manifest per 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, è una soluzione automatizzata, ma non completamente headless. Tuttavia, Selenium può essere configurato per eseguire Chrome headless con un po' di lavoro. Ti consiglio Eseguire Selenium con Headless Chrome se vuoi ricevere le istruzioni complete su come eseguire la configurazione autonomamente, ma di seguito trovi alcuni esempi per iniziare.

Con 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 rispetto a 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: il modulo di nodi che esegue il wrapping del protocollo DevTools
  • Lighthouse: strumento automatizzato per testare la qualità dell'app web; fa un uso intensivo del protocollo
  • chrome-Avvio app: modulo nodo per il lancio di Chrome, pronto per l'automazione

Demo

  • "The Headless Web" - L'ottimo post del blog di Paul Kinlan sull'utilizzo di Headless con api.ai.

Domande frequenti

Devo utilizzare il flag --disable-gpu?

Solo su Windows. Non è più richiesto da altre piattaforme. Il flag --disable-gpu è una soluzione temporanea per alcuni bug. Non avrai bisogno di questo flag nelle versioni future di Chrome. Visita la pagina crbug.com/737678 per maggiori informazioni.

Mi serve ancora Xvfb?

No. Chrome headless non utilizza una finestra, quindi non è più necessario un server di visualizzazione come Xvfb. Se non lo fai, potrai eseguire facilmente i tuoi test automatici.

Che cos'è Xvfb? Xvfb è un server di visualizzazione in memoria per sistemi 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?

Guarda 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 utilizzare questa funzionalità con Selenium / WebDriver / ChromeDriver?

Sì. Consulta la sezione Utilizzo di Selenium, WebDriver e ChromeDriver.

Qual è la relazione con PhantomJS?

Chrome headless è simile a strumenti come PhantomJS. Entrambi possono essere utilizzati per i 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 Chrome senza testa utilizza l'ultima versione di Blink.

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

Dove posso segnalare i bug?

Per i bug relativi a Chrome senza testa, segnalali su crbug.com.

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