Service worker multiorigine - Esperimento con il recupero estero

Sfondo

I service worker offrono agli sviluppatori web la possibilità di rispondere alle richieste di rete effettuate dalle loro applicazioni web, consentendo loro di continuare a lavorare anche offline, di combattere il lie-fi e di implementare complesse interazioni con la cache come stale-while-revalidate. Tuttavia, i service worker sono storicamente legati a un'origine specifica: in qualità di proprietario di un'app web, è tua responsabilità scrivere ed eseguire il deployment di un service worker per intercettare tutte le richieste di rete effettuate dalla tua app web. In questo modello, ogni service worker è responsabile della gestione anche delle richieste multiorigine, ad esempio quelle relative a un'API di terze parti o ai caratteri web.

E se un fornitore di terze parti di un'API, di caratteri web o di un altro servizio di uso comune avesse il potere di implementare il proprio service worker che avesse la possibilità di gestire le richieste effettuate da altre origini alla propria? I provider potevano implementare la propria logica di networking personalizzata e sfruttare un'unica istanza cache autorevole per l'archiviazione delle risposte. Ora, grazie al recupero esterno, questo tipo di implementazione di worker di servizio di terze parti è una realtà.

Il deployment di un worker di servizio che implementa il recupero esterno è utile per qualsiasi fornitore di un servizio a cui si accede tramite richieste HTTPS dai browser. Basta pensare a scenari in cui potresti fornire una versione del tuo servizio indipendente dalla rete, in cui i browser potrebbero sfruttare una cache di risorse comune. I servizi che potrebbero trarre vantaggio da questa funzionalità includono, a titolo esemplificativo:

  • Provider di API con interfacce RESTful
  • Fornitori di caratteri web
  • Provider di analisi dei dati
  • Provider di hosting di immagini
  • Reti CDN (Content Delivery Network) generiche

Immagina, ad esempio, di essere un fornitore di servizi di analisi. Se esegui il deployment di un worker del servizio di recupero esterno, puoi assicurarti che tutte le richieste al tuo servizio che non vanno a buon fine mentre un utente è offline vengano messe in coda e riprodotte quando la connettività viene ripristinata. Sebbene sia possibile per i client di un servizio implementare un comportamento simile tramite i service worker proprietari, richiedere a ogni singolo client di scrivere una logica su misura per il servizio non è scalabile quanto affidarsi a un service worker di recupero esterno condiviso di cui esegui il deployment.

Prerequisiti

Token prova dell'origine

Il recupero straniero è ancora considerato sperimentale. Per evitare di integrare prematuramente questo design prima che venga completamente specificato e concordato dai fornitori di browser, è stato implementato in Chrome 54 come prova dell'origine. Finché il recupero esterno resta sperimentale, per utilizzare questa nuova funzionalità con il servizio che ospiti dovrai richiedere un token che abbia come ambito l'origine specifica del tuo servizio. Il token deve essere incluso come intestazione di risposta HTTP in tutte le richieste cross-origin per le risorse che vuoi gestire tramite il recupero esterno, nonché nella risposta per la risorsa JavaScript del tuo service worker:

Origin-Trial: token_obtained_from_signup

Il periodo di prova terminerà a marzo 2017. Entro quel momento, ci aspettiamo di aver individuato le eventuali modifiche necessarie per stabilizzare la funzionalità e (ci auguriamo) di averla attivata per impostazione predefinita. Se il recupero da origini esterne non è abilitato per impostazione predefinita entro questa data, la funzionalità associata ai token di prova dell'origine esistenti non funzionerà più.

Per facilitare la sperimentazione del recupero da origini esterne prima di registrarti per un token di prova di Origin ufficiale, puoi bypassare il requisito in Chrome per il tuo computer locale andando a chrome://flags/#enable-experimental-web-platform-features e attivando il flag "Funzionalità della piattaforma web sperimentale". Tieni presente che questa operazione deve essere eseguita in ogni istanza di Chrome che vuoi utilizzare nelle sperimentazioni locali, mentre con un token di prova Origin la funzionalità sarà disponibile per tutti gli utenti di Chrome.

HTTPS

Come per tutti gli implementazioni di service worker, è necessario accedere al server web utilizzato per la pubblicazione delle risorse e dello script del service worker tramite HTTPS. Inoltre, l'intercettazione del recupero esterno si applica solo alle richieste provenienti da pagine ospitate su origini sicure, pertanto i client del tuo servizio devono utilizzare HTTPS per sfruttare l'implementazione del recupero esterno.

Utilizzo di recupero esterno

Una volta rimossi i prerequisiti, diamo un'occhiata ai dettagli tecnici necessari per configurare e avviare un worker del servizio di recupero esterno.

Registrazione del service worker

La prima sfida che probabilmente dovrai affrontare è come registrare il tuo service worker. Se hai già lavorato con i service worker, probabilmente conosci quanto segue:

// You can't do this!
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
}

Questo codice JavaScript per la registrazione di un worker di servizio proprietario ha senso nel contesto di un'app web, attivato da un utente che accede a un URL sotto il tuo controllo. Tuttavia, non è un approccio valido per registrare un worker di servizio di terze parti, quando l'unica interazione del browser con il tuo server è la richiesta di una risorsa secondaria specifica, non una navigazione completa. Se il browser richiede, ad esempio, un'immagine da un server CDN di tua gestione, non puoi anteporre lo snippet di JavaScript alla risposta e aspettarti che venga eseguito. È necessario un metodo diverso di registrazione del worker di servizio, al di fuori del normale contesto di esecuzione di JavaScript.

La soluzione si presenta sotto forma di un'intestazione HTTP che il server può includere in qualsiasi risposta:

Link: </service-worker.js>; rel="serviceworker"; scope="/"

Analizziamo l'intestazione di esempio nei suoi componenti, ciascuno separato da un carattere ;.

  • </service-worker.js> è obbligatorio e viene utilizzato per specificare il percorso del file del tuo worker di servizio (sostituisci /service-worker.js con il percorso appropriato dello script). Corrisponde direttamente alla stringa scriptURL che verrebbe altrimenti passata come primo parametro a navigator.serviceWorker.register(). Il valore deve essere racchiuso tra caratteri <> (come richiesto dalla specifica dell'intestazione Link) e, se viene fornito un URL relativo anziché assoluto, verrà interpretato come relativo alla posizione della risposta.
  • È inoltre obbligatorio specificare il campo rel="serviceworker", che deve essere incluso senza necessità di personalizzazione.
  • scope=/ è una dichiarazione di ambito facoltativa, equivalente alla stringa options.scope che puoi passare come secondo parametro a navigator.serviceWorker.register(). Per molti casi d'uso, va bene con l'utilizzo dell'ambito predefinito, quindi non esitare a escluderlo, a meno che tu non sappia di averne bisogno. Le stesse limitazioni relative all'ambito massimo consentito, insieme alla possibilità di allentarle tramite l'intestazione Service-Worker-Allowed, si applicano alle registrazioni delle intestazioni Link.

Come per una registrazione "tradizionale" del servizio worker, l'utilizzo dell'intestazione Link consente di installare un servizio worker che verrà utilizzato per la prossima richiesta effettuata in base all'ambito registrato. Il corpo della risposta che include l'intestazione speciale verrà utilizzato così com'è e sarà disponibile immediatamente per la pagina, senza attendere il completamento dell'installazione da parte dell'operatore di servizi esterno.

Ricorda che il recupero esterno è attualmente implementato come prova dell'origine, quindi accanto all'intestazione della risposta al link dovrai includere anche un'intestazione Origin-Trial valida. L'insieme minimo di intestazioni di risposta da aggiungere per registrare il tuo worker del servizio di recupero esterno è

Link: </service-worker.js>; rel="serviceworker"
Origin-Trial: token_obtained_from_signup

Debug della registrazione

Durante lo sviluppo, ti consigliamo di verificare che il Service worker di recupero esterno sia installato correttamente e che stia elaborando le richieste. Ci sono alcune cose che puoi controllare negli Strumenti per sviluppatori di Chrome per verificare che tutto funzioni come previsto.

Vengono inviate le intestazioni di risposta appropriate?

Per registrare il worker del servizio di recupero esterno, devi impostare un'intestazione Link in una risposta a una risorsa ospitata sul tuo dominio, come descritto in precedenza in questo post. Durante il periodo di prova dell'origine, e supponendo che non sia impostato chrome://flags/#enable-experimental-web-platform-features, devi impostare anche un'intestazione della risposta Origin-Trial. Puoi verificare che il tuo server web stia impostando queste intestazioni osservando la voce nel riquadro Network di DevTools:

Intestazioni visualizzate nel riquadro Rete.

Il service worker di recupero dati esterni è registrato correttamente?

Puoi anche confermare la registrazione del servizio worker sottostante, incluso il relativo ambito, esaminando l'elenco completo dei service worker nel riquadro Applicazione di DevTools. Assicurati di selezionare l'opzione "Mostra tutto", poiché per impostazione predefinita vengono visualizzati solo i worker per l'origine corrente.

Il service worker di recupero esterno nel riquadro Applicazioni.

Gestore eventi di installazione

Ora che hai registrato il service worker di terze parti, questo avrà la possibilità di rispondere agli eventi install e activate, proprio come farebbe qualsiasi altro service worker. Può sfruttare questi eventi, ad esempio, per compilare le cache con le risorse richieste durante l'evento install o per eliminare le cache obsolete nell'evento activate.

Oltre alle normali attività di memorizzazione nella cache degli eventi install, è necessario un passaggio aggiuntivo obbligatorio all'interno del gestore eventi install del tuo worker di servizio di terze parti. Il codice deve chiamare registerForeignFetch(), come nell'esempio seguente:

self.addEventListener('install', event => {
    event.registerForeignFetch({
    scopes: [self.registration.scope], // or some sub-scope
    origins: ['*'] // or ['https://example.com']
    });
});

Esistono due opzioni di configurazione, entrambe obbligatorie:

  • scopes accetta un array di una o più stringhe, ciascuna delle quali rappresenta un ambito per le richieste che attiveranno un evento foreignfetch. Ma aspetta, potresti pensare, ho già definito un ambito durante la registrazione del service worker. È vero e questo ambito complessivo è ancora pertinente: ogni ambito specificato qui deve essere uguale all'ambito complessivo del worker di servizio o un sottoambito. Le ulteriori limitazioni dell'ambito qui ti consentono di eseguire il deployment di un service worker per tutti gli usi in grado di gestire sia gli eventi fetch proprietari (per le richieste effettuate dal tuo sito) sia gli eventi foreignfetch di terze parti (per le richieste effettuate da altri domini) e chiarire che solo un sottoinsieme dell'ambito più ampio dovrebbe attivare foreignfetch. In pratica, se esegui il deployment di un service worker dedicato alla gestione solo di eventi foreignfetch di terze parti, utilizzerai un singolo ambito esplicito uguale a quello complessivo del service worker. Questo è ciò che fa l'esempio precedente, utilizzando il valore self.registration.scope.
  • Inoltre, origins accetta un array di una o più stringhe e consente di limitare il gestore foreignfetch in modo che risponda solo alle richieste provenienti da domini specifici. Ad esempio, se consenti esplicitamente "https://example.com", una richiesta effettuata da una pagina ospitata all'indirizzo https://example.com/path/to/page.html per una risorsa pubblicata dal tuo ambito di recupero esterno attiverà il gestore del recupero esterno, ma le richieste effettuate da https://random-domain.com/path/to/page.html non attiveranno il gestore. A meno che tu non abbia un motivo specifico per attivare la logica di recupero esterna solo per un sottoinsieme di origini remote, puoi semplicemente specificare '*' come unico valore nell'array e saranno consentite tutte le origini.

Il gestore di eventi foreignfetch

Ora che hai installato il tuo worker di servizio di terze parti e l'hai configurato tramite registerForeignFetch(), avrà la possibilità di intercettare le richieste di risorse secondarie cross-origin al tuo server che rientrano nell'ambito del recupero esterno.

In un service worker proprietario tradizionale, ogni richiesta attivava un evento fetch a cui il service worker ha avuto la possibilità di rispondere. Al nostro worker di servizio di terze parti viene data la possibilità di gestire un evento leggermente diverso, denominato foreignfetch. Concettualmente, i due eventi sono abbastanza simili e ti offrono l'opportunità di esaminare la richiesta in arrivo e, facoltativamente, di fornire una risposta tramite respondWith():

self.addEventListener('foreignfetch', event => {
    // Assume that requestLogic() is a custom function that takes
    // a Request and returns a Promise which resolves with a Response.
    event.respondWith(
    requestLogic(event.request).then(response => {
        return {
        response: response,
        // Omit to origin to return an opaque response.
        // With this set, the client will receive a CORS response.
        origin: event.origin,
        // Omit headers unless you need additional header filtering.
        // With this set, only Content-Type will be exposed.
        headers: ['Content-Type']
        };
    })
    );
});

Nonostante le somiglianze concettuali, ci sono alcune differenze nella pratica quando si chiama respondWith() su un ForeignFetchEvent. Anziché fornire semplicemente un Response (o Promise che si risolve in un Response) a respondWith(), come fai con un FetchEvent, devi passare un Promise che si risolve in un oggetto con proprietà specifiche al respondWith() di ForeignFetchEvent:

  • response è obbligatorio e deve essere impostato sull'oggetto Response che verrà restituito al client che ha effettuato la richiesta. Se fornisci un valore diverso da un Response valido, la richiesta del client verrà interrotta con un errore di rete. A differenza di quando chiami respondWith() all'interno di un gestore di eventi fetch, devi fornire un Response qui, non un Promise che si risolve con un Response. Puoi costruire la tua risposta tramite una catena di promesse e passare questa catena come parametro all'elemento respondWith() di foreignfetch, ma la catena deve risolversi con un oggetto contenente la proprietà response impostata su un oggetto Response. Puoi vedere una dimostrazione di questo nell'esempio di codice riportato sopra.
  • origin è facoltativo e viene utilizzato per determinare se la risposta restituita è opaca. Se non lo specifichi, la risposta sarà opaca e il client avrà accesso limitato al corpo e alle intestazioni della risposta. Se la richiesta è stata effettuata con mode: 'cors', la restituzione di una risposta opaca verrà trattata come un errore. Tuttavia, se specifichi un valore di stringa uguale all'origine del client remoto (che può essere ottenuto tramite event.origin), attivi esplicitamente la fornitura di una risposta abilitata CORS al client.
  • Anche headers è facoltativo ed è utile solo se specifichi anche origin e restituisci una risposta CORS. Per impostazione predefinita, solo le intestazioni nell'elenco di intestazioni delle risposte sicure CORS saranno incluse nella risposta. Se devi filtrare ulteriormente i risultati restituiti, headers accetta un elenco di uno o più nomi di intestazioni e lo utilizzerà come lista consentita delle intestazioni da esporre nella risposta. In questo modo puoi attivare CORS impedendo al contempo l'esposizione diretta al client remoto delle intestazioni di risposta potenzialmente sensibili.

È importante notare che, quando viene eseguito, il gestore foreignfetch ha accesso a tutte le credenziali e ad ambient Authority dell'origine che ospita il service worker. In qualità di sviluppatore che esegue il deployment di un worker di servizio con recupero di dati esterni abilitato, è tua responsabilità assicurarti di non divulgare dati di risposta privilegiati che altrimenti non sarebbero disponibili in virtù di queste credenziali. Richiedere l'attivazione per le risposte CORS è un passaggio per limitare l'esposizione involontaria, ma in qualità di sviluppatore puoi inviare esplicitamente richieste fetch() all'interno del gestore foreignfetch che non utilizzano le credenziali implicite tramite:

self.addEventListener('foreignfetch', event => {
    // The new Request will have credentials omitted by default.
    const noCredentialsRequest = new Request(event.request.url);
    event.respondWith(
    // Replace with your own request logic as appropriate.
    fetch(noCredentialsRequest)
        .catch(() => caches.match(noCredentialsRequest))
        .then(response => ({response}))
    );
});

Considerazioni per i clienti

Esistono alcune considerazioni aggiuntive che influiscono sul modo in cui il tuo worker del servizio di recupero dati esterni gestisce le richieste effettuate dai clienti del tuo servizio.

Clienti che hanno il proprio worker di servizio proprietario

Alcuni clienti del tuo servizio potrebbero avere già il proprio servizio worker proprietario che gestisce le richieste provenienti dalla loro app web. Cosa significa questo per il tuo servizio worker di recupero di terze parti esterno?

Gli handler fetch in un worker di servizio proprietario hanno la prima opportunità di rispondere a tutte le richieste effettuate dall'app web, anche se è presente un worker di servizio di terze parti con foreignfetch abilitato con un ambito che copre la richiesta. Tuttavia, i client con worker di servizio proprietari possono comunque usufruire del tuo worker di servizio di recupero esterno.

All'interno di un service worker proprietario, l'uso di fetch() per recuperare risorse multiorigine attiva il service worker di recupero esterno appropriato. Ciò significa che il codice seguente può sfruttare l'handler foreignfetch:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    // If event.request is under your foreign fetch service worker's
    // scope, this will trigger your foreignfetch handler.
    event.respondWith(fetch(event.request));
});

Allo stesso modo, se esistono gestori di recupero proprietari che non chiamano event.respondWith() durante la gestione delle richieste per la risorsa multiorigine, la richiesta verrà automaticamente indirizzata al gestore foreignfetch:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    if (event.request.mode === 'same-origin') {
    event.respondWith(localRequestLogic(event.request));
    }

    // Since event.respondWith() isn't called for cross-origin requests,
    // any foreignfetch handlers scoped to the request will get a chance
    // to provide a response.
});

Se un gestore fetch proprietario chiama event.respondWith(), ma non utilizza fetch() per richiedere una risorsa nell'ambito del tuo ambito di recupero esterno, il tuo worker del servizio di recupero esterno non avrà la possibilità di gestire la richiesta.

Clienti che non dispongono del proprio servizio worker

Tutti i client che inviano richieste a un servizio di terze parti possono trarre vantaggio dal deployment di un worker del servizio di recupero esterno, anche se non utilizzano già il proprio worker del servizio. I client non devono fare nulla di specifico per attivare l'utilizzo di un worker del servizio di recupero dati esterno, a condizione che utilizzino un browser che lo supporti. Ciò significa che, se esegui il deployment di un worker del servizio di recupero esterno, la logica di richiesta personalizzata e la cache condivisa saranno immediatamente vantaggiose per molti dei clienti del tuo servizio, senza che debbano eseguire ulteriori passaggi.

Riepilogo: dove i clienti cercano una risposta

Tenendo conto delle informazioni riportate sopra, possiamo assemblare una gerarchia di origini che un client utilizzerà per trovare una risposta a una richiesta cross-origin.

  1. L'handler fetch di un worker di servizio proprietario (se presente)
  2. L'handler foreignfetch di un worker di servizio di terze parti (se presente e solo per le richieste cross-origin)
  3. La cache HTTP del browser (se esiste una nuova risposta)
  4. La rete

Il browser inizia dall'alto e, a seconda dell'implementazione del servizio worker, continua a scendere nell'elenco finché non trova una fonte per la risposta.

Scopri di più

Non perderti nulla

L'implementazione della prova dell'origine del recupero estera da parte di Chrome è soggetta a modifiche man mano che rispondiamo ai feedback degli sviluppatori. Terremo aggiornato questo post tramite le modifiche in linea e prenderemo nota delle modifiche specifiche indicate di seguito man mano che vengono apportate. Condivideremo inoltre informazioni sui cambiamenti più importanti tramite l'account Twitter @chromiumdev.