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'arrière-plan ou d'événement de l'extension pour s'assurer que le code d'arrière-plan reste en dehors du thread principal. Cela permet aux extensions de ne s'exécuter que lorsque cela est nécessaire, ce qui permet d'économiser des ressources.

Les pages d'arrière-plan sont un composant fondamental des extensions depuis leur introduction. Pour faire simple, les pages d'arrière-plan fournissent un environnement qui existe indépendamment de toute autre fenêtre ou de tout autre onglet. Cela permet aux extensions d'observer les événements et d'agir en conséquence.

Cette page décrit les tâches permettant de convertir des pages d'arrière-plan en service workers d'extension. Pour en savoir plus sur les service workers d'extension en général, consultez le tutoriel Gérer les événements avec des 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'extension

Dans certains contextes, les service workers 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 d'arrière-plan

Les service workers présentent un certain nombre de différences par rapport aux pages d'arrière-plan.

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

Modifications à apporter

Vous devrez 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 façon dont un service worker est spécifié dans le fichier manifeste est différente de celle dont les scripts d'arrière-plan sont spécifiés. De plus :

  • Comme ils ne peuvent pas accéder au DOM ni à l'interface window, vous devrez 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 devrez remplacer les appels à cette interface par des appels à fetch().
  • Comme elles s'arrêtent lorsqu'elles ne sont pas utilisées, vous devez conserver les états de l'application plutôt que de vous fier à des variables globales. L'arrêt des service workers peut également mettre fin aux minuteurs avant qu'ils ne soient terminés. Vous devrez 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 d'arrière-plan sont remplacées par un service worker. Les modifications du 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'aurez 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 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 aux objets DOM et 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 l'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.

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

Dans le document hors écran, effectuez toute action que vous auriez précédemment exécutée dans un script d'arrière-plan. Par exemple, vous pouvez copier du 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 service workers 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, vous pouvez effectuer l'une des deux actions suivantes. Tout d'abord, vous pouvez le remplacer par des appels à un autre mécanisme de stockage. L'espace de noms browser.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, vérifiez browser.storage pour vos données.
  4. Si vos données sont introuvables, créez un document hors écran et appelez runtime.sendMessage() pour lancer la routine de conversion.
  5. Dans le gestionnaire runtime.onMessage que vous avez ajouté au document hors écran, appelez la routine de conversion.

Il existe également quelques nuances dans le fonctionnement des API de stockage Web dans les extensions. 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) n'est pas garanti dans Manifest V3. Examinez le code suivant.

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

Cela fonctionne avec une page d'arrière-plan persistante, car la page s'exécute en permanence 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énements au niveau supérieur de votre script. Cela permet à Chrome de trouver et d'appeler immédiatement le gestionnaire de clics de votre action, même si votre extension n'a pas fini d'exécuter sa logique de démarrage.

browser.action.onClicked.addListener(handleActionClick);

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

Remplacer XMLHttpRequest() par fetch() global

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

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

Conserver les états

Les service workers sont éphémères, ce qui signifie qu'ils sont susceptibles de démarrer, de s'exécuter et de s'arrêter à plusieurs reprises au cours de la session de navigation 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é supprimé. Pour contourner ce problème, utilisez les API Storage comme source de vérité. Un exemple vous montrera 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 navigation d'un utilisateur.

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

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

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

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

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

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

Convertir les 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 service workers, car les minuteurs sont annulés chaque fois que le service worker est arrêté.

Script d'arrière-plan Manifest V2
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  browser.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 browser.alarms.create(name, { delayInMinutes: 3 });
}

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

Maintenir le service worker actif

Les service workers sont par définition basés sur des é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.

Maintenir 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() qui peut prendre plus de cinq minutes (par exemple, un téléchargement volumineux sur une connexion potentiellement de mauvaise qualité).
  • Un calcul asynchrone complexe prenant 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. Veuillez noter que cette option n'est réservée qu'à des cas exceptionnels et que, dans la plupart des situations, il existe généralement une meilleure façon d'obtenir le même résultat, propre à la plate-forme.

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

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

waitUntil(someExpensiveCalculation());

Maintenir un service worker actif en continu

Dans de rares cas, il est nécessaire de prolonger la durée de vie indéfiniment. Nous avons identifié les entreprises et l'éducation comme les cas d'utilisation les plus importants, et nous l'autorisons spécifiquement dans ces domaines, mais nous ne le prenons pas en charge de manière générale. Dans ces circonstances exceptionnelles, il est possible de maintenir un service worker actif en appelant régulièrement une API d'extension simple. Il est important de noter 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 l'enseignement. Cela n'est pas autorisé dans les autres cas, et l'équipe chargée des extensions Chrome se réserve le droit de prendre des mesures contre 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 browser.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 browser.storage.local.get('last-heartbeat'))['last-heartbeat'];
}