Maîtriser le Développement d'Extensions de Navigateur : De l'Idée à la Publication
Maîtriser le Développement d'Extensions de Navigateur : De l'Idée à la Publication

Scripts d'Arrière-Plan et Scripts de Contenu : Le Cœur Interactif de Votre Extension

Dans le cadre de notre cours "Maîtriser le Développement d'Extensions de Navigateur : De l'Idée à la Publication", nous avons déjà exploré la structure de base d'une extension et son fichier manifeste. Aujourd'hui, nous plongeons au cœur de l'interactivité : les scripts d'arrière-plan et les scripts de contenu. Ces deux types de scripts sont les moteurs qui animent votre extension, lui permettant d'interagir avec les pages web, de gérer des événements et de communiquer avec le monde extérieur.

Comprendre leur rôle, leurs limitations et leurs mécanismes de communication est fondamental pour bâtir des extensions puissantes et réactives. Préparez-vous à démystifier la manière dont votre extension prend vie dans le navigateur.

Comprendre l'Architecture des Extensions

Avant d'aborder les scripts, il est crucial de visualiser l'architecture d'une extension. Une extension n'est pas une simple page web ; c'est un ensemble de composants qui travaillent de concert. Imaginez votre extension comme une petite application avec sa propre logique, sa propre interface (par exemple, un popup) et ses propres interactions avec les pages web.

Au centre de cette architecture se trouvent deux acteurs majeurs :

  • Les Scripts d'Arrière-Plan (Background Scripts / Service Workers) : Le cerveau de l'extension. Ils gèrent la logique globale, les événements du navigateur et la communication avec les API de l'extension.
  • Les Scripts de Contenu (Content Scripts) : Les yeux et les mains de l'extension. Injectés directement dans les pages web, ils peuvent lire, modifier et interagir avec le contenu de ces pages.

Les Scripts d'Arrière-Plan (Background Scripts / Service Workers)

Les scripts d'arrière-plan sont la pierre angulaire de la logique de votre extension. Ils opèrent dans un environnement séparé du contenu des pages web et ont accès à l'ensemble des API d'extension du navigateur (par exemple, chrome.tabs, chrome.storage, chrome.alarms).

Rôle et Fonctionnalités

Le script d'arrière-plan est idéal pour :

  • Écouter les événements du navigateur : Par exemple, la navigation vers une nouvelle page, la création d'un nouvel onglet, la mise à jour d'un onglet.
  • Gérer l'état de l'extension : Sauvegarder des préférences utilisateur, des données.
  • Effectuer des tâches de longue haleine : Requêtes réseau, opérations complexes qui ne nécessitent pas d'interface utilisateur directe.
  • Servir de coordinateur : Relayer des messages entre différentes parties de l'extension (scripts de contenu, popup, options).

Manifest V2 vs V3 : Une Évolution Majeure

Il est important de noter l'évolution des scripts d'arrière-plan entre Manifest V2 et Manifest V3 :

  • Manifest V2 : Les scripts d'arrière-plan étaient souvent des pages persistantes (background.html avec un script) ou non persistantes (se chargeant et se déchargeant).
  • Manifest V3 : Le modèle a migré vers les Service Workers. Les Service Workers sont des scripts non persistants qui sont activés uniquement lorsque des événements se produisent ou lorsque l'extension a besoin d'eux. Cela améliore les performances et réduit la consommation de ressources, mais impose certaines contraintes (par exemple, pas d'accès direct au DOM de l'extension, gestion de l'état global différente).

Dans le contexte moderne, nous nous concentrons sur les Service Workers pour Manifest V3.

Exemple : Un Service Worker Simple

Imaginons un Service Worker qui détecte l'ouverture d'un nouvel onglet et affiche une notification.

1. Déclaration dans manifest.json : Pour qu'un script soit reconnu comme un Service Worker, il doit être déclaré dans le fichier manifest.json sous la clé background :

// manifest.json
{
  "manifest_version": 3,
  "name": "Mon Extension Super Cool",
  "version": "1.0",
  "permissions": ["tabs", "notifications"], // Nécessaire pour accéder aux onglets et aux notifications
  "host_permissions": ["<all_urls>"], // Peut être nécessaire pour certaines interactions générales
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  }
}

Explication : Nous spécifions manifest_version: 3 et indiquons que background.js est notre service_worker. Nous ajoutons également les permissions et host_permissions nécessaires ainsi que les icônes.

2. Le Fichier background.js :

// background.js
chrome.runtime.onInstalled.addListener(() => {
  console.log('Extension installée ou mise à jour !');
});

// Écoute l'événement de création d'un nouvel onglet
chrome.tabs.onCreated.addListener((tab) => {
  console.log(`Nouvel onglet créé avec l'ID : ${tab.id} et URL : ${tab.url}`);

  // Crée une notification
  chrome.notifications.create({
    type: 'basic',
    iconUrl: 'icon128.png', // Assurez-vous d'avoir une icône dans votre dossier
    title: 'Nouvel Onglet !',
    message: `Vous avez ouvert un nouvel onglet avec l'URL: ${tab.url || 'inconnue'}`
  });
});

// Écoute les messages provenant d'autres parties de l'extension (ex: scripts de contenu)
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "afficherMessageBackground") {
    console.log("Message reçu du script de contenu :", request.data);
    sendResponse({ status: "Message reçu par le background." });
  }
  return true; // Important pour gérer sendResponse de manière asynchrone si nécessaire
});

Explication :

  • chrome.runtime.onInstalled.addListener : Un événement qui se déclenche une seule fois lors de l'installation ou de la mise à jour de l'extension.
  • chrome.tabs.onCreated.addListener : Un écouteur d'événements qui se déclenche chaque fois qu'un nouvel onglet est créé. Il reçoit l'objet tab en argument, contenant des informations sur l'onglet.
  • chrome.notifications.create : Une API pour afficher des notifications système.
  • chrome.runtime.onMessage.addListener : Ceci est un exemple de la manière dont le Service Worker peut écouter des messages provenant d'autres parties de l'extension (nous y reviendrons en détail plus tard). Le return true est crucial si sendResponse est appelée de manière asynchrone (par exemple, après une requête réseau ou un setTimeout).

Les Scripts de Contenu (Content Scripts)

Si les scripts d'arrière-plan sont le cerveau, les scripts de contenu sont les mains et les yeux de votre extension sur les pages web. Ils sont injectés directement dans le contexte d'une page web spécifique, leur permettant d'interagir avec son DOM, son JavaScript et son CSS.

Rôle et Fonctionnalités

Les scripts de contenu sont parfaits pour :

  • Lire et modifier le DOM : Injecter du HTML, modifier des styles, extraire des données.
  • Interagir avec le JavaScript de la page : Appeler des fonctions, modifier des variables globales (avec prudence).
  • Ajouter des fonctionnalités aux pages existantes : Boutons, barres latérales, filtres.
  • Envoyer des informations au script d'arrière-plan : Par exemple, envoyer le texte sélectionné par l'utilisateur.

Isolation et Sécurité

Les scripts de contenu s'exécutent dans un monde isolé. Cela signifie :

  • Accès au DOM : Ils ont accès au même DOM que le script de la page.
  • Isolation JS : Ils n'ont pas accès aux variables ou fonctions JavaScript définies par la page elle-même, et vice-versa. Chaque script de contenu dispose de son propre environnement JavaScript. Cela prévient les conflits et renforce la sécurité.
  • Accès limité aux API d'extension : Contrairement au script d'arrière-plan, les scripts de contenu n'ont accès qu'à un sous-ensemble des API d'extension, principalement chrome.runtime pour la communication.

Exemple : Un Script de Contenu qui Modifie le DOM

Créons un script de contenu qui modifie le titre de toutes les pages visitées et ajoute un paragraphe.

1. Déclaration dans manifest.json : Les scripts de contenu sont déclarés dans le manifest.json sous la clé content_scripts. Vous devez spécifier les pages sur lesquelles ils doivent être injectés (matches).

// manifest.json (suite du fichier précédent)
{
  "manifest_version": 3,
  "name": "Mon Extension Super Cool",
  "version": "1.0",
  "permissions": ["tabs", "notifications"],
  "host_permissions": ["<all_urls>"],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"], // Injecter sur toutes les URL
      "js": ["content.js"],
      "css": ["content.css"], // Optionnel : injecter du CSS
      "run_at": "document_idle" // Quand injecter le script (document_start, document_end, document_idle)
    }
  ],
  "icons": {
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  }
}

Explication :

  • matches: ["<all_urls>"] : Indique que le script doit être injecté sur toutes les pages. Vous pouvez être plus spécifique, par exemple ["*://*.google.com/*"] pour les pages Google.
  • js: ["content.js"] : Le chemin vers votre fichier JavaScript de script de contenu.
  • css: ["content.css"] : Optionnel, pour injecter des feuilles de style.
  • run_at: "document_idle" : Le moment où le script doit être injecté. document_idle est généralement un bon choix, car il attend que la page soit prête et que le DOM soit entièrement construit.

2. Le Fichier content.js :

// content.js
console.log("Script de contenu injecté !");

// Modifie le titre de la page
document.title = "Page modifiée par l'extension !";

// Ajoute un nouveau paragraphe au corps de la page
const newParagraph = document.createElement('p');
newParagraph.textContent = "Ce paragraphe a été ajouté par votre extension.";
newParagraph.style.backgroundColor = "lightgreen";
newParagraph.style.padding = "10px";
newParagraph.style.border = "1px solid green";
document.body.prepend(newParagraph); // Ajoute au début du body

// Exemple d'interaction avec le script d'arrière-plan
// Envoie un message au Service Worker pour lui dire que la page a été modifiée
chrome.runtime.sendMessage({ action: "afficherMessageBackground", data: "Page modifiée avec succès : " + window.location.href });

Explication :

  • Le script accède directement à document.title et document.body pour les modifier.
  • Il utilise des méthodes DOM standard (createElement, prepend) pour manipuler la structure de la page.
  • chrome.runtime.sendMessage : Permet au script de contenu d'envoyer un message au Service Worker (encore une fois, nous détaillerons la communication ensuite).

Communication entre Scripts d'Arrière-Plan et Scripts de Contenu

C'est ici que la magie opère ! Pour que votre extension soit réellement interactive, vos scripts d'arrière-plan et scripts de contenu doivent pouvoir échanger des informations. Ils ne partagent pas le même environnement d'exécution, donc une communication explicite est nécessaire.

Pourquoi la communication est essentielle ?

  • Un script de contenu a besoin d'informations du script d'arrière-plan (ex: préférences utilisateur, données provenant d'une API distante).
  • Un script de contenu doit informer le script d'arrière-plan d'une action utilisateur sur la page (ex: clic sur un élément, texte sélectionné).
  • Le script d'arrière-plan a besoin d'envoyer des commandes au script de contenu (ex: modifier une partie de la page, afficher un message).

Le mécanisme principal de communication est l'échange de messages.

Messages Unidirectionnels (one-time requests)

Le modèle le plus courant est l'envoi d'un message ponctuel, avec une option pour une réponse.

  • Du script de contenu au Service Worker : Le script de contenu envoie un message au Service Worker. Le Service Worker peut optionnellement renvoyer une réponse.
  • Du Service Worker au script de contenu : Le Service Worker peut envoyer un message à un script de contenu spécifique dans un onglet donné. Le script de contenu peut optionnellement renvoyer une réponse.

Exemple de Communication : Demande et Réponse

Reprenons nos exemples précédents pour illustrer la communication.

1. Le Service Worker (background.js) écoute et répond :

// background.js (suite du fichier précédent)
chrome.runtime.onInstalled.addListener(() => {
  console.log('Extension installée ou mise à jour !');
});

chrome.tabs.onCreated.addListener((tab) => {
  console.log(`Nouvel onglet créé avec l'ID : ${tab.id} et URL : ${tab.url}`);
  chrome.notifications.create({
    type: 'basic',
    iconUrl: 'icon128.png',
    title: 'Nouvel Onglet !',
    message: `Vous avez ouvert un nouvel onglet avec l'URL: ${tab.url || 'inconnue'}`
  });
});

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "getApiKey") {
    // Imaginons que la clé API soit stockée ici ou récupérée via une autre API
    const apiKey = "VOTRE_CLE_API_SECRETE_12345";
    sendResponse({ apiKey: apiKey, message: "Clé API envoyée !" });
    return true; // Important : indique que sendResponse sera appelée de manière asynchrone
  }
  if (request.action === "afficherMessageBackground") {
    console.log("Message reçu du script de contenu :", request.data);
    sendResponse({ status: "Message reçu par le background." });
    return true;
  }
});

// Le Service Worker peut aussi envoyer un message à un script de contenu
function sendMessageToContentScript(tabId, message) {
  // Vérifie si l'onglet est toujours valide avant d'envoyer
  chrome.tabs.get(tabId, (tab) => {
    if (chrome.runtime.lastError || !tab) {
      console.error(`Impossible de trouver l'onglet ${tabId}.`, chrome.runtime.lastError);
      return;
    }
    chrome.tabs.sendMessage(tabId, message, (response) => {
      if (chrome.runtime.lastError) {
        console.error(`Erreur lors de l'envoi au script de contenu de l'onglet ${tabId}:`, chrome.runtime.lastError.message);
        return;
      }
      if (response) {
        console.log(`Réponse du script de contenu (${tabId}) :`, response);
      } else {
        console.log(`Pas de réponse du script de contenu (${tabId}).`);
      }
    });
  });
}

// Exemple : envoyer un message au script de contenu de l'onglet actif après 5 secondes
// (Note: dans un Service Worker, les opérations asynchrones comme setTimeout doivent être gérées
// avec attention car le worker peut se terminer. Pour des délais longs, utilisez chrome.alarms)
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
  if (tabs[0]) {
    const activeTabId = tabs[0].id;
    setTimeout(() => {
      console.log("Envoi d'un message au script de contenu de l'onglet actif.");
      sendMessageToContentScript(activeTabId, { action: "updateDOM", data: "Nouveau message du background !" });
    }, 5000);
  }
});

Explication du Service Worker :

  • L'écouteur chrome.runtime.onMessage.addListener reçoit le request (le message), le sender (informations sur l'origine du message) et la fonction sendResponse (pour envoyer une réponse).
  • Si sendResponse est appelée de manière asynchrone (par exemple, après une Promise ou setTimeout), vous devez return true de l'écouteur pour indiquer que la fonction sendResponse sera utilisée plus tard.
  • chrome.tabs.sendMessage(tabId, message, callback) : Permet au Service Worker d'envoyer un message à un script de contenu spécifique dans l'onglet tabId. Le callback reçoit la réponse du script de contenu.
  • Nous avons ajouté une vérification chrome.tabs.get et chrome.runtime.lastError pour gérer les cas où l'onglet n'existe plus ou il y a une erreur.

2. Le Script de Contenu (content.js) envoie et reçoit :

// content.js (suite du fichier précédent)
console.log("Script de contenu injecté !");

document.title = "Page modifiée par l'extension !";

const newParagraph = document.createElement('p');
newParagraph.textContent = "Ce paragraphe a été ajouté par votre extension.";
newParagraph.style.backgroundColor = "lightgreen";
newParagraph.style.padding = "10px";
newParagraph.style.border = "1px solid green";
document.body.prepend(newParagraph);

// Envoie un message au Service Worker pour demander une clé API
chrome.runtime.sendMessage({ action: "getApiKey" }, (response) => {
  if (response && response.apiKey) {
    console.log("Clé API reçue du background :", response.apiKey);
    const apiDataDiv = document.createElement('div');
    apiDataDiv.textContent = `Clé API de l'extension : ${response.apiKey.substring(0, 5)}...`;
    apiDataDiv.style.cssText = "background-color: #ffe0b2; padding: 8px; border: 1px solid orange; margin-top: 10px;";
    document.body.prepend(apiDataDiv);
  } else {
    console.error("Erreur ou pas de clé API reçue.");
  }
});

// Écoute les messages provenant du Service Worker
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "updateDOM") {
    console.log("Message reçu du background pour mettre à jour le DOM :", request.data);
    const updateDiv = document.createElement('div');
    updateDiv.textContent = `Message du background : "${request.data}"`;
    updateDiv.style.cssText = "background-color: #b2ebf2; padding: 8px; border: 1px solid #00bcd4; margin-top: 10px;";
    document.body.prepend(updateDiv);
    sendResponse({ status: "DOM mis à jour !" }); // Envoie une réponse au background
    return true;
  }
});

Explication du Script de Contenu :

  • chrome.runtime.sendMessage(message, callback) : Le script de contenu envoie un message au Service Worker. Le callback est exécuté lorsque le Service Worker envoie une réponse.
  • L'écouteur chrome.runtime.onMessage.addListener fonctionne de manière similaire à celui du Service Worker, lui permettant de recevoir des messages et d'y répondre. Encore une fois, return true est utilisé si sendResponse est asynchrone.

Messages Bidirectionnels Persistants (Long-Lived Connections)

Pour des communications plus complexes ou des flux de données continus, vous pouvez utiliser des ports. Les ports permettent d'ouvrir une connexion persistante entre deux parties de l'extension (ex: Service Worker et Content Script), ce qui est utile pour envoyer plusieurs messages sur la même connexion sans avoir à établir une nouvelle connexion à chaque fois. Cependant, pour la plupart des cas, les messages unidirectionnels sont suffisants et plus simples à implémenter.

Points Clés à Retenir

  • Scripts d'Arrière-Plan (Service Workers) : Le cerveau de l'extension. Accès complet aux API d'extension, gère les événements du navigateur, ne peut pas interagir directement avec le DOM des pages web.
  • Scripts de Contenu : Les yeux et les mains. Injectés dans les pages web, accès direct au DOM de la page, environnement JavaScript isolé, accès limité aux API d'extension (principalement chrome.runtime).
  • Communication : Essentielle pour l'interactivité. Se fait principalement via chrome.runtime.sendMessage (pour des messages ponctuels depuis le contenu vers le background) et chrome.tabs.sendMessage (pour envoyer au contenu depuis le background).
  • Manifest V3 : Les Service Workers remplacent les scripts d'arrière-plan persistants pour une meilleure performance et une consommation de ressources optimisée.
  • Sécurité : L'isolation des scripts de contenu garantit que votre extension ne peut pas interférer avec le JavaScript de la page de manière inattendue, et vice-versa.

Conclusion

Vous avez maintenant une compréhension solide des rôles cruciaux joués par les scripts d'arrière-plan (Service Workers) et les scripts de contenu dans le développement d'extensions de navigateur. Vous savez comment déclarer ces scripts dans votre manifest.json, comment ils opèrent dans leurs environnements respectifs et, surtout, comment ils peuvent communiquer pour créer une expérience utilisateur riche et interactive.

Maîtriser ces concepts est un jalon majeur. C'est la base qui vous permettra de transformer des idées complexes en fonctionnalités tangibles, d'automatiser des tâches, de personnaliser l'expérience de navigation et d'interagir intelligemment avec le web. Dans les prochaines leçons, nous explorerons d'autres composants comme les popups et les pages d'options, qui s'appuieront fortement sur la communication avec votre Service Worker.