Skrypty service worker z innych domen – eksperymenty z pobieraniem z innych domen

Tło

Usługa w tle umożliwia deweloperom stron internetowych reagowanie na żądania sieciowe wysyłane przez ich aplikacje internetowe, co pozwala im pracować nawet w trybie offline, zwalczać problemy z siecią LTE („lie-fi”) oraz wdrażać złożone interakcje z pamięcią podręcznej, takie jak sprawdzanie ważności w trybie „stale-while-revalidate”. Jednak skrypty service worker były do tej pory powiązane z konkretnym źródłem. Jako właściciel aplikacji internetowej musisz napisać i wdrażać skrypt service worker, który przechwytuje wszystkie żądania sieciowe wysyłane przez Twoją aplikację internetową. W tym modelu każdy serwis worker odpowiada za obsługę żądań między domenami, na przykład do interfejsu API innej firmy lub do czcionek internetowych.

Co by się stało, gdyby zewnętrzny dostawca interfejsu API, czcionek internetowych lub innej powszechnie używanej usługi mógł wdrożyć własnego pracownika usługi, który mógłby obsługiwać żądania wysyłane przez inne źródła do ich źródła? Dostawcy mogliby stosować własną niestandardową logikę sieciową i korzystać z jednego wiarygodnego elementu pamięci podręcznej do przechowywania odpowiedzi. Teraz, dzięki zapytaniu zewnętrznemu, tego typu wdrażanie usług innych firm jest możliwe.

Wdrożenie skryptu service worker, który implementuje pobieranie obce, ma sens w przypadku każdego dostawcy usługi, do którego dostęp jest uzyskiwany za pomocą żądań HTTPS z przeglądarek. Zastanów się tylko, czy możesz udostępnić wersję usługi niezależną od sieci, w której przeglądarki mogłyby skorzystać ze wspólnej pamięci podręcznej zasobów. Usługi, które mogą korzystać z tych funkcji, to m.in.:

  • Dostawcy interfejsów API z interfejsami RESTful
  • Dostawcy czcionek internetowych
  • Dostawcy usług analitycznych
  • Dostawcy usług hostingu obrazów
  • Ogólne sieci dostarczania treści

Wyobraź sobie na przykład, że jesteś dostawcą usług analitycznych. Wdrażając obcy mechanizm roboczy usługi pobierania, możesz zapewnić, że wszystkie żądania wysyłane do Twojej usługi, które kończyć się niepowodzeniem, gdy użytkownik jest offline, będą w kolejce i odtwarzane ponownie po powrocie połączenia. Mimo że klienty danej usługi implementują podobne działanie za pomocą własnych mechanizmów Service Worker, wymaganie od każdego klienta napisania własnej logiki dla usługi nie jest tak skalowalne, jak korzystanie ze współużytkowanego obcego skryptu usługi pobierania, który wdrożysz.

Wymagania wstępne

Token wersji próbnej Origin

Pobieranie zagranicznych treści jest nadal uznawane za eksperymentalne. Aby uniknąć przedwczesnego wdrożenia tej funkcji, zanim zostanie ona w pełni określona i zaakceptowana przez dostawców przeglądarek, została ona zaimplementowana w Chrome 54 jako próbna implementacja Origin. Dopóki funkcja pobierania z zewnętrznego źródła jest nadal w fazie eksperymentalnej, aby korzystać z niej w usłudze, którą hostujesz, musisz poprosić o token ograniczony do konkretnego źródła usługi. Token należy podawać jako nagłówek odpowiedzi HTTP we wszystkich żądaniach z innych domen dotyczących zasobów, które mają być obsługiwane za pomocą obcego pobierania, oraz w odpowiedzi dla zasobu JavaScript mechanizmu Service Worker:

Origin-Trial: token_obtained_from_signup

Okres próbny kończy się w marcu 2017 r. Spodziewamy się, że wprowadzimy zmiany niezbędne do ustabilizowania funkcji i (miamy nadzieję) włączenia jej domyślnie. Jeśli pobieranie obcych nie będzie do tego czasu domyślnie włączone, funkcje powiązane z istniejącymi tokenami wersji próbnej origin przestaną działać.

Aby ułatwić eksperymentowanie z obsługą zewnętrznych danych przed zarejestrowaniem się w oficjalnym programie Origin Trial, możesz pominąć ten wymóg w Chrome na komputerze lokalnym. W tym celu otwórz chrome://flags/#enable-experimental-web-platform-features i włącz flagę „Experimental Web Platform features” (Eksperymentalne funkcje platformy internetowej). Pamiętaj, że trzeba to zrobić w każdym wystąpieniu Chrome, którego chcesz używać w lokalnych eksperymentach. Z kolei w ramach wersji próbnej origin token będzie dostępny dla wszystkich użytkowników Chrome.

HTTPS

Podobnie jak w przypadku wszystkich usług wdrożeń, serwer internetowy, który służy do obsługi zasobów i skryptu usługi musi być dostępny przez HTTPS. Ponadto przechwytywanie pobierania z innych źródeł dotyczy tylko żądań pochodzących ze stron hostowanych w bezpiecznych źródłach, więc klienci Twojej usługi muszą używać HTTPS, aby korzystać z implementacji pobierania z innych źródeł.

Korzystanie z obsługi zewnętrznej

Po zapoznaniu się z wymaganiami wstępnymi przyjrzyjmy się szczegółom technicznym, które są potrzebne do uruchomienia usługi pobierania z zewnętrznego źródła.

Rejestrowanie skryptu service worker

Pierwszym problemem, z którym się prawdopodobnie zetkniesz, będzie rejestracja pracownika usługi. Jeśli korzystasz z usług, prawdopodobnie wiesz, że:

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

Ten kod JavaScriptu służący do rejestracji własnego serwisu workera ma sens w kontekście aplikacji internetowej, która jest wywoływana przez użytkownika, gdy ten przechodzi do kontrolowanego przez Ciebie adresu URL. Nie jest to jednak odpowiednie podejście do rejestrowania usługi workera u usługodawcy zewnętrznego, gdy jedyną interakcją przeglądarki z serwerem jest żądanie określonego podzasobu, a nie pełna nawigacja. Jeśli przeglądarka żąda np. obrazu z serwera CDN, którego jesteś właścicielem, nie możesz dodać do odpowiedzi tego fragmentu kodu JavaScript i spodziewać się, że zostanie on wykonany. Wymaga to innej metody rejestracji usługi niż w ramach zwykłego kontekstu wykonywania JavaScriptu.

Rozwiązanie polega na dodaniu do odpowiedzi nagłówka HTTP, który serwer może zawierać w dowolnej odpowiedzi:

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

Podzielmy ten przykładowy nagłówek na jego komponenty, z których każdy jest oddzielony znakiem ;.

  • Argument </service-worker.js> jest wymagany i służy do określenia ścieżki do pliku usługi (zastąp /service-worker.js odpowiednią ścieżką do skryptu). Odpowiada to bezpośrednio ciągowi znaków scriptURL, który w innym przypadku byłby przekazywany jako pierwszy parametr funkcji navigator.serviceWorker.register(). Wartość musi być ujęta w znaki <> (zgodnie z specyfikacją nagłówka Link), a jeśli podany jest adres URL względny, a nie bezwzględny, zostanie on zinterpretowany jako względny względem lokalizacji odpowiedzi.
  • rel="serviceworker" jest też wymagany i powinien być uwzględniony bez konieczności dostosowania.
  • scope=/ to opcjonalna deklaracja zakresu odpowiadający ciągowi znaków options.scope, który możesz przekazać jako drugi parametr do funkcji navigator.serviceWorker.register(). W wielu przypadkach domyślny zakres jest prawidłowy, więc możesz go pominąć, jeśli nie wiesz, że jest Ci potrzebny. Rejestracje nagłówków Link podlegają tym samym ograniczeniom dotyczącym maksymalnego zakresu dozwolonego oraz możliwości złagodzenia tych ograniczeń za pomocą nagłówka Service-Worker-Allowed.

Podobnie jak w przypadku „tradycyjnej” rejestracji pracownika usługi, użycie nagłówka Link spowoduje zainstalowanie pracownika usługi, który będzie używany do następnego żądania wysłanego do zarejestrowanego zakresu. Treść odpowiedzi, która zawiera specjalny nagłówek, zostanie użyta bez zmian i będzie dostępna dla strony natychmiast, bez oczekiwania na zakończenie instalacji przez zewnętrznego pracownika usługi.

Pamiętaj, że pobieranie z innego źródła jest obecnie wdrażane w ramach testowania origin, więc oprócz nagłówka odpowiedzi Link musisz też umieścić prawidłowy nagłówek Origin-Trial. Minimalny zestaw nagłówków odpowiedzi, które należy dodać, aby zarejestrować element worker usługi pobierania z zewnątrz, to:

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

Debugowanie rejestracji

Podczas tworzenia aplikacji warto sprawdzić, czy usługa pobierania z zewnętrznego źródła jest prawidłowo zainstalowana i przetwarza żądania. Aby sprawdzić, czy wszystko działa zgodnie z oczekiwaniami, możesz sprawdzić kilka rzeczy w Narzędziach deweloperskich w Chrome.

Czy wysyłane są prawidłowe nagłówki odpowiedzi?

Aby zarejestrować zewnętrznego pracownika usługi pobierania, musisz ustawić nagłówek Link w odpowiedzi na zasób hostowany w Twojej domenie, tak jak opisano wcześniej w tym poście. W okresie próbnym origin i zakładając, że nie masz ustawionej chrome://flags/#enable-experimental-web-platform-features, musisz też ustawić nagłówek odpowiedzi Origin-Trial. Aby sprawdzić, czy Twój serwer WWW ustawia te nagłówki, sprawdź wpis w panelu Sieć w Narzędziach deweloperskich:

Nagłówki wyświetlane w panelu Sieć.

Czy skrypt service worker pobierania z zewnętrznego źródła jest prawidłowo zarejestrowany?

Możesz też sprawdzić podstawową rejestrację usługi workera, w tym jej zakres, przeglądając pełną listę usług workera w panelu aplikacji w DevTools. Pamiętaj, aby wybrać opcję „Pokaż wszystkie”, ponieważ domyślnie zobaczysz tylko service workery dla bieżącego źródła.

Obcy skrypt service worker w panelu Aplikacje.

Moduł obsługi zdarzenia instalacji

Po zarejestrowaniu zewnętrznego skryptu service worker będzie można reagować na zdarzenia install i activate, tak jak w przypadku każdego innego skryptu service worker. Może ona korzystać z tych zdarzeń, aby na przykład wypełniać pamięci podręczne wymaganymi zasobami podczas zdarzenia install lub usuwać nieaktualne pamięci podręczne podczas zdarzenia activate.

Oprócz standardowych działań związanych z przechowywaniem w pamięci podręcznej zdarzeń install jest jeszcze wymagany dodatkowy krok w obsługującym zdarzenie install modułu usługi w ramach usługi wtyczki internetowej. Twój kod musi wywołać funkcję registerForeignFetch(), jak w tym przykładzie:

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

Dostępne są 2 opcje konfiguracji:

  • Funkcja scopes przyjmuje tablicę co najmniej 1 ciągu tekstowego, z których każdy reprezentuje zakres żądań, które będą uruchamiać zdarzenie foreignfetch. Ale zaczekaj, może myślisz, że zakres został już zdefiniowany podczas rejestracji skryptu service worker Zgadza się. Ten ogólny zakres jest nadal istotny. Każdy zakres, który tutaj określisz, musi być równy ogólnemu zakresowi lub być podzbiorem ogólnego zakresu usługi. Dodatkowe ograniczenia zakresu, które znajdziesz tutaj, pozwalają wdrożyć uniwersalny mechanizm Service Worker, który obsługuje zarówno własne zdarzenia fetch (w przypadku żądań wysłanych z Twojej witryny), jak i zdarzeń foreignfetch innych firm (w przypadku żądań z innych domen). Dzięki temu masz jasne, że tylko podzbiór większego zakresu może aktywować foreignfetch. W praktyce, jeśli wdrażasz pracownika serwisowego przeznaczonego tylko do obsługi zdarzeń foreignfetch innych firm, użyj jednego, wyraźnego zakresu, który jest równy ogólnemu zakresowi pracownika serwisowego. Aby to zrobić, użyj wartości self.registration.scope w przykładzie powyżej.
  • Funkcja origins przyjmuje też tablicę zawierającą co najmniej 1 ciąg znaków i umożliwia ograniczenie działania modułu obsługi foreignfetch tylko do odpowiadania na żądania z określonych domen. Jeśli na przykład zezwolisz wyraźnie na dostęp do adresu „https://example.com”, żądanie wysłane ze strony hostowanej pod adresem https://example.com/path/to/page.html w przypadku zasobu wyświetlanego z obrębu pobierania z zewnętrznego źródła spowoduje uruchomienie modułu obsługi pobierania z zewnętrznego źródła, ale żądania wysłane z adresu https://random-domain.com/path/to/page.html nie uruchomią tego modułu. Jeśli nie masz konkretnego powodu, aby logikę pobierania z zewnętrznego źródła aktywować tylko w przypadku podzbioru źródeł zewnętrznych, możesz w prost podać wartość '*' jako jedyną wartość w tablicy, a w ten sposób wszystkie źródła będą dozwolone.

Moduł obsługi zdarzenia foreignfetch

Teraz, gdy masz zainstalowanego zewnętrznego pracownika usługi i został on skonfigurowany za pomocą registerForeignFetch(), może przechwytywać żądania podzasobów z innych źródeł wysyłane do Twojego serwera, które mieszczą się w zakresie pobierania z zewnętrznych źródeł.

W tradycyjnym własnym środowisku service worker każde żądanie wywołuje zdarzenie fetch, na które miał on odpowiedzieć. Nasz zewnętrzny pracownik usługi ma możliwość obsłużenia nieco innego zdarzenia o nazwie foreignfetch. Oba te zdarzenia są dość podobne i dają Ci możliwość sprawdzenia przychodzącego żądania oraz opcjonalnie udzielenia odpowiedzi za pomocą 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']
        };
    })
    );
});

Mimo podobieństw koncepcyjnych w praktyce występują pewne różnice w przypadku wywoływania respondWith() w ramach ForeignFetchEvent. Zamiast przekazywania respondWith() wartości Response (lub Promise, która jest mapowana na Response) tak jak w przypadku FetchEvent, musisz przekazać Promise, które jest mapowane na obiekt z określonymi właściwościami do respondWith() ForeignFetchEvent:

  • response jest wymagany i musi być ustawiony na obiekt Response, który zostanie zwrócony do klienta, który wysłał żądanie. Jeśli podasz coś innego niż prawidłowa wartość Response, żądanie klienta zostanie zakończone z błędem sieci. W odróżnieniu od wywołania funkcji respondWith() w modułach obsługi zdarzeń fetch musisz podać tutaj wartość Response, a nie Promise, która jest interpretowana jako Response. Odpowiedź możesz tworzyć za pomocą łańcucha obietnic i przekazywać go jako parametr do metody foreignfetch respondWith(), ale łańcuch musi zawierać obiekt z właściwością response ustawioną na obiekt Response. Przykładowy kod znajdziesz powyżej.
  • Parametr origin jest opcjonalny i służy do określania, czy zwracana odpowiedź jest przezroczysta. Jeśli tego nie zrobisz, odpowiedź będzie nieprzejrzysta, a klient będzie mieć ograniczony dostęp do treści i nagłówków odpowiedzi. Jeśli żądanie zostało wysłane z parametrem mode: 'cors', zwrócenie odpowiedzi bez przejrzystości zostanie potraktowane jako błąd. Jeśli jednak określisz wartość ciągu równą pochodzeniu klienta zdalnego (którą można uzyskać za pomocą interfejsu event.origin), wyraźnie wyrażasz zgodę na przekazywanie klientowi odpowiedzi z włączoną obsługą CORS.
  • Identyfikator headers jest też opcjonalny i przydaje się tylko wtedy, gdy podajesz też identyfikator origin i zwracasz odpowiedź CORS. Domyślnie w odpowiedzi będą uwzględniane tylko nagłówki z listy nagłówków odpowiedzi bezpiecznych w świetle CORS. Jeśli chcesz dodatkowo filtrować zwracane dane, możesz podać listę nazw nagłówków, które mają być uwzględnione w odpowiedzi. Dzięki temu możesz włączyć CORS, a jednocześnie uniemożliwić udostępnianie potencjalnie poufnych nagłówków odpowiedzi bezpośrednio klientowi zdalnym.

Pamiętaj, że gdy moduł obsługi foreignfetch jest uruchamiany, ma on dostęp do wszystkich danych uwierzytelniających i środowiskowych uprawnień pochodzenia, które obsługuje moduł service worker. Jako deweloper wdrażający element service worker z obsługą funkcji fetch w obrębie innego elementu usługi masz obowiązek zadbać o to, aby nie doszło do wycieku żadnych poufnych danych odpowiedzi, które nie byłyby dostępne na podstawie tych danych logowania. Wymóg wyrażenia zgody na odpowiedzi CORS to jeden z kroków do ograniczenia niezamierzonej ekspozycji na koronawirusa. Jako deweloper możesz jednak w swoim module obsługi foreignfetch wyraźnie wysyłać żądania fetch(), które nie używają domniemanych danych logowania za pomocą:

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

Informacje dla klienta

Występują dodatkowe kwestie, które mają wpływ na to, jak zagraniczny skrypt usługi pobierania obsługuje żądania wysyłane od klientów tej usługi.

Klienci, którzy mają własny skrypt service worker

Niektórzy klienci Twojej usługi mogą już mieć własne komponenty service worker po stronie klienta, które obsługują żądania pochodzące z ich aplikacji internetowych. Co to oznacza dla komponentu service worker po stronie obcej, który pobiera dane z usługi innej firmy?

Obsługa fetch w usługach własnych w ramach workera ma pierwszeństwo w reagowaniu na wszystkie żądania wysyłane przez aplikację internetową, nawet jeśli istnieje usługa zewnętrzna z obsługą foreignfetch, której zakres obejmuje żądanie. Klienci z własnymi pracownikami usługi mogą jednak nadal korzystać z pracownika usługi pobierania z innego źródła.

W skrypcie service worker własnym użycie funkcji fetch() do pobierania zasobów z innych źródeł spowoduje wywołanie odpowiedniego skryptu service worker do pobierania z innych źródeł. Oznacza to, że kod podobny do tego może korzystać z obsługi 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));
});

Podobnie, jeśli istnieją własne moduły obsługi pobierania, ale nie wywołują one funkcji event.respondWith() podczas obsługi żądań dotyczących zasobu w innej domenie, żądanie zostanie automatycznie przekazane do modułu obsługi 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.
});

Jeśli fetch handler firmy zewnętrznej wywołuje event.respondWith(), ale nie używa fetch() do żądania zasobu w zakresie pobierania z zewnętrznego źródła, skrypt service worker do pobierania z zewnętrznego źródła nie będzie miał możliwości obsłużenia żądania.

Klienci, którzy nie mają własnego serwisu

Wszyscy klienci wysyłający żądania do usługi zewnętrznej mogą korzystać z usług zewnętrznych, które wdrażają zewnętrznego pracownika usługi pobierania, nawet jeśli nie korzystają jeszcze z własnego pracownika usługi. Klienci nie muszą nic robić, aby zacząć korzystać z obsługiwanego przez zewnętrzny proces obsługiwany przez usługę, o ile używają przeglądarki, która ją obsługuje. Oznacza to, że po wdrożeniu zewnętrznego pracownika usługi pobierania Twoja niestandardowa logika żądania i współdzielona pamięć podręczna będą od razu dostępne dla wielu klientów usługi bez konieczności podejmowania przez nich dodatkowych działań.

Podsumowanie: gdzie klienci szukają odpowiedzi

Biorąc pod uwagę powyższe informacje, możemy utworzyć hierarchię źródeł, których klient będzie używać do znajdowania odpowiedzi na żądanie między domenami.

  1. fetch handler (jeśli występuje) skryptu service worker firmy zewnętrznej
  2. foreignfetchhandler (jeśli jest obecny i tylko w przypadku żądań między domenami) usługi wątek usługi zewnętrznej
  3. Pamięć podręczna HTTP przeglądarki (jeśli istnieje nowa odpowiedź)
  4. Sieć

Przeglądarka zaczyna od góry i w zależności od implementacji usługi roboczej będzie kontynuować przeglądanie listy, aż znajdzie źródło odpowiedzi.

Więcej informacji

Bądź na bieżąco

Implementacja w Chrome testowania origin z obsługą pobierania z innych domen może ulec zmianie w odpowiedzi na opinie programistów. Będziemy aktualizować ten post, wprowadzając zmiany w tekście, a także będziemy oznaczać konkretne zmiany poniżej w miarę ich wprowadzania. Informacje o istotnych zmianach będziemy też udostępniać na koncie @chromiumdev na Twitterze.