Migrer vers un service worker

Remplacer les pages d'arrière-plan ou d'événement par un service worker

Un service worker remplace la page d'événement ou d'arrière-plan de l'extension pour s'assurer que le code en arrière-plan ne s'exécute pas sur le thread principal. Les extensions ne s'exécutent donc que lorsque cela est nécessaire, ce qui permet d'économiser des ressources.

Les pages en arrière-plan sont un composant essentiel des extensions depuis leur introduction. Pour faire simple, les pages en arrière-plan offrent un environnement indépendant de toute autre fenêtre ou de tout autre onglet. Cela permet aux extensions d'observer et d'agir en réponse aux événements.

Cette page décrit les tâches à effectuer pour convertir des pages en arrière-plan en workers de service d'extension. Pour en savoir plus sur les service workers d'extension de manière générale, consultez le tutoriel Gérer les événements avec les service workers et la section À propos des service workers d'extension.

Différences entre les scripts d'arrière-plan et les service workers d'extensions

Dans certains contextes, les workers de service d'extension sont appelés "scripts d'arrière-plan". Bien que les service workers d'extension s'exécutent en arrière-plan, les appeler "scripts d'arrière-plan" est quelque peu trompeur, car cela implique des fonctionnalités identiques. Les différences sont décrites ci-dessous.

Modifications apportées aux pages en arrière-plan

Les service workers présentent plusieurs différences avec les pages en arrière-plan.

  • Elles fonctionnent en dehors du thread principal, ce qui signifie qu'elles n'interfèrent pas avec le contenu de l'extension.
  • Elles disposent de fonctionnalités spéciales, telles que l'interception d'événements d'extraction à l'origine de l'extension, comme ceux provenant d'un pop-up de barre d'outils.
  • Ils peuvent communiquer et interagir avec d'autres contextes via l'interface client.

Modifications à apporter

Vous devez apporter quelques modifications au code pour tenir compte des différences de fonctionnement entre les scripts d'arrière-plan et les service workers. Pour commencer, la manière dont un service worker est spécifié dans le fichier manifeste est différente de celle des scripts en arrière-plan. De plus :

  • Comme ils ne peuvent pas accéder au DOM ni à l'interface window, vous devez déplacer ces appels vers une autre API ou vers un document hors écran.
  • Les écouteurs d'événements ne doivent pas être enregistrés en réponse aux promesses renvoyées ni dans les rappels d'événements.
  • Comme elles ne sont pas rétrocompatibles avec XMLHttpRequest(), vous devez remplacer les appels à cette interface par des appels à fetch().
  • Comme ils s'arrêtent lorsqu'ils ne sont pas utilisés, vous devez conserver les états de l'application plutôt que de vous appuyer sur des variables globales. L'arrêt des services workers peut également mettre fin aux minuteurs avant leur expiration. Vous devez les remplacer par des alarmes.

Cette page décrit ces tâches en détail.

Mettre à jour le champ "background" dans le fichier manifeste

Dans Manifest V3, les pages en arrière-plan sont remplacées par un service worker. Les modifications apportées au fichier manifeste sont listées ci-dessous.

  • Remplacez "background.scripts" par "background.service_worker" dans manifest.json. Notez que le champ "service_worker" accepte une chaîne, et non un tableau de chaînes.
  • Supprimez "background.persistent" de manifest.json.
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifest V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

Le champ "service_worker" accepte une seule chaîne. Vous n'avez besoin du champ "type" que si vous utilisez des modules ES (à l'aide du mot clé import). Sa valeur sera toujours "module". Pour en savoir plus, consultez la section Principes de base des service workers d'extension.

Déplacer les appels DOM et de fenêtre vers un document hors écran

Certaines extensions ont besoin d'accéder au DOM et aux objets de fenêtre sans ouvrir visuellement une nouvelle fenêtre ou un nouvel onglet. L'API Offscreen prend en charge ces cas d'utilisation en ouvrant et en fermant les documents non affichés empaquetés avec une extension, sans perturber l'expérience utilisateur. À l'exception de la transmission de messages, les documents hors écran ne partagent pas d'API avec d'autres contextes d'extension, mais fonctionnent comme des pages Web complètes avec lesquelles les extensions peuvent interagir.

Pour utiliser l'API Offscreen, créez un document hors écran à partir du service worker.

chrome.offscreen.createDocument({
  url: chrome.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

Dans le document hors écran, effectuez toute action que vous auriez pu exécuter précédemment dans un script en arrière-plan. Par exemple, vous pouvez copier le texte sélectionné sur la page hôte.

let textEl = document.querySelector('#text');
textEl.value = data;
textEl.select();
document.execCommand('copy');

Communiquez entre les documents hors écran et les nœuds de calcul de service d'extension à l'aide de la transmission de messages.

Convertir localStorage en un autre type

L'interface Storage de la plate-forme Web (accessible depuis window.localStorage) ne peut pas être utilisée dans un service worker. Pour résoudre ce problème, effectuez l'une des deux opérations suivantes. Tout d'abord, vous pouvez le remplacer par des appels à un autre mécanisme de stockage. L'espace de noms chrome.storage.local convient à la plupart des cas d'utilisation, mais d'autres options sont disponibles.

Vous pouvez également déplacer ses appels vers un document hors écran. Par exemple, pour migrer des données précédemment stockées dans localStorage vers un autre mécanisme:

  1. Créez un document hors écran avec une routine de conversion et un gestionnaire runtime.onMessage.
  2. Ajoutez une routine de conversion au document hors écran.
  3. Dans le service worker de l'extension, recherchez vos données dans chrome.storage.
  4. Si vos données ne sont pas trouvées, create un document hors écran et appelez runtime.sendMessage() pour démarrer la routine de conversion.
  5. Dans le gestionnaire runtime.onMessage que vous avez ajouté au document hors écran, appelez la routine de conversion.

Le fonctionnement des API de stockage Web dans les extensions présente également quelques nuances. Pour en savoir plus, consultez Stockage et cookies.

Enregistrer les écouteurs de manière synchrone

L'enregistrement d'un écouteur de manière asynchrone (par exemple, dans une promesse ou un rappel) ne fonctionne pas nécessairement dans le fichier manifeste V3. Prenons l'exemple de code suivant.

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.browserAction.setBadgeText({ text: badgeText });
  chrome.browserAction.onClicked.addListener(handleActionClick);
});

Cela fonctionne avec une page de fond persistante, car elle est constamment exécutée et n'est jamais réinitialisée. Dans Manifest V3, le service worker est réinitialisé lorsque l'événement est distribué. Cela signifie que lorsque l'événement se déclenche, les écouteurs ne sont pas enregistrés (car ils sont ajoutés de manière asynchrone) et l'événement est manqué.

Déplacez plutôt l'enregistrement de l'écouteur d'événement au niveau supérieur de votre script. Ainsi, Chrome pourra immédiatement trouver et appeler le gestionnaire de clics de votre action, même si votre extension n'a pas fini d'exécuter sa logique de démarrage.

chrome.action.onClicked.addListener(handleActionClick);

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.action.setBadgeText({ text: badgeText });
});

Remplacer XMLHttpRequest() par fetch() global

XMLHttpRequest() ne peut pas être appelé à partir d'un service worker, d'une extension ou d'un autre élément. Remplacez les appels de votre script en arrière-plan à XMLHttpRequest() par des appels à fetch() global.

XMLHttpRequest()
const xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.readyState);

xhr.open('GET', '/api', true);
console.log('OPENED', xhr.readyState);

xhr.onload = () => {
    console.log('DONE', xhr.readyState);
};
xhr.send(null);
fetch()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

États persistants

Les services workers sont éphémères, ce qui signifie qu'ils démarrent, s'exécutent et s'arrêtent probablement de manière répétée pendant la session de navigateur d'un utilisateur. Cela signifie également que les données ne sont pas immédiatement disponibles dans les variables globales, car le contexte précédent a été détruit. Pour contourner ce problème, utilisez les API de stockage comme source de référence. Un exemple vous montre comment procéder.

L'exemple suivant utilise une variable globale pour stocker un nom. Dans un service worker, cette variable peut être réinitialisée plusieurs fois au cours de la session de navigateur d'un utilisateur.

Script en arrière-plan Manifest V2
let savedName = undefined;

chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    savedName = name;
  }
});

chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.sendMessage(tab.id, { name: savedName });
});

Pour le fichier manifeste V3, remplacez la variable globale par un appel à l'API Storage.

Service worker Manifest V3
chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    chrome.storage.local.set({ name });
  }
});

chrome.action.onClicked.addListener(async (tab) => {
  const { name } = await chrome.storage.local.get(["name"]);
  chrome.tabs.sendMessage(tab.id, { name });
});

Convertir des minuteurs en alarmes

Il est courant d'utiliser des opérations différées ou périodiques à l'aide des méthodes setTimeout() ou setInterval(). Toutefois, ces API peuvent échouer dans les services workers, car les minuteurs sont annulés chaque fois que le service worker est arrêté.

Script en arrière-plan Manifest V2
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

Utilisez plutôt l'API Alarms. Comme pour les autres écouteurs, les écouteurs d'alarme doivent être enregistrés au niveau supérieur de votre script.

Service worker Manifest V3
async function startAlarm(name, duration) {
  await chrome.alarms.create(name, { delayInMinutes: 3 });
}

chrome.alarms.onAlarm.addListener(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
});

Maintenir le service worker en vie

Les services workers sont par définition basés sur les événements et s'arrêtent en cas d'inactivité. Chrome peut ainsi optimiser les performances et la consommation de mémoire de votre extension. Pour en savoir plus, consultez notre documentation sur le cycle de vie des service workers. Dans des cas exceptionnels, des mesures supplémentaires peuvent être nécessaires pour s'assurer qu'un service worker reste actif plus longtemps.

Conserver un service worker actif jusqu'à la fin d'une opération de longue durée

Lors d'opérations de service worker de longue durée qui n'appellent pas d'API d'extension, le service worker peut s'arrêter en cours d'opération. Voici quelques exemples :

  • Une requête fetch() peut prendre plus de cinq minutes (par exemple, un téléchargement volumineux avec une connexion potentiellement mauvaise).
  • Un calcul asynchrone complexe qui prend plus de 30 secondes.

Pour prolonger la durée de vie du service worker dans ces cas, vous pouvez appeler régulièrement une API d'extension simple pour réinitialiser le compteur de délai avant expiration. Notez que cette méthode n'est réservée qu'aux cas exceptionnels. Dans la plupart des cas, il existe généralement une meilleure méthode, propre à la plate-forme, pour obtenir le même résultat.

L'exemple suivant montre une fonction d'assistance waitUntil() qui maintient votre service worker en vie jusqu'à ce qu'une promesse donnée soit résolue:

async function waitUntil(promise) = {
  const keepAlive = setInterval(chrome.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

Maintenir un service worker en activité en continu

Dans de rares cas, il est nécessaire d'étendre la durée de vie indéfiniment. Nous avons identifié l'entreprise et l'enseignement comme les principaux cas d'utilisation, et nous les autorisons spécifiquement dans ces domaines, mais nous ne les acceptons pas en général. Dans ces circonstances exceptionnelles, vous pouvez maintenir un service worker en appelant périodiquement une API d'extension simple. Notez bien que cette recommandation ne s'applique qu'aux extensions exécutées sur des appareils gérés pour des cas d'utilisation en entreprise ou dans le secteur de l'éducation. Dans les autres cas, cela n'est pas autorisé, et l'équipe chargée des extensions Chrome se réserve le droit de prendre des mesures à l'encontre de ces extensions à l'avenir.

Utilisez l'extrait de code suivant pour maintenir votre service worker actif:

/**
 * Tracks when a service worker was last alive and extends the service worker
 * lifetime by writing the current time to extension storage every 20 seconds.
 * You should still prepare for unexpected termination - for example, if the
 * extension process crashes or your extension is manually stopped at
 * chrome://serviceworker-internals. 
 */
let heartbeatInterval;

async function runHeartbeat() {
  await chrome.storage.local.set({ 'last-heartbeat': new Date().getTime() });
}

/**
 * Starts the heartbeat interval which keeps the service worker alive. Call
 * this sparingly when you are doing work which requires persistence, and call
 * stopHeartbeat once that work is complete.
 */
async function startHeartbeat() {
  // Run the heartbeat once at service worker startup.
  runHeartbeat().then(() => {
    // Then again every 20 seconds.
    heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
  });
}

async function stopHeartbeat() {
  clearInterval(heartbeatInterval);
}

/**
 * Returns the last heartbeat stored in extension storage, or undefined if
 * the heartbeat has never run before.
 */
async function getLastHeartbeat() {
  return (await chrome.storage.local.get('last-heartbeat'))['last-heartbeat'];
}