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

Wprowadzenie

Skrypty service worker pozwalają programistom stron internetowych odpowiadać na żądania sieciowe wysyłane przez ich aplikacje internetowe. Dzięki temu mogą pracować nawet w trybie offline, walczyć z lie-fi i wdrażać złożone interakcje z pamięci podręcznej, takie jak stale-pending-revalidate. Mechanizmy Service Worker były jednak w przeszłości powiązane z konkretnym źródłem – jako właściciel aplikacji internetowej Twoim obowiązkiem jest napisanie i wdrożenie skryptu service worker, który przechwytuje wszystkie żądania sieciowe wysyłane przez Twoją aplikację internetową. W tym modelu każdy skrypt service worker jest odpowiedzialny za obsługę nawet żądań z innych domen, np. do interfejsu API innej firmy lub czcionek internetowych.

Co, jeśli zewnętrzny dostawca interfejsu API, czcionek internetowych lub innej powszechnie używanej usługi miał możliwość wdrożenia własnego skryptu service worker, który miał możliwość obsługi żądań wysyłanych do źródła przez inne źródła? Dostawcy mogą zaimplementować własne niestandardowe mechanizmy logiczne dla sieci i wykorzystywać pojedynczą, autorytatywną instancję pamięci podręcznej do przechowywania odpowiedzi. Dzięki pobieraniu obcemu tego typu wdrożenia zewnętrznych mechanizmów Service Worker stają się rzeczywistością.

Wdrażanie skryptu service worker z implementacją pobierania obcego ma sens w przypadku każdego dostawcy usługi, do której dostęp jest uzyskiwany za pomocą żądań HTTPS z przeglądarek. Rozważ tylko scenariusze, w których możesz udostępnić niezależną od sieci wersję usługi, ponieważ przeglądarki mogą skorzystać ze wspólnej pamięci podręcznej zasobów. Przykładowe usługi, dla których może to być korzystne, to m.in.:

  • dostawców interfejsów API z interfejsami RESTful;
  • Dostawcy czcionek internetowych
  • Dostawcy usług analitycznych
  • Dostawcy hostingu obrazów
  • Sieci dostarczania treści ogólnych

Wyobraź sobie na przykład, że prowadzisz firmę oferującą narzędzia analityczne. Wdrażając obcy skrypt service worker, możesz mieć pewność, że wszystkie żądania wysyłane do usługi, które kończą się niepowodzeniem, gdy użytkownik jest w trybie offline, są umieszczane w kolejce i ponownie odtwarzane po powrocie połączenia. Mimo że klienty usługi implementowały podobne zachowanie za pomocą własnych mechanizmów Service Worker, to wymaganie od każdego klienta do zapisania dostosowanej logiki usługi nie jest tak skalowalne, jak w przypadku korzystania z udostępnianego obcojęzycznego mechanizmu pobierania, który został wdrożony przez Ciebie.

Wymagania wstępne

Token wersji próbnej origin

Pobieranie obce jest nadal uznawane za eksperymentalne. Aby zapobiec przedwczesnemu wypaleniu tego projektu, zanim zostanie on w pełni określony i uzgodniony przez dostawców przeglądarek, wdrożyliśmy go w Chrome 54 w ramach wersji próbnej origin. Dopóki pobieranie obce pozostaje w fazie eksperymentalnej, aby używać tej nowej funkcji w hostowanej przez siebie usłudze, musisz poprosić o token ograniczony do określonego źródła Twojej usługi. Token powinien być dołączany jako nagłówek odpowiedzi HTTP we wszystkich żądaniach z innych domen dotyczących zasobów, które chcesz obsłużyć za pomocą pobierania w obcym języku, a także w odpowiedzi dla zasobu JavaScript skryptu service worker:

Origin-Trial: token_obtained_from_signup

Okres próbny zakończy się w marcu 2017 r. Oczekujemy, że do tego czasu dokonamy wszelkich zmian niezbędnych do ustabilizowania działania tej funkcji i włączymy ją domyślnie. Jeśli pobieranie obce nie będzie do tego czasu włączone, funkcje powiązane z istniejącymi tokenami testowania origin przestaną działać.

Aby ułatwić eksperymentowanie z pobieraniem obcym przed zarejestrowaniem się do uzyskania oficjalnego tokena wersji próbnej, możesz pominąć wymaganie w Chrome na komputerze lokalnym. Aby to zrobić, otwórz chrome://flags/#enable-experimental-web-platform-features i włącz flagę „Eksperymentalne funkcje platformy internetowej”. Pamiętaj, że trzeba to zrobić w każdym wystąpieniu Chrome, którego chcesz używać w eksperymentach lokalnych. Natomiast w przypadku tokena wersji próbnej origin ta funkcja będzie dostępna dla wszystkich użytkowników Chrome.

HTTPS

Tak jak w przypadku wszystkich wdrożeń mechanizmu Service Worker, serwer WWW używany do obsługi zarówno zasobów, jak i skryptu service worker musi mieć dostęp przez HTTPS. Poza tym przechwytywanie pobierania obcego ma zastosowanie tylko do żądań pochodzących ze stron hostowanych w bezpiecznych źródłach. Dlatego klienci Twojej usługi muszą korzystać z protokołu HTTPS, aby móc korzystać z implementacji pobierania z innego źródła.

Korzystanie z pobierania z zagranicy

Mając już za sobą wymagania wstępne, przejdźmy do szczegółów technicznych, które są niezbędne do uruchomienia agenta service worker z innego kraju.

Rejestrowanie skryptu service worker

Pierwszym wyzwaniem, na jakie natrafisz, jest sposób zarejestrowania pracownika service worker. Jeśli masz już doświadczenie w pracy z mechanizmami Service Worker, prawdopodobnie znasz te zagadnienia:

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

Ten kod JavaScript do rejestracji własnych mechanizmów Service Worker ma sens w kontekście aplikacji internetowej. Jest on uruchamiany, gdy użytkownik otwiera określony przez Ciebie adres URL. Nie jest to jednak możliwe w sytuacji, gdy jedyna interakcja przeglądarki z serwerem, jaka może uzyskać dostęp do konkretnego zasobu podrzędnego, a nie pełnej nawigacji. Jeśli przeglądarka zażąda np. obrazu z Twojego serwera CDN, nie możesz dołączyć tego fragmentu kodu JavaScript przed odpowiedzią i oczekiwać, że zostanie on uruchomiony. Wymagana jest inna metoda rejestracji skryptu service worker, która wykracza poza normalny kontekst wykonywania kodu JavaScript.

Rozwiązaniem jest nagłówek HTTP, który Twój serwer może umieścić w dowolnej odpowiedzi:

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

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

  • Parametr </service-worker.js> jest wymagany i służy do określania ścieżki do pliku skryptu service worker (zastąp /service-worker.js odpowiednią ścieżką do skryptu). Odpowiada to bezpośrednio ciągowi znaków scriptURL, który w innym przypadku zostałby przekazany jako pierwszy parametr do funkcji navigator.serviceWorker.register(). Wartość musi być zawarta w <> znakach (zgodnie ze specyfikacją nagłówka Link). Jeśli podasz względny, a nie bezwzględny adres URL, zostanie ona zinterpretowana jako względna wobec lokalizacji odpowiedzi.
  • Wymagana jest też właściwość rel="serviceworker", którą należy uwzględnić bez konieczności dostosowywania.
  • scope=/ to opcjonalna deklaracja zakresu odpowiadająca ciągowi znaków options.scope, który możesz przekazywać jako drugi parametr do funkcji navigator.serviceWorker.register(). W wielu przypadkach zakres domyślny nie odpowiada Ci, więc możesz go pominąć, jeśli nie wiesz, że jest Ci potrzebny. Te same ograniczenia dotyczące maksymalnego dozwolonego zakresu oraz możliwość łagodzenia tych ograniczeń za pomocą nagłówka Service-Worker-Allowed mają zastosowanie do rejestracji nagłówka Link.

Podobnie jak w przypadku „tradycyjnej” rejestracji mechanizmów Service Worker użycie nagłówka Link spowoduje zainstalowanie skryptu service worker, który będzie używany w następnym żądaniu wysłanym do zarejestrowanego zakresu. Treść odpowiedzi zawierająca specjalny nagłówek zostanie użyta w takiej postaci i będzie dostępna od razu na stronie bez oczekiwania na zakończenie instalacji przez zagraniczny skrypt service worker.

Pamiętaj, że pobieranie obce jest obecnie zaimplementowane jako test origin, więc obok nagłówka odpowiedzi Link musisz też uwzględnić prawidłowy nagłówek Origin-Trial. Minimalny zestaw nagłówków odpowiedzi, który należy dodać, aby zarejestrować skrypt service worker pobierania obcego, to

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

Rejestracja debugowania

W trakcie programowania warto sprawdzić, czy obcy skrypt service worker jest prawidłowo zainstalowany i przetwarza żądania. Aby sprawdzić, czy wszystko działa zgodnie z oczekiwaniami, możesz sprawdzić kilka rzeczy w Narzędziach dla deweloperów w Chrome.

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

Aby zarejestrować skrypt service worker pobierania obcego, musisz ustawić nagłówek Link w odpowiedzi na zasób hostowany w Twojej domenie, zgodnie z opisem w tym poście. W trakcie okresu próbnego origin, jeśli nie masz ustawionego chrome://flags/#enable-experimental-web-platform-features, musisz też ustawić nagłówek odpowiedzi Origin-Trial. Aby sprawdzić, czy serwer WWW ustawia te nagłówki, spójrz na wpis w panelu Network (Sieć) w Narzędziach deweloperskich:

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

Czy instancja robocza usługi Foreign Fetch jest prawidłowo zarejestrowana?

Możesz też potwierdzić podstawową rejestrację skryptu service worker, w tym jego zakres, sprawdzając pełną listę skryptów service worker w panelu aplikacji Narzędzi deweloperskich. Pamiętaj, aby wybrać opcję „Pokaż wszystko”, ponieważ domyślnie są widoczne tylko mechanizmy Service Worker z bieżącego źródła.

Skrypt service worker pobierania z innych obcych krajów w panelu Aplikacje.

Moduł obsługi zdarzeń instalacji

Po zarejestrowaniu zewnętrznego skryptu service worker będzie on mógł odpowiadać na zdarzenia install i activate, tak jak zrobi to każdy inny skrypt service worker. Może wykorzystać te zdarzenia, np. do wypełnienia pamięci podręcznej wymaganymi zasobami podczas zdarzenia install lub usunięcia nieaktualnych pamięci podręcznych w zdarzeniu activate.

Oprócz zwykłych installdziałań w pamięci podręcznej zdarzeń występuje dodatkowy krok wymagany w ramach modułu obsługi zdarzeń install zewnętrznego skryptu service worker. Twój kod musi wywoływać 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. Obie są wymagane:

  • scopes pobiera tablicę składającą się z co najmniej 1 ciągu znaków, z których każdy reprezentuje zakres żądań wywołujących zdarzenie foreignfetch. Zaczekaj, być może myślisz: zakres został już zdefiniowany podczas rejestracji mechanizmu Service Worker. To prawda i ten ogólny zakres jest nadal istotny – każdy określony tutaj zakres musi być równy ogólnemu zakresowi skryptu service worker lub tylko jednemu z nich. Dodatkowe ograniczenia zakresu pozwalają na wdrożenie uniwersalnego skryptu service worker, który może obsługiwać zarówno własne zdarzenia fetch (w przypadku żądań wysyłanych z Twojej własnej witryny), jak i zewnętrzne zdarzenia foreignfetch (w przypadku żądań pochodzących z innych domen). Należy wyraźnie zaznaczyć, że tylko część większego zakresu może wywoływać zdarzenie foreignfetch. W praktyce, jeśli wdrażasz skrypt service worker przeznaczony tylko do obsługi zdarzeń foreignfetch innych firm, najlepiej jest używać jednego, wyraźnego zakresu równego ogólnemu zakresowi skryptu service worker. Właśnie to da się osiągnąć w przykładzie powyżej, z użyciem wartości self.registration.scope.
  • origins pobiera też tablicę z co najmniej 1 ciągiem i pozwala ograniczyć działanie modułu foreignfetch do odpowiadania tylko na żądania z konkretnych domen. Jeśli na przykład wyraźnie zezwolisz na „https://example.com”, żądanie wysłane ze strony hostowanej w https://example.com/path/to/page.html dotyczącej zasobu udostępnianego z zakresu obcego pobierania spowoduje uruchomienie modułu obsługi obcego pobierania, ale żądania wysyłane z adresu https://random-domain.com/path/to/page.html nie wyzwolą Twojego modułu obsługi. Jeśli nie masz konkretnego powodu, aby uruchamiać logikę pobierania obcego tylko dla podzbioru odległych źródeł, możesz po prostu określić '*' jako jedyną wartość w tablicy, a wszystkie źródła będą dozwolone.

Moduł obsługi zdarzeń zagranicznych

Po zainstalowaniu i skonfigurowaniu instancji roboczej usługi innej firmy za pomocą registerForeignFetch() będzie ona miała szansę przechwycić do serwera żądania zasobów podrzędnych z innych domen, które należą do zakresu pobierania obcego.

W tradycyjnym, własnym serwerze service worker każde żądanie wywołuje zdarzenie fetch, na które ten skrypt miał szansę odpowiedzieć. Nasz zewnętrzny skrypt service worker może obsłużyć nieco inne zdarzenie o nazwie foreignfetch. Te 2 zdarzenia są dosyć podobne i umożliwiają sprawdzenie przychodzącego żądania oraz opcjonalnie wysłanie na nie odpowiedzi w 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']
        };
    })
    );
});

Pomimo podobieństw koncepcyjnych istnieje kilka różnic w praktyce podczas wywoływania funkcji respondWith() na urządzeniu ForeignFetchEvent. Zamiast udostępniać obiektowi respondWith() (lub Promise, który kończy się symbolem Response), tak jak robisz w przypadku FetchEvent, musisz przekazać Promise, który otwiera się z obiektem z określonymi właściwościami, do parametru respondWith() ForeignFetchEvent:Response

  • Parametr response jest wymagany i musi być ustawiony na obiekt Response, który będzie zwracany klientowi, który wysłał żądanie. Jeśli podasz coś innego niż prawidłowy Response, żądanie klienta zostanie zakończone z powodu błędu sieci. W przeciwieństwie do funkcji respondWith() w module obsługi zdarzeń fetch musisz podać tutaj wartość Response, a nie Promise, która kończy się wartością Response. Możesz utworzyć odpowiedź za pomocą łańcucha obietnicy i przekazać ten łańcuch jako parametr do obiektu respondWith() obiektu foreignfetch. Łańcuch musi jednak być zdefiniowany za pomocą obiektu zawierającego właściwość response ustawioną na obiekt Response. Widać to w przykładowym kodzie powyżej.
  • origin jest opcjonalny i służy do określania, czy zwracana odpowiedź jest nieprzejrzysta. Jeśli pominiesz tę informację, odpowiedź będzie nieprzejrzysta, a klient będzie miał ograniczony dostęp do jej treści i nagłówków. Jeśli żądanie zostało wysłane za pomocą metody mode: 'cors', zwrócenie nieprzejrzystej odpowiedzi będzie traktowane jako błąd. Jeśli jednak podasz wartość ciągu równą pochodzeniu klienta zdalnego (którą można uzyskać za pomocą event.origin), wyraźnie zgadzasz się na dostarczanie klientowi odpowiedzi z obsługą CORS.
  • Parametr headers jest też opcjonalny i przydatny tylko wtedy, gdy określasz jednocześnie właściwość origin i zwracasz odpowiedź CORS. Domyślnie w odpowiedzi będą uwzględniane tylko nagłówki z listy nagłówków odpowiedzi na liście bezpiecznych CORS. Jeśli chcesz dokładniej filtrować zwracane dane, nagłówki pobierają listę z co najmniej 1 nazwą nagłówka i używają jej jako listy dozwolonych nagłówków, które mają być widoczne w odpowiedzi. Pozwala to na korzystanie z CORS, a jednocześnie zapobiega ujawnianiu potencjalnie poufnych nagłówków odpowiedzi klientowi zdalnemu.

Pamiętaj, że po uruchomieniu modułu obsługi foreignfetch ma on dostęp do wszystkich danych logowania i uprawnień otoczenia źródła hostującego skrypt service worker. Jako deweloper wdrażający obcy skrypt service worker z obsługą pobierania odpowiada Twoim zobowiązaniom – nie możesz ujawnić żadnych danych o podwyższonym standardzie, które nie byłyby dostępne w związku z tymi danymi logowania. Wymaganie zgody na odpowiedzi CORS to jeden z kroków, które pozwalają ograniczyć przypadki niezamierzonej ekspozycji. Jako programista możesz jednak wprost wysyłać żądania fetch() do modułu obsługi foreignfetch, 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}))
    );
});

Uwagi klienta

Istnieją pewne dodatkowe kwestie, które wpływają na sposób, w jaki skrypt service worker obsługuje żądania wysyłane przez klientów Twojej usługi.

Klienty, które mają własny skrypt service worker

Niektórzy klienci Twojej usługi mogą już mieć własny skrypt service worker obsługujący żądania pochodzące z ich aplikacji internetowej. Co to oznacza dla zewnętrznego mechanizmu pobierania.

Moduły obsługi fetch we własnym mechanizmie Service Worker mają pierwszą możliwość udzielenia odpowiedzi na wszystkie żądania wysyłane przez aplikację internetową, nawet jeśli istnieje zewnętrzny skrypt service worker z włączonym foreignfetch i zakresem obejmującym to żądanie. Jednak klienci z własnymi mechanizmami Service Worker wciąż mogą korzystać z obcojęzycznego skryptu service worker.

Użycie elementu fetch() do pobrania zasobów z innych domen w obrębie własnego skryptu service worker spowoduje uruchomienie odpowiedniego skryptu pobierania obcego. Oznacza to, że ten kod może korzystać z modułu 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 występują własne moduły obsługi pobierania, ale nie wywołują one event.respondWith() podczas obsługi żądań zasobów z innych domen, zostanie ono 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 własny moduł obsługi fetch wywołuje metodę event.respondWith(), ale nie używa fetch() do żądania zasobu w zakresie pobierania obcego, wówczas mechanizm pobierania obcego języka nie będzie miał możliwości obsługi żądania.

Klienty, które nie mają własnego skryptu service worker

Wszystkie klienty wysyłające żądania do usługi zewnętrznej mogą odnieść korzyści z wdrożenia obcego skryptu service worker, nawet jeśli klient nie korzysta jeszcze z własnego skryptu service worker. Aby wyrazić zgodę na korzystanie z obcego skryptu service worker, klient musi mieć odpowiednią przeglądarkę. Oznacza to, że wdrożenie obcego skryptu pobierania pozwoli wprowadzić niestandardową logikę żądania i wspólną pamięć podręczną od razu, co przyniesie korzyści wielu klientom Twojej usługi bez konieczności wykonywania przez nich dodatkowych czynności.

Jak to wszystko połączyć: tam, gdzie klienci szukają odpowiedzi

Biorąc pod uwagę powyższe informacje, możemy utworzyć hierarchię źródeł, z których klient będzie korzystać, aby znaleźć odpowiedź na żądanie z innej domeny.

  1. Moduł obsługi fetch własnego skryptu service worker (jeśli jest dostępny)
  2. Moduł obsługi foreignfetch zewnętrznego skryptu service worker (jeśli jest dostępny i tylko w przypadku żądań z innych domen)
  3. Pamięć podręczna HTTP przeglądarki (jeśli istnieje nowa odpowiedź)
  4. Sieć

Przeglądarka zaczyna się od góry i w zależności od implementacji skryptu service worker porusza się w dół listy, dopóki nie znajdzie źródła odpowiedzi.

Więcej informacji

Bądź na bieżąco

Implementacja wersji próbnej źródła obcego pobierania w Chrome może ulec zmianie w związku z opiniami deweloperów. Będziemy go aktualizować za pomocą zmian wprowadzonych bezpośrednio w wyszukiwarce, a w razie potrzeby będziemy informować o wprowadzanych poniżej zmianach. Informacje o najważniejszych zmianach udostępnimy też na koncie @chromiumdev na Twitterze.