Migrazione a un service worker

Sostituzione delle pagine di sfondo o di eventi con un service worker

Un service worker sostituisce la pagina di sfondo o di evento dell'estensione per garantire che il codice in background rimanga fuori dal thread principale. In questo modo, le estensioni vengono eseguite solo quando necessario, risparmiando risorse.

Le pagine di sfondo sono un componente fondamentale delle estensioni sin dalla loro introduzione. In parole povere, le pagine di sfondo forniscono un ambiente che vive indipendentemente da qualsiasi altra finestra o scheda. In questo modo, le estensioni possono osservare e agire in risposta agli eventi.

Questa pagina descrive le attività per convertire le pagine in background in service worker di estensione. Per saperne di più sui service worker delle estensioni in generale, consulta il tutorial Gestisci gli eventi con i service worker e la sezione Informazioni sui service worker delle estensioni.

Differenze tra script in background e service worker di estensione

In alcuni contesti, i service worker delle estensioni vengono chiamati "script in background". Sebbene i service worker delle estensioni vengano eseguiti in background, chiamarli script in background è un po' fuorviante, in quanto implica funzionalità identiche. Le differenze sono descritte di seguito.

Modifiche dalle pagine in background

I service worker presentano una serie di differenze rispetto alle pagine in background.

  • Funzionano al di fuori del thread principale, il che significa che non interferiscono con i contenuti dell'estensione.
  • Hanno funzionalità speciali, come l'intercettazione degli eventi di recupero sull'origine dell'estensione, ad esempio quelli di un popup della barra degli strumenti.
  • Possono comunicare e interagire con altri contesti tramite l'interfaccia Clients.

Modifiche che dovrai apportare

Dovrai apportare alcune modifiche al codice per tenere conto delle differenze tra il funzionamento degli script in background e dei service worker. Per iniziare, il modo in cui un service worker viene specificato nel file manifest è diverso da come vengono specificati gli script in background. Inoltre:

  • Poiché non possono accedere al DOM o all'interfaccia window, dovrai spostare queste chiamate in un'altra API o in un documento fuori schermo.
  • I listener di eventi non devono essere registrati in risposta alle promise restituite o all'interno dei callback degli eventi.
  • Poiché non sono compatibili con le versioni precedenti di XMLHttpRequest(), dovrai sostituire le chiamate a questa interfaccia con chiamate a fetch().
  • Poiché vengono terminate quando non in uso, dovrai rendere persistenti gli stati dell'applicazione anziché fare affidamento su variabili globali. La chiusura dei service worker può anche interrompere i timer prima che siano completati. Dovrai sostituirli con delle sveglie.

Questa pagina descrive queste attività nel dettaglio.

Aggiorna il campo "background" nel manifest

In Manifest V3, le pagine in background vengono sostituite da un service worker. Le modifiche al file manifest sono elencate di seguito.

  • Sostituisci "background.scripts" con "background.service_worker" in manifest.json. Tieni presente che il campo "service_worker" accetta una stringa, non un array di stringhe.
  • Rimuovi "background.persistent" da manifest.json.
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifest V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

Il campo "service_worker" accetta una singola stringa. Il campo "type" è necessario solo se utilizzi i moduli ES (utilizzando la parola chiave import). Il suo valore sarà sempre "module". Per saperne di più, consulta Nozioni di base sui service worker delle estensioni.

Sposta le chiamate DOM e window in un documento fuori schermo

Alcune estensioni devono accedere agli oggetti DOM e finestra senza aprire visivamente una nuova finestra o scheda. L'API Offscreen supporta questi casi d'uso aprendo e chiudendo i documenti non visualizzati pacchettizzati con l'estensione, senza interrompere l'esperienza utente. Ad eccezione del passaggio di messaggi, i documenti fuori schermo non condividono le API con altri contesti di estensione, ma funzionano come pagine web complete con cui le estensioni possono interagire.

Per utilizzare l'API Offscreen, crea un documento fuori schermo dal service worker.

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

Nel documento fuori schermo, esegui qualsiasi azione che in precedenza avresti eseguito in uno script in background. Ad esempio, potresti copiare il testo selezionato nella pagina host.

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

Comunica tra i documenti fuori schermo e i service worker delle estensioni utilizzando il passaggio di messaggi.

Converti localStorage in un altro tipo

L'interfaccia Storage della piattaforma web (accessibile da window.localStorage) non può essere utilizzata in un service worker. Per risolvere il problema, puoi procedere in due modi. Innanzitutto, puoi sostituirlo con chiamate a un altro meccanismo di archiviazione. Lo spazio dei nomi browser.storage.local è adatto alla maggior parte dei casi d'uso, ma sono disponibili altre opzioni.

Puoi anche spostare le chiamate in un documento fuori schermo. Ad esempio, per eseguire la migrazione dei dati precedentemente archiviati in localStorage a un altro meccanismo:

  1. Crea un documento fuori schermo con una routine di conversione e un gestore runtime.onMessage.
  2. Aggiungi una routine di conversione al documento fuori schermo.
  3. Nel service worker dell'estensione, controlla browser.storage per i tuoi dati.
  4. Se i dati non vengono trovati, crea un documento fuori schermo e chiama runtime.sendMessage() per avviare la routine di conversione.
  5. Nel gestore runtime.onMessage che hai aggiunto al documento fuori schermo, chiama la routine di conversione.

Esistono anche alcune sfumature nel funzionamento delle API di archiviazione web nelle estensioni. Scopri di più in Spazio di archiviazione e cookie.

Registra i listener in modo sincrono

La registrazione di un listener in modo asincrono (ad esempio all'interno di una promise o di un callback) non è garantita in Manifest V3. Considera il seguente codice.

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

Questo funziona con una pagina di sfondo persistente perché la pagina è costantemente in esecuzione e non viene mai reinizializzata. In Manifest V3, il service worker verrà reinizializzato quando l'evento viene inviato. Ciò significa che quando l'evento viene attivato, i listener non verranno registrati (poiché vengono aggiunti in modo asincrono) e l'evento non verrà rilevato.

Sposta invece la registrazione del listener di eventi al livello superiore dello script. In questo modo, Chrome sarà in grado di trovare e richiamare immediatamente il gestore dei clic dell'azione, anche se l'estensione non ha terminato l'esecuzione della logica di avvio.

browser.action.onClicked.addListener(handleActionClick);

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

Sostituisci XMLHttpRequest() con fetch() globale

XMLHttpRequest() non può essere chiamato da un service worker, un'estensione o altro. Sostituisci le chiamate dallo script in background a XMLHttpRequest() con chiamate a 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);

Stati di persistenza

I service worker sono temporanei, il che significa che probabilmente verranno avviati, eseguiti e terminati ripetutamente durante la sessione del browser di un utente. Significa anche che i dati non sono immediatamente disponibili nelle variabili globali, poiché il contesto precedente è stato eliminato. Per ovviare a questo problema, utilizza le API Storage come fonte di attendibilità. Un esempio mostrerà come fare.

L'esempio seguente utilizza una variabile globale per archiviare un nome. In un service worker, questa variabile potrebbe essere reimpostata più volte nel corso della sessione del browser di un utente.

Script in background di 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 });
});

Per Manifest V3, sostituisci la variabile globale con una chiamata all'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 });
});

Converti i timer in sveglie

È comune utilizzare operazioni ritardate o periodiche utilizzando i metodi setTimeout() o setInterval(). Tuttavia, queste API possono non funzionare nei service worker, perché i timer vengono annullati ogni volta che il service worker viene terminato.

Script in background di Manifest V2
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  browser.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

Utilizza invece l'API Alarms. Come per gli altri listener, i listener di sveglia devono essere registrati nel livello superiore dello 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(),
  });
});

Mantieni attivo il service worker

I service worker sono per definizione basati sugli eventi e vengono terminati in caso di inattività. In questo modo Chrome può ottimizzare le prestazioni e il consumo di memoria della tua estensione. Scopri di più nella nostra documentazione sul ciclo di vita dei service worker. In casi eccezionali, potrebbero essere necessarie misure aggiuntive per garantire che un service worker rimanga attivo per un periodo di tempo più lungo.

Mantieni attivo un service worker fino al termine di un'operazione a lunga esecuzione

Durante le operazioni dei service worker a lunga esecuzione che non chiamano le API di estensione, il service worker potrebbe arrestarsi a metà operazione. Ecco alcuni esempi:

  • Una richiesta fetch() che potrebbe richiedere più di cinque minuti (ad esempio un download di grandi dimensioni su una connessione potenzialmente scadente).
  • Un calcolo asincrono complesso che richiede più di 30 secondi.

Per estendere la durata del service worker in questi casi, puoi chiamare periodicamente un'API di estensione semplice per reimpostare il contatore del timeout. Tieni presente che questa opzione è riservata solo a casi eccezionali e nella maggior parte delle situazioni esiste un modo migliore e idiomatico per la piattaforma per ottenere lo stesso risultato.

L'esempio seguente mostra una funzione helper waitUntil() che mantiene attivo il service worker fino alla risoluzione di una determinata promise:

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

waitUntil(someExpensiveCalculation());

Mantieni un service worker attivo continuamente

In rari casi, è necessario estendere la durata a tempo indeterminato. Abbiamo identificato le aziende e l'istruzione come i casi d'uso più importanti e consentiamo specificamente questa operazione, ma non la supportiamo in generale. In queste circostanze eccezionali, è possibile mantenere attivo un service worker chiamando periodicamente un'API di estensione di base. È importante sottolineare che questo consiglio si applica solo alle estensioni in esecuzione su dispositivi gestiti per casi d'uso aziendali o didattici. Non è consentito in altri casi e il team delle estensioni di Chrome si riserva il diritto di intraprendere azioni contro queste estensioni in futuro.

Utilizza il seguente snippet di codice per mantenere attivo il service worker:

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