Skrypty service worker w środowisku produkcyjnym

Zrzut ekranu w orientacji pionowej

Podsumowanie

Dowiedz się, jak wykorzystaliśmy biblioteki skryptu service worker, aby aplikacja internetowa Google I/O 2015 była szybko i działa głównie w trybie offline.

Przegląd

Tegoroczną aplikację internetową Google I/O 2015 napisał zespół Google ds. relacji z programistami na podstawie projektów naszych znajomych z firmy Instrument, którzy przygotowali sprytny eksperyment audiowizualny. Celem naszego zespołu było dopilnowanie, aby aplikacja internetowa I/O (którą będę nosić pod nazwą IOWA) prezentowała wszystkie możliwości nowoczesnej sieci. Usługa w trybie offline była na pierwszym miejscu.

Jeśli czytałeś ostatnio inne artykuły w tej witrynie, na pewno zdarzyło Ci się natrafić na pracowników usługowych i zdziwisz się, że obsługa offline przez IOWA jest w dużej mierze zależna od usług wsparcia offline. Z myślą o rzeczywistych potrzebach organizacji IOWA opracowaliśmy 2 biblioteki do obsługi 2 różnych przypadków użycia w trybie offline: sw-precache do automatyzacji wyprzedzenia zasobów statycznych i sw-toolbox do obsługi buforowania w czasie działania aplikacji i strategii kreacji zastępczych.

Biblioteki uzupełniają się wzajemnie i umożliwiły nam wdrożenie skutecznej strategii, w której „powłoka” zawartości statycznej IOWA zawsze była udostępniana bezpośrednio z pamięci podręcznej, a zasoby dynamiczne lub zdalne były udostępniane z sieci, a w razie potrzeby można stosować wartości zastępcze do odpowiedzi statycznych lub przechowywanych w pamięci podręcznej.

Ustalam działanie urządzenia sw-precache

Statyczne zasoby IOWA – HTML, JavaScript, CSS i obrazy – stanowią podstawową powłokę aplikacji internetowej. Rozważając buforowanie tych zasobów, mieliśmy 2 ważne wymagania: chcieliśmy mieć pewność, że większość zasobów statycznych będzie przechowywana w pamięci podręcznej oraz że będą one zawsze aktualne. Narzędzie sw-precache zostało stworzone z myślą o tych wymaganiach.

Integracja podczas kompilacji

sw-precache z procesem kompilacji opartym na gulp w IOWA. Korzystamy z serii glob wzorców, aby wygenerować pełną listę wszystkich zasobów statycznych używanych przez IOWA.

staticFileGlobs: [
    rootDir + '/bower_components/**/*.{html,js,css}',
    rootDir + '/elements/**',
    rootDir + '/fonts/**',
    rootDir + '/images/**',
    rootDir + '/scripts/**',
    rootDir + '/styles/**/*.css',
    rootDir + '/data-worker-scripts.js'
]

Alternatywne sposoby, takie jak kodowanie listy nazw plików na stałe w tablicy i konieczność zwracania numeru wersji pamięci podręcznej za każdym razem, gdy któraś z tych zmian jest zbyt podatna na błędy, zwłaszcza że sprawdzanie kodu miało miejsce wielu członków zespołu. Nikt nie chce przerywać działania trybu offline, pomijając nowy plik w ręcznie zarządzanej tablicy. Integracja podczas kompilacji oznaczała, że bez obaw mogliśmy wprowadzać zmiany w istniejących plikach i dodawać nowe.

Aktualizowanie zasobów w pamięci podręcznej

sw-precache generuje podstawowy skrypt skryptu service worker, który zawiera unikalny szyfr MD5 dla każdego zasobu, który jest wstępnie buforowany. Za każdym razem, gdy istniejący zasób zmieni się lub zostanie dodany nowy zasób, skrypt skryptu service worker jest generowany ponownie. Spowoduje to automatyczne uruchomienie procesu aktualizacji skryptu service worker, w którym nowe zasoby są przechowywane w pamięci podręcznej, a nieaktualne zasoby są trwale usuwane. Wszystkie istniejące zasoby z identycznymi haszami MD5 pozostają niezmienione. Oznacza to, że użytkownicy, którzy odwiedzili witrynę przed pobraniem, jedynie pobrali minimalny zestaw zmienionych zasobów, co zapewnia większą wydajność niż w przypadku dużej utraty ważności pamięci podręcznej.

Każdy plik pasujący do jednego z wzorców glob jest pobierany i zapisywany w pamięci podręcznej podczas pierwszej wizyty użytkownika na IOWA. Dołożyliśmy wszelkich starań, by w pamięci podręcznej znajdowały się tylko krytyczne zasoby potrzebne do wyrenderowania strony. Treści dodatkowe, takie jak multimedia użyte w eksperymencie audiowizualnym lub zdjęcia profilowe prelegentów podczas sesji, nie zostały zapisane w pamięci podręcznej. Zamiast tego do obsługi żądań offline użyliśmy biblioteki sw-toolbox.

sw-toolbox, dla wszystkich naszych potrzeb

Jak już wspomnieliśmy, nie można zapisać wszystkich zasobów, które są niezbędne dla działania witryny w trybie offline. Niektóre zasoby są zbyt duże lub rzadko używane, aby korzystać z nich, a inne mają charakter dynamiczny, np. odpowiedzi ze zdalnego interfejsu API lub usługi. Fakt, że żądanie nie jest wstępnie zapisane w pamięci podręcznej, nie oznacza, że musi się ono wiązać z NetworkError. Rozwiązanie sw-toolbox dało nam elastyczność przy wdrażaniu modułów obsługi żądań, które obsługują buforowanie w czasie działania niektórych zasobów, a w przypadku innych niestandardowych kreacji zastępczych. Wykorzystaliśmy go też do aktualizacji zasobów zapisanych w pamięci podręcznej w odpowiedzi na powiadomienia push.

Oto kilka przykładów niestandardowych modułów obsługi żądań, które opracowaliśmy na podstawie pakietu sw-toolbox. Łatwo je zintegrować z podstawowym skryptem skryptu service worker za pomocą narzędzia importScripts parameter w sw-precache, który pobiera samodzielne pliki JavaScript do zakresu mechanizmu Service Worker.

Eksperyment audio/wizualny

W eksperymencie audiowizualnym zastosowaliśmy strategię sw-toolbox networkFirst dotyczącą pamięci podręcznej. Wszystkie żądania HTTP pasujące do wzorca adresu URL używanego w eksperymencie są najpierw wysyłane do sieci. Jeśli odpowiedź przebiegnie pomyślnie, zostanie ona usunięta za pomocą interfejsu Cache Storage API. Jeśli kolejne żądanie zostało wysłane, gdy sieć była niedostępna, zostanie użyta odpowiedź z pamięci podręcznej.

Pamięć podręczna była automatycznie aktualizowana za każdym razem, gdy wracała udana odpowiedź sieci, więc nie musieliśmy tworzyć wersji zasobów ani wpisów o wygaśnięciu.

toolbox.router.get('/experiment/(.+)', toolbox.networkFirst);

Zdjęcia profilowe rozmówców

W przypadku obrazów profilowych rozmówców naszym celem było wyświetlenie zapisanej w pamięci podręcznej wersji obrazu danego rozmówcy (jeśli była dostępna), a w razie braku możliwości pobrania obrazu powrócił do sieci. Jeśli żądanie sieciowe nie powiodło się, w ostatecznym rozrachunku użyliśmy ogólnego obrazu zastępczego, który został zapisany w pamięci podręcznej (dzięki czemu jest zawsze dostępny). Jest to typowa strategia stosowana w przypadku obrazów, które można zastąpić ogólnymi symbolami zastępczymi. Można ją też łatwo wdrożyć dzięki łańcuchom instrukcji cacheFirst i cacheOnly obiektu sw-toolbox.

var DEFAULT_PROFILE_IMAGE = 'images/touch/homescreen96.png';

function profileImageRequest(request) {
    return toolbox.cacheFirst(request).catch(function() {
    return toolbox.cacheOnly(new Request(DEFAULT_PROFILE_IMAGE));
    });
}

toolbox.precache([DEFAULT_PROFILE_IMAGE]);
toolbox.router.get('/(.+)/images/speakers/(.*)',
                    profileImageRequest,
                    {origin: /.*\.googleapis\.com/});
Zdjęcia profilowe ze strony sesji
Obrazy profilowe ze strony sesji.

Aktualizacje harmonogramów użytkowników

Jedną z kluczowych funkcji IOWA było umożliwienie zalogowanym użytkownikom tworzenia i realizacji harmonogramu sesji, w których planowali wziąć udział. Zgodnie z oczekiwaniami aktualizacje sesji były przeprowadzane za pomocą żądań HTTP POST wysyłanych do serwera backendu. Spędziliśmy trochę czasu, aby wypracować najlepszy sposób obsługi żądań zmiany stanu, gdy użytkownik jest offline. Przygotowaliśmy kombinację zakończonych niepowodzeniem żądań w IndexedDB, w połączeniu z logiką na głównej stronie internetowej, która sprawdza IndexedDB pod kątem oczekujących żądań w kolejce i ponawia wszystkie znalezione żądania.

var DB_NAME = 'shed-offline-session-updates';

function queueFailedSessionUpdateRequest(request) {
    simpleDB.open(DB_NAME).then(function(db) {
    db.set(request.url, request.method);
    });
}

function handleSessionUpdateRequest(request) {
    return global.fetch(request).then(function(response) {
    if (response.status >= 500) {
        return Response.error();
    }
    return response;
    }).catch(function() {
    queueFailedSessionUpdateRequest(request);
    });
}

toolbox.router.put('/(.+)api/v1/user/schedule/(.+)',
                    handleSessionUpdateRequest);
toolbox.router.delete('/(.+)api/v1/user/schedule/(.+)',
                        handleSessionUpdateRequest);

Ponowne próby zostały wykonane na podstawie kontekstu strony głównej, więc mogliśmy mieć pewność, że zawierały nowy zestaw danych logowania użytkownika. Po pomyślnym zakończeniu próby pojawił się komunikat informujący użytkownika o zastosowaniu aktualizacji znajdujących się w kolejce.

simpleDB.open(QUEUED_SESSION_UPDATES_DB_NAME).then(function(db) {
    var replayPromises = [];
    return db.forEach(function(url, method) {
    var promise = IOWA.Request.xhrPromise(method, url, true).then(function() {
        return db.delete(url).then(function() {
        return true;
        });
    });
    replayPromises.push(promise);
    }).then(function() {
    if (replayPromises.length) {
        return Promise.all(replayPromises).then(function() {
        IOWA.Elements.Toast.showMessage(
            'My Schedule was updated with offline changes.');
        });
    }
    });
}).catch(function() {
    IOWA.Elements.Toast.showMessage(
    'Offline changes could not be applied to My Schedule.');
});

Google Analytics offline

Podobnie wdrożyliśmy moduł obsługi w kolejce wszystkich nieudanych żądań Google Analytics i próbowaliśmy je odtworzyć później, gdy sieć będzie już dostępna. Przy tym podejściu bycie offline nie oznacza rezygnacji z statystyk dostępnych w Google Analytics. Do każdego żądania w kolejce dodaliśmy parametr qt. Ustawiliśmy czas, który upłynął od wysłania żądania. W ten sposób zadbano o to, aby backend Google Analytics trafił do backendu Google Analytics zgodnie z odpowiednim czasem atrybucji zdarzenia. Google Analytics oficjalnie obsługuje wartości qt wynoszące do 4 godzin, dlatego dołożyliśmy wszelkich starań, aby te żądania były odtwarzane jak najszybciej przy każdym uruchomieniu skryptu service worker.

var DB_NAME = 'offline-analytics';
var EXPIRATION_TIME_DELTA = 86400000;
var ORIGIN = /https?:\/\/((www|ssl)\.)?google-analytics\.com/;

function replayQueuedAnalyticsRequests() {
    simpleDB.open(DB_NAME).then(function(db) {
    db.forEach(function(url, originalTimestamp) {
        var timeDelta = Date.now() - originalTimestamp;
        var replayUrl = url + '&qt=' + timeDelta;
        fetch(replayUrl).then(function(response) {
        if (response.status >= 500) {
            return Response.error();
        }
        db.delete(url);
        }).catch(function(error) {
        if (timeDelta > EXPIRATION_TIME_DELTA) {
            db.delete(url);
        }
        });
    });
    });
}

function queueFailedAnalyticsRequest(request) {
    simpleDB.open(DB_NAME).then(function(db) {
    db.set(request.url, Date.now());
    });
}

function handleAnalyticsCollectionRequest(request) {
    return global.fetch(request).then(function(response) {
    if (response.status >= 500) {
        return Response.error();
    }
    return response;
    }).catch(function() {
    queueFailedAnalyticsRequest(request);
    });
}

toolbox.router.get('/collect',
                    handleAnalyticsCollectionRequest,
                    {origin: ORIGIN});
toolbox.router.get('/analytics.js',
                    toolbox.networkFirst,
                    {origin: ORIGIN});

replayQueuedAnalyticsRequests();

Strony docelowe powiadomień push

Mechanizmy Service Worker obsługiwały nie tylko funkcje offline IOWA, lecz także powiadomienia push, które służyły do powiadamiania użytkowników o aktualizacjach ich sesji dodanych do zakładek. Strona docelowa powiązana z tymi powiadomieniami zawierała zaktualizowane szczegóły sesji. Te strony docelowe były już przechowywane w pamięci podręcznej jako część witryny, więc działały offline. Musieliśmy się jednak upewnić, że szczegóły sesji na tej stronie są aktualne, nawet w trybie offline. W tym celu zmodyfikowaliśmy metadane sesji znajdujące się w pamięci podręcznej, dodając aktualizacje, które spowodowały wysłanie powiadomienia push, i zamieściliśmy wynik w pamięci podręcznej. Będą one używane przy następnym otwarciu strony z informacjami o sesji, zarówno online, jak i offline.

caches.open(toolbox.options.cacheName).then(function(cache) {
    cache.match('api/v1/schedule').then(function(response) {
    if (response) {
        parseResponseJSON(response).then(function(schedule) {
        sessions.forEach(function(session) {
            schedule.sessions[session.id] = session;
        });
        cache.put('api/v1/schedule',
                    new Response(JSON.stringify(schedule)));
        });
    } else {
        toolbox.cache('api/v1/schedule');
    }
    });
});

Porady i wskazówki

Oczywiście nikt nie pracuje nad projektem na skalę IOWA, jeśli nie wpadniesz na kilka niepewności. Oto niektóre z nich i jak z nimi pracowaliśmy.

Nieaktualna treść

Gdy planujesz strategię buforowania (wdrożoną za pomocą mechanizmów Service Worker lub standardowej pamięci podręcznej przeglądarki), istnieje kompromis między szybkim dostarczaniem zasobów a dostarczaniem najświeższych. W ramach sw-precache wdrożyliśmy agresywną strategię skoncentrowaną na pamięci podręcznej na potrzeby powłoki aplikacji, co oznacza, że nasz skrypt service worker nie sprawdza aktualizacji sieci przed zwróceniem kodu HTML, JavaScript i CSS na stronie.

Na szczęście udało nam się skorzystać ze zdarzeń cyklu życia skryptu service worker, aby wykryć, kiedy nowa treść była dostępna po wczytaniu strony. Po wykryciu zaktualizowanego skryptu service worker wyświetlimy użytkownikowi komunikat informujący, że powinien ponownie załadować stronę, aby zobaczyć najnowszą zawartość.

if (navigator.serviceWorker && navigator.serviceWorker.controller) {
    navigator.serviceWorker.controller.onstatechange = function(e) {
    if (e.target.state === 'redundant') {
        var tapHandler = function() {
        window.location.reload();
        };
        IOWA.Elements.Toast.showMessage(
        'Tap here or refresh the page for the latest content.',
        tapHandler);
    }
    };
}
Toast najnowszych treści
Komunikat „Najnowsze treści”.

Upewnij się, że treści statyczne są statyczne.

sw-precache używa skrótu MD5 zawartości plików lokalnych i pobiera tylko zasoby, których hasz został zmieniony. Oznacza to, że zasoby są dostępne na stronie niemal natychmiast, ale gdy coś zostanie zapisane w pamięci podręcznej, będzie przechowywane w pamięci podręcznej, dopóki nie zostanie przydzielony nowy hasz w zaktualizowanym skrypcie skryptu service worker.

Wystąpił problem z tym działaniem podczas wejścia-wyjścia, ponieważ nasz backend musiał dynamicznie aktualizować identyfikatory wideo transmitowanych na żywo w YouTube dla każdego dnia konferencji. Główny plik szablonu był statyczny i niezmieniony, więc przepływ aktualizacji naszego skryptu service worker nie został uruchomiony. W przypadku wielu użytkowników to, co miało być dynamiczną odpowiedzią serwera z aktualizacją filmów z YouTube, okazało się odpowiedzią w pamięci podręcznej.

Aby uniknąć tego typu problemów, zadbaj o to, aby Twoja aplikacja internetowa była zorganizowana w taki sposób, że powłoka jest zawsze statyczna i bezpiecznie buforowana. Wszystkie dynamiczne zasoby, które modyfikują powłokę, są wczytywane niezależnie.

Przechowywanie w pamięci podręcznej żądań z pamięci podręcznej

Gdy sw-precache wysyła żądania wstępnego buforowania zasobów, korzysta z tych odpowiedzi bez końca, dopóki uważa, że hasz MD5 pliku nie uległ zmianie. Oznacza to, że szczególnie ważne jest, aby odpowiedź na żądanie buforowania była aktualna i nie została zwrócona z pamięci podręcznej HTTP przeglądarki. Tak, żądania fetch() wysyłane w skrypcie service worker mogą wykorzystywać dane z pamięci podręcznej HTTP przeglądarki.

Aby odpowiedzi, które wstępnie przechowujemy, pochodziły z sieci, a nie z pamięci podręcznej HTTP przeglądarki, sw-precache automatycznie dołącza parametr zapytania pomijający pamięć podręczną do każdego żądanego adresu URL. Jeśli nie używasz sw-precache i używasz strategii uzyskiwania odpowiedzi z pamięci podręcznej, sprawdź, czy robisz coś podobnego we własnym kodzie.

Czytelniejszym rozwiązaniem na potrzeby pomijania pamięci podręcznej jest ustawienie trybu pamięci podręcznej każdego elementu Request używanego do wstępnego buforowania na reload, co zapewni, że odpowiedź będzie pochodzić z sieci. Jednak na razie opcja trybu pamięci podręcznej nie jest obsługiwana w Chrome.

Obsługa logowania i wylogowywania

Użytkownicy IOWA mogli logować się na konta Google i aktualizować swoje niestandardowe harmonogramy wydarzeń, ale wiązało się to z koniecznością późniejszego wylogowania się. Zapisywanie spersonalizowanych odpowiedzi w pamięci podręcznej to oczywiście trudny temat i nie zawsze istnieje 1 właściwe podejście.

Ponieważ korzystanie z osobistego harmonogramu, nawet w trybie offline, było kluczowym elementem IOWA, zdecydowaliśmy, że dane w pamięci podręcznej będą odpowiednie. Gdy użytkownik się wyloguje, wyczyściliśmy dane sesji zapisane w pamięci podręcznej.

    self.addEventListener('message', function(event) {
      if (event.data === 'clear-cached-user-data') {
        caches.open(toolbox.options.cacheName).then(function(cache) {
          cache.keys().then(function(requests) {
            return requests.filter(function(request) {
              return request.url.indexOf('api/v1/user/') !== -1;
            });
          }).then(function(userDataRequests) {
            userDataRequests.forEach(function(userDataRequest) {
              cache.delete(userDataRequest);
            });
          });
        });
      }
    });

Uważaj na dodatkowe parametry zapytania.

Gdy skrypt service worker szuka odpowiedzi w pamięci podręcznej, jako klucza używa adresu URL żądania. Domyślnie adres URL żądania musi być dokładnie taki sam jak adres URL służący do przechowywania odpowiedzi z pamięci podręcznej, w tym wszelkie parametry zapytania z części wyszukiwania adresu URL.

Spowodowało to problem w fazie programowania, gdy zaczęliśmy używać parametrów adresu URL do śledzenia, skąd pochodzi ruch. Na przykład dodaliśmy parametr utm_source=notification do adresów URL, które zostały otwarte po kliknięciu jednego z naszych powiadomień, a parametr utm_source=web_app_manifest został użyty w start_url w manifeście aplikacji internetowej. Adresy URL, które wcześniej dopasowane do odpowiedzi w pamięci podręcznej, były podawane jako braki po dołączeniu tych parametrów.

Częściowo udało się rozwiązać ten problem w ramach opcji ignoreSearch, której można używać podczas wywoływania metody Cache.match(). Chrome nie obsługuje jeszcze ignoreSearch, a nawet jeśli tak, jest to działanie typu „wszystko albo nic”. Potrzebny był sposób ignorowania niektórych parametrów zapytań w adresie URL i uwzględnianie innych istotnych parametrów.

Rozszerzyliśmy zakres sw-precache, aby usunąć niektóre parametry zapytania przed sprawdzeniem dopasowania z pamięci podręcznej i umożliwić programistom określenie parametrów, które mają być ignorowane za pomocą opcji ignoreUrlParametersMatching. Oto implementacja:

function stripIgnoredUrlParameters(originalUrl, ignoredRegexes) {
    var url = new URL(originalUrl);

    url.search = url.search.slice(1)
    .split('&')
    .map(function(kv) {
        return kv.split('=');
    })
    .filter(function(kv) {
        return ignoredRegexes.every(function(ignoredRegex) {
        return !ignoredRegex.test(kv[0]);
        });
    })
    .map(function(kv) {
        return kv.join('=');
    })
    .join('&');

    return url.toString();
}

Co to oznacza dla Ciebie

Integracja mechanizmu Service Worker z aplikacją internetową Google I/O jest prawdopodobnie najbardziej złożonym, rzeczywistym zastosowaniem, jakie było stosowane do tej pory. Czekamy na pojawienie się społeczności programistów stron internetowych za pomocą narzędzi utworzonych przez nas sw-precache i sw-toolbox, a także z opisanych przez nas technik tworzenia własnych aplikacji internetowych. Skrypty service worker to ulepszenia, z których możesz zacząć korzystać już dziś. Jeśli są one częścią poprawnie uporządkowanej aplikacji internetowej, korzyści związane z szybkością działania i działaniem w trybie offline są bardzo ważne dla użytkowników.