Migrer vers un service worker

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

Un service worker remplace l'arrière-plan ou la page d'événements de l'extension pour s'assurer que le code d'arrière-plan reste hors du thread principal. Cela permet aux extensions de s'exécuter uniquement en cas de besoin, ce qui permet d'économiser des ressources.

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

Cette page décrit les tâches de conversion des pages en arrière-plan en service workers d'extension. Pour en savoir plus sur les service workers d'extension de façon 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'extension

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

Modifications depuis les pages en arrière-plan

Les service workers présentent un certain nombre de 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 des extensions.
  • Elles offrent des fonctionnalités spéciales, comme intercepter les événements d'extraction 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 client.

Modifications que vous devez apporter

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

  • Étant donné qu'ils ne peuvent pas accéder au DOM ni à l'interface window, vous devez déplacer ces appels vers une autre API ou dans un document hors écran.
  • Les écouteurs d'événements ne doivent pas être enregistrés en réponse à des promesses renvoyées ou dans des rappels d'événements.
  • Étant donné qu'ils ne sont pas rétrocompatibles avec XMLHttpRequest(), vous devrez remplacer les appels à cette interface par des appels à fetch().
  • Étant donné qu'elles s'arrêtent lorsqu'elles ne sont pas utilisées, vous devez conserver les états d'application plutôt que d'utiliser des variables globales. Il est également possible d'arrêter des 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 "arrière-plan" 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 indiqué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. Le champ "type" n'est nécessaire que si vous utilisez des modules ES (avec le mot clé import). Sa valeur sera toujours "module". Pour en savoir plus, consultez Principes de base des services d'extension de l'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 window 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 des 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 fourni par le service worker.

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

Dans le document hors écran, effectuez toutes les actions que vous auriez auparavant exécutées 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');

La communication entre des documents hors écran et les service workers s'effectue via la transmission de messages.

Convertir localStorage vers 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, deux solutions s'offrent à vous. Tout d'abord, vous pouvez le remplacer par des appels vers un autre mécanisme de stockage. L'espace de noms chrome.storage.local est adapté à 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 vos données sur chrome.storage.
  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 des 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.

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

Cela fonctionne avec une page d'arrière-plan persistante, car elle 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 envoyé. 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 sera manqué.

Déplacez plutôt l'enregistrement de l'écouteur d'événements vers le 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 global fetch()

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

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, d'exécuter et d'arrêter plusieurs fois la session de navigateur d'un utilisateur. Cela signifie également que les données ne sont pas immédiatement disponibles dans les variables globales puisque le contexte précédent a été supprimé. Pour contourner ce problème, utilisez les API de stockage comme source de référence. Pour savoir comment procéder, consultez un exemple.

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 d'une session de navigateur.

Script d'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 Manifest 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 retardées ou périodiques à l'aide des méthodes setTimeout() ou setInterval(). Toutefois, ces API peuvent échouer chez 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(() => {
  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 actif

Par définition, les service workers sont 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. Des cas exceptionnels peuvent nécessiter des mesures supplémentaires pour garantir qu'un service worker reste en vie 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, un 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 (un téléchargement volumineux sur une connexion potentiellement médiocre, par exemple)
  • Calcul asynchrone complexe qui prend plus de 30 secondes.

Dans ce cas, pour prolonger la durée de vie du service worker, vous pouvez appeler régulièrement une API d'extension simple afin de réinitialiser le compteur de délai d'inactivité. Veuillez noter que cette procédure est réservée à des cas exceptionnels et que, dans la plupart des cas, il existe un meilleur moyen idiomatique pour la plate-forme afin d'obtenir le même résultat.

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

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 vie en continu

Dans de rares cas, il est nécessaire de prolonger la durée de vie indéfiniment. Nous avons identifié les principaux cas d'utilisation pour les entreprises et les établissements d'enseignement, et nous y autorisons spécifiquement cette utilisation, mais nous ne les 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 en établissement scolaire. Cela n'est pas autorisé dans les autres cas, et l'équipe Chrome se réserve le droit de prendre des mesures à l'encontre de ces extensions par la suite.

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'];
}