Premiers pas avec Headless Chrome

Résumé

Headless Chrome sera disponible dans Chrome 59. Il permet d'exécuter le navigateur Chrome dans un environnement headless. En gros, exécuter Chrome sans Chrome ! Il intègre toutes les fonctionnalités modernes de la plate-forme Web fournies par Chromium et le moteur de rendu Blink à la ligne de commande.

En quoi est-ce utile ?

Un navigateur sans interface graphique est un excellent outil pour les tests automatisés et les environnements de serveur où vous n'avez pas besoin d'une interface utilisateur visible. Par exemple, vous pouvez effectuer des tests sur une vraie page Web, en créer un fichier PDF ou simplement inspecter la façon dont le navigateur affiche une URL.

Démarrage sans interface graphique (CLI)

Pour commencer à utiliser le mode headless, le plus simple est d'ouvrir le binaire Chrome à partir de la ligne de commande. Si vous avez installé Chrome 59 ou une version ultérieure, démarrez Chrome avec l'indicateur --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 doit renvoyer vers votre installation de Chrome. L'emplacement exact varie d'une plateforme à l'autre. Sous Mac, j'ai créé des alias pratiques pour chaque version de Chrome installée.

Si vous utilisez la version stable de Chrome et que vous ne pouvez pas obtenir la version bêta, nous vous recommandons d'utiliser 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"

Téléchargez Chrome Canary ici.

Fonctionnalités de la ligne de commande

Dans certains cas, vous n'aurez peut-être pas besoin d'écrire un script pour Chrome sans interface graphique. Certains indicateurs de ligne de commande utiles permettent d'effectuer des tâches courantes.

Imprimer le DOM

L'indicateur --dump-dom imprime document.body.innerHTML sur stdout:

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

Créer un PDF

L'indicateur --print-to-pdf crée un PDF de la page:

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

Effectuer des captures d'écran

Pour effectuer une capture d'écran d'une page, utilisez l'indicateur --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'exécution avec --screenshot génère un fichier nommé screenshot.png dans le répertoire de travail actuel. Si vous cherchez des captures d'écran en pleine page, c'est un peu plus complexe. Il y a un excellent article de blog de David Schnurr pour en savoir plus. Consultez Utiliser Headless Chrome comme outil automatisé de capture d'écran .

Mode REPL (boucle lecture-évaluation-impression)

L'option --repl exécute Headless dans un mode permettant d'évaluer les expressions JavaScript dans le navigateur, directement à partir de la ligne de commande:

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

Déboguer Chrome sans interface de navigateur ?

Lorsque vous exécutez Chrome avec --remote-debugging-port=9222, il démarre une instance sur laquelle le protocole DevTools est activé. Ce protocole permet de communiquer avec Chrome et de piloter l'instance de navigateur sans interface graphique. C'est également grâce à ces outils que Sublime, VS Code et Node sont utilisés pour le débogage à distance d'une application. #synergy

Comme vous n'avez pas d'interface utilisateur pour voir la page, accédez à http://localhost:9222 dans un autre navigateur pour vérifier que tout fonctionne correctement. Vous verrez une liste de pages Inspectable, sur lesquelles vous pouvez cliquer pour voir ce que le mode headless génère:

Outils de développement distants
UI de débogage à distance des outils de développement

Vous pouvez alors utiliser les fonctionnalités familières des outils de développement pour inspecter, déboguer et modifier la page comme vous le feriez normalement. Si vous utilisez la programmation sans interface graphique, cette page est également un outil de débogage puissant qui vous permet de voir toutes les commandes brutes du protocole DevTools en transit sur le réseau et en communiquant avec le navigateur.

Utiliser de façon automatisée (nœud)

Marionnettiste

Puppeteer est une bibliothèque Node développée par l'équipe Chrome. Il fournit une API de haut niveau pour contrôler Chrome headless (ou complet). Elle est semblable à d'autres bibliothèques de tests automatisés telles que Phantom et NightmareJS, mais elle ne fonctionne qu'avec les dernières versions de Chrome.

Puppeteer permet, entre autres, de faire des captures d'écran, de créer des PDF, de parcourir des pages et d'extraire des informations sur ces pages facilement. Je vous recommande d'utiliser cette bibliothèque si vous souhaitez automatiser rapidement les tests des navigateurs. Il masque les complexités du protocole des outils de développement et se charge des tâches redondantes, comme le lancement d'une instance de débogage de Chrome.

Installez-la:

npm i --save puppeteer

Exemple : imprimer le user-agent

const puppeteer = require('puppeteer');

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

Exemple : Faire une capture d'écran de la page

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

Consultez la documentation de Puppeteer pour en savoir plus sur l'API complète.

La bibliothèque CRI

chrome-remote-interface est une bibliothèque de niveau inférieur à celle de l'API Puppeteer. Je vous le recommande si vous souhaitez vous rapprocher du métal et utiliser directement le protocole DevTools.

Lancer Chrome

chrome-remote-interface ne lance pas Chrome pour vous. Vous devez donc vous en occuper vous-même.

Dans la section "CLI", nous avons démarré Chrome manuellement à l'aide de --headless --remote-debugging-port=9222. Toutefois, pour automatiser entièrement les tests, vous souhaiterez probablement générer Chrome à partir de votre application.

Vous pouvez utiliser 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) => {
  ...
});

Toutefois, les choses se compliquent si vous recherchez une solution portable compatible avec plusieurs plates-formes. Regardez simplement le chemin d'accès codé en dur vers Chrome :(

Utilisation de ChromeLauncher

Lighthouse est un outil merveilleux pour tester la qualité de vos applications Web. Un module robuste de lancement de Chrome a été développé dans Lighthouse et est désormais extrait pour une utilisation autonome. Le module NPM chrome-launcher détermine où Chrome est installé, configure une instance de débogage, lance le navigateur et le ferme une fois le programme terminé. Cerise sur le gâteau : elle fonctionne sur plusieurs plates-formes grâce à Node.js.

Par défaut, chrome-launcher tente de lancer Chrome Canary (s'il est installé), mais vous pouvez modifier cela pour sélectionner manuellement Chrome à utiliser. Pour l'utiliser, commencez par l'installer à partir de npm:

npm i --save chrome-launcher

Exemple : Utilisation de chrome-launcher pour lancer sans interface graphique

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'exécution de ce script ne fait pas grand-chose, mais vous devriez voir une instance de Chrome se lancer dans le gestionnaire de tâches qui a chargé about:blank. N'oubliez pas qu'il n'y aura aucune UI de navigateur. Nous n'avons pas de tête.

Pour contrôler le navigateur, nous avons besoin du protocole DevTools.

Récupérer des informations sur la page

Installons la bibliothèque:

npm i --save chrome-remote-interface
Exemples

Exemple : imprimer le 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']);
});

Vous obtenez un résultat semblable au suivant: HeadlessChrome/60.0.3082.0

Exemple : Vérifier si le site dispose d'un fichier manifeste d'application 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.
});

})();

Exemple : Extrayez le <title> de la page à l'aide des 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.
});

})();

Utiliser Selenium, WebDriver et ChromeDriver

Pour le moment, Selenium ouvre une instance complète de Chrome. En d'autres termes, il s'agit d'une solution automatisée, mais pas complètement sans interface graphique. Cependant, Selenium peut être configuré pour exécuter Chrome sans interface graphique avec un minimum d'efforts. Je vous recommande d'exécuter Selenium avec Headless Chrome si vous souhaitez obtenir des instructions complètes sur la configuration. Vous trouverez ci-dessous quelques exemples pour vous aider à démarrer.

Utiliser ChromeDriver

ChromeDriver 2.32 utilise Chrome 61 et fonctionne bien avec Headless Chrome.

Installer :

npm i --save-dev selenium-webdriver chromedriver

Exemple :

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

Utiliser WebDriverIO

WebDriverIO est une API de niveau supérieur qui vient compléter Selenium WebDriver.

Installer :

npm i --save-dev webdriverio chromedriver

Exemple: Filtrer les fonctionnalités CSS sur 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();

})();

Autres ressources

Voici quelques ressources utiles pour vous lancer:

Fichiers Docs

Outils

  • chrome-remote-interface : module de nœud qui encapsule le protocole DevTools
  • Lighthouse : outil automatisé permettant de tester la qualité des applications Web ; fait une utilisation intensive du protocole
  • chrome-launcher : module de nœud pour lancer Chrome, prêt pour l'automatisation

Démonstrations

  • "The Headless Web" : l'excellent article de blog de Paul Kinlan sur l'utilisation de headless avec api.ai.

Questions fréquentes

Ai-je besoin de l'option --disable-gpu ?

Uniquement sous Windows. Les autres plates-formes n'en ont plus besoin. L'option --disable-gpu est une solution temporaire permettant de contourner quelques bugs. Vous n'aurez plus besoin de cet indicateur dans les futures versions de Chrome. Pour en savoir plus, consultez crbug.com/737678.

J'ai toujours besoin de Xvfb ?

Non. Chrome sans interface graphique n'utilise pas de fenêtre. Un serveur d'affichage tel que Xvfb n'est donc plus nécessaire. Sans cela, vous pourrez exécuter vos tests automatisés sans problème.

Qu'est-ce que Xvfb ? Xvfb est un serveur d'affichage en mémoire pour les systèmes de type Unix qui vous permet d'exécuter des applications graphiques (comme Chrome) sans écran physique connecté. De nombreux utilisateurs utilisent Xvfb pour exécuter des versions antérieures de Chrome afin d'effectuer des tests "headless".

Comment créer un conteneur Docker qui exécute Headless Chrome ?

Regardez lighthouse-ci. Il contient un exemple de fichier Dockerfile qui utilise node:8-slim comme image de base, installe et exécute Lighthouse sur l'environnement flexible App Engine.

Puis-je l'utiliser avec Selenium / WebDriver / ChromeDriver ?

Oui. Consultez la page Utiliser Selenium, WebDriver et ChromeDriver.

Quel est le lien avec PhantomJS ?

Headless Chrome est semblable à des outils tels que PhantomJS. Les deux peuvent être utilisés pour des tests automatisés dans un environnement sans interface graphique. La principale différence entre les deux est que Phantom utilise une ancienne version de WebKit comme moteur de rendu, tandis que Headless Chrome utilise la dernière version de Blink.

Pour le moment, Phantom fournit une API de niveau supérieur à celle du protocole DevTools.

Où puis-je signaler des bugs ?

Si vous rencontrez un bug lié à Chrome sans interface graphique, signalez-le sur crbug.com.

Pour les bugs du protocole des outils de développement, signalez-les à l'adresse github.com/ChromeDevTools/devtools-protocol.