Więcej niż SPA – alternatywne architektury dla Twojej aplikacji PWA

Porozmawiajmy o architekturze.

Omówię ważny, ale potencjalnie niezrozumiały zagadnienie: architekturę aplikacji internetowej, a w szczególności wpływ decyzji architektonicznych na projekt progresywnej aplikacji internetowej.

„Architektura” może wydawać się niejasna i nie być od razu jasne, dlaczego to ma znaczenie. Jednym ze sposobów myślenia o architekturze jest zadawanie sobie następujących pytań: gdy użytkownik odwiedza stronę w mojej witrynie, jaki kod HTML jest ładowany? A co jest wczytywane, gdy odwiedzają inną stronę?

Odpowiedzi na te pytania nie zawsze są proste, a gdy zaczniesz myśleć o progresywnych aplikacjach internetowych, mogą się one stać jeszcze bardziej skomplikowane. Chciałbym przedstawić jedną z możliwych architektur, które okazały się skuteczne. W tym artykule oznaczę swoje decyzje jako „moje podejście” do tworzenia progresywnej aplikacji internetowej.

Możesz korzystać z mojej metody przy tworzeniu własnej aplikacji PWA, ale zawsze istnieją inne, rozsądne rozwiązania. Mam nadzieję, że to, jak te elementy ze sobą współgrają, zainspiruje Cię i pozwoli Ci dostosować je do swoich potrzeb.

PWA Stack Overflow

Wraz z tym artykułem utworzyliśmy PWA w Stack Overflow. Spędzam dużo czasu na czytaniu i udzielaniu się w serwisie Stack Overflow. Chciałem(-am) utworzyć aplikację internetową, która ułatwiłaby przeglądanie najczęstszych pytań na dany temat. Działa na bazie publicznego Stack Exchange API. Jest to oprogramowanie open source. Więcej informacji znajdziesz w projekcie GitHub.

Aplikacje wielostronicowe (MPA)

Zanim przejdziemy do szczegółów, wyjaśnijmy różne terminy i wyjaśnimy działanie technologii. Najpierw omówię, co nazywam „aplikacjami wielostronicowymi” lub „MPA”.

MPA to zaawansowana nazwa tradycyjnej architektury od początku powstania sieci. Za każdym razem, gdy użytkownik otwiera nowy adres URL, przeglądarka stopniowo renderuje kod HTML odpowiadający tej stronie. Nie próbujemy zachowywać stanu strony ani jej treści pomiędzy różnymi opcjami nawigacji. Każda wizyta na nowej stronie to początek nowego.

Odróżnia się to od modelu SPA służącego do tworzenia aplikacji internetowych, w którym gdy użytkownik otworzy nową sekcję, przeglądarka uruchamia kod JavaScript, by zaktualizować istniejącą stronę. Zarówno SPA, jak i MPA to modele, które można stosować w równym stopniu, ale w tym poście chcę przeanalizować koncepcje PWA w kontekście aplikacji wielostronicowej.

Szybkie działanie

Słyszeliście już, jak ja (i wielu innych użytkowników) używam określenia „progresywna aplikacja internetowa” czy też PWA. Możliwe, że znasz już niektóre z materiałów stanowiących tło w innym miejscu w tej witrynie.

Progresywną aplikację internetową można traktować jako aplikację internetową, która zapewnia użytkownikom najwyższej jakości obsługę i która naprawdę zdobywa miejsce na ekranie głównym użytkownika. Akronim „FIRE” oznacza Fast, Integrated, Reliable i Engaging – podsumowuje wszystkie atrybuty, o których należy pamiętać podczas tworzenia aplikacji PWA.

W tym artykule skupimy się na podzbiorze tych atrybutów: Szybka i Niezawodna.

Szybkość: chociaż „szybko” może oznaczać różne rzeczy w zależności od kontekstu, omówimy zalety szybkości wczytywania reklam z sieci.

Niezawodna: Jednak sama szybkość to za mało. Aby aplikacja internetowa działała jak aplikacja PWA, musi być niezawodna. Musi być wystarczająco odporna, aby zawsze coś wczytać, nawet jeśli jest to tylko niestandardowa strona błędu, niezależnie od stanu sieci.

Niezawodnie szybka: na koniec jeszcze raz przeredaguję definicję PWA i zajmę się tym, co oznacza tworzenie niezawodnie szybko działających aplikacji. Szybkość i niezawodność nie wystarczy w sieci o małych opóźnieniach. Aby aplikacja internetowa była stabilna, jej szybkość jest stabilna niezależnie od podstawowych warunków sieciowych.

Włączanie technologii: mechanizmy Service Workers + Cache Storage API

Progresywne aplikacje internetowe to nowe poprzeczki dla szybkości i odporności. Na szczęście platforma internetowa udostępnia pewne elementy, które pozwalają urzeczywistnić takie wyniki. Mam na myśli skrypty service worker i Cache Storage API.

Możesz utworzyć skrypt service worker, który nasłuchuje żądań przychodzących, przekazuje część odpowiedzi do sieci i przechowuje kopię odpowiedzi do wykorzystania w przyszłości za pomocą interfejsu Cache Storage API.

Skrypt service worker używający interfejsu Cache Storage API do zapisywania kopii odpowiedzi sieciowej.

Następnym razem, gdy aplikacja internetowa wykona to samo żądanie, jej skrypt service worker może sprawdzić pamięć podręczną i zwrócić wcześniej zapisaną odpowiedź.

Skrypt service worker używający interfejsu Cache Storage API do odpowiadania, z pominięciem sieci.

Unikanie korzystania z sieci w miarę możliwości to kluczowy element oferowania stabilnej wydajności.

„Isomorficzny” JavaScript

Kolejną koncepcją, którą chcę poruszyć, jest to, co czasami określa się jako „izomorficzny” lub „uniwersalny” JavaScript. Mówiąc prościej, chodzi o to, że ten sam kod JavaScript może być używany przez różne środowiska wykonawcze. Tworząc PWA, chciałem udostępniać kod JavaScript między serwerem backendu a mechanizmem Service Worker.

Istnieje wiele prawidłowych sposobów udostępniania kodu w ten sposób, ale moim podejściem było użycie modułów ES jako ostatecznych kodów źródłowych. Następnie przetranspilowałem i połączyłem te moduły na potrzeby serwera i skryptu service worker za pomocą kombinacji interfejsów Babel i Rollup. W moim projekcie pliki z rozszerzeniem .mjs to kod, który znajduje się w module ES.

Serwer

Mając na uwadze te pojęcia i terminologię, zobaczmy, jak powstała moja aplikacja PWA w Stack Overflow. Zacznę od omówienia naszego serwera backendu i wyjaśnię, jak wpisuje się to w ogólną architekturę.

Szukałem połączenia dynamicznego backendu z hostingiem statycznym i postanowiłem skorzystać z platformy Firebase.

Funkcje w Cloud Functions w Firebase automatycznie uruchamiają środowisko oparte na węzłach w odpowiedzi na przychodzące żądanie i integrują się z znaną mi już platformą Express HTTP. Oferuje też gotowe do użycia hosting dla wszystkich zasobów statycznych mojej witryny. Przyjrzyjmy się temu, jak serwer obsługuje żądania.

Gdy przeglądarka wysyła do naszego serwera żądanie nawigacji, przechodzi przez następujący proces:

Omówienie generowania odpowiedzi dotyczącej nawigacji po stronie serwera.

Serwer kieruje żądanie na podstawie adresu URL i używa szablonów do utworzenia pełnego dokumentu HTML. Używam kombinacji danych z interfejsu Stack Exchange API, a także częściowych fragmentów HTML, które serwer przechowuje lokalnie. Gdy mechanizm service worker wie, jak odpowiedzieć, może zacząć przesyłać kod HTML z powrotem do aplikacji internetowej.

Warto bardziej szczegółowo przeanalizować 2 elementy: kierowanie i szablony.

Routing

Jeśli chodzi o routing, chciałem użyć natywnej składni routingu platformy Express. Jest on wystarczająco elastyczny, aby dopasowywać proste prefiksy adresów URL oraz adresy URL, które zawierają parametry w ramach ścieżki. Tu utworzę mapowanie między nazwami tras dla bazowego wzorca ekspresowego, do którego będzie pasować.

const routes = new Map([
  ['about', '/about'],
  ['questions', '/questions/:questionId'],
  ['index', '/'],
]);

export default routes;

Dzięki temu mogę się odwoływać do tego mapowania bezpośrednio w kodzie serwera. Gdy występuje dopasowanie do danego wzorca ekspresowego, odpowiedni moduł obsługi odpowiada szablonom logicznym specyficznym dla pasującej trasy.

import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
  // Templating logic.
});

Szablony po stronie serwera

Na czym polega ta logika tworzenia szablonów? Korzystałem z metody, która polegała na łączeniu częściowych fragmentów HTML jeden po drugim. Ten model dobrze się sprawdza w transmisjach strumieniowych.

Serwer natychmiast odsyła kilka początkowych fragmentów kodu HTML, dzięki czemu przeglądarka jest w stanie od razu wyrenderować tę częściową stronę. Gdy serwer łączy pozostałe źródła danych, przesyła je strumieniowo do przeglądarki do momentu, gdy dokument będzie gotowy.

Aby dowiedzieć się, co mam na myśli, zajrzyj do kodu ekspresowego dla jednej z naszych tras:

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

Korzystając z metody write() obiektu response, i odwołując się do lokalnie przechowywanych szablonów częściowych, mogę natychmiast uruchomić strumień odpowiedzi bez blokowania zewnętrznych źródeł danych. Przeglądarka pobiera początkowy kod HTML, a potem od razu renderuje odpowiedni interfejs i wczytuje wiadomość.

Następna część naszej strony zawiera dane z interfejsu Stack Exchange API. Ich uzyskanie oznacza, że nasz serwer musi wysłać żądanie sieciowe. Aplikacja internetowa nie może renderować żadnych innych elementów, dopóki nie otrzyma odpowiedzi i nie przetworzy pliku, ale użytkownicy nie mogą patrzeć na pusty ekran podczas oczekiwania.

Gdy aplikacja internetowa otrzyma odpowiedź z interfejsu Stack Exchange API, wywołuje niestandardową funkcję szablonów, aby przetłumaczyć dane z interfejsu API na odpowiedni kod HTML.

Język szablonów

Stosowanie szablonów może być zaskakująco sporym zagadnieniem, a ja wybrałam tylko jedno z nich. Postaraj się zastąpić własne rozwiązanie, zwłaszcza jeśli korzystasz ze starszej platformy szablonów.

W moim przypadku zastosowanie miało zastosowanie tylko z literałów szablonów JavaScriptu, gdzie część logiki została podzielona na funkcje pomocnicze. Jedną z korzyści związanych z tworzeniem MPA jest to, że nie trzeba śledzić aktualizacji stanu i ponownie renderować kodu HTML, więc podstawowe podejście, które wygenerowało statyczny kod HTML, sprawdziło się w moim przypadku.

Oto przykład, jak stosuję szablon dynamicznej części indeksu HTML w indeksie mojej aplikacji internetowej. Podobnie jak w przypadku moich tras logika szablonów jest przechowywana w module ES, który można zaimportować zarówno do serwera, jak i do mechanizmu Service Worker.

export function index(tag, items) {
  const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
  const form = `<form method="GET">...</form>`;
  const questionCards = items
    .map(item =>
      questionCard({
        id: item.question_id,
        title: item.title,
      })
    )
    .join('');
  const questions = `<div id="questions">${questionCards}</div>`;
  return title + form + questions;
}

Te funkcje szablonów to w pełni kod JavaScript, dlatego warto podzielić tę logikę na mniejsze funkcje pomocnicze. Tutaj przekazujem każdy element zwrócony w odpowiedzi interfejsu API do jednej z takich funkcji, która tworzy standardowy element HTML ze wszystkimi odpowiednimi atrybutami.

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url="${questionUrl(id)}">${title}</a>`;
}

Na szczególną uwagę zasługuje atrybut danych, który dodaję do każdego linku (data-cache-url) na adres URL interfejsu Stack Exchange API, który jest potrzebny do wyświetlenia odpowiedniego pytania. Pamiętaj o tym. Odtworzę je później.

Wracam do modułu obsługi tras, po zakończeniu tworzenia szablonów przesyłam do przeglądarki końcową część kodu HTML strony i kończę ten strumień. Jest to informacja dla przeglądarki, że renderowanie progresywne zostało zakończone.

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

Oto krótka prezentacja konfiguracji mojego serwera. Użytkownicy, którzy odwiedzają moją aplikację internetową po raz pierwszy, zawsze otrzymają odpowiedź z serwera, ale gdy wróci do niej, mój skrypt service worker zacznie odpowiadać. Spróbujmy.

Skrypt service worker

Omówienie generowania odpowiedzi nawigacyjnej w mechanizmie Service Worker.

Ten diagram powinien wyglądać znajomo – wiele z przedstawionych przeze mnie wcześniej elementów znajduje się tutaj w nieco innej kolejności. Omówmy proces żądań z uwzględnieniem skryptu service worker.

Nasz mechanizm Service Worker obsługuje przychodzące żądania nawigacji dla danego adresu URL i, tak jak mój serwer, korzysta z połączenia routingu i logiki szablonów, aby określić odpowiedź.

Sposób jest taki sam jak wcześniej, ale z kilkoma elementami podstawowymi niskiego poziomu, takimi jak fetch() i Cache Storage API. Używam tych źródeł danych do tworzenia odpowiedzi HTML, którą skrypt service worker przekazuje z powrotem do aplikacji internetowej.

Workbox

Zamiast zaczynać od zera, utworzę skrypt service worker na bazie bibliotek wysokiego poziomu o nazwie Workbox. Stanowi solidne podstawy dla logiki buforowania, routingu i generowania odpowiedzi każdego skryptu service worker.

Routing

Tak jak w przypadku kodu po stronie serwera, skrypt service worker musi wiedzieć, jak dopasować żądanie przychodzące do odpowiedniej logiki odpowiedzi.

W moim zamierzeniu chcieliśmy przetłumaczyć każdą trasę ekspresową na odpowiednie wyrażenie regularne, korzystając z pomocnej biblioteki regexparam. Po wykonaniu translacji mogę skorzystać z wbudowanej obsługi routingu wyrażeń regularnych w Workbox.

Po zaimportowaniu modułu z wyrażeniami regularnymi rejestruję każde wyrażenie regularne na routerze Workbox. W ramach każdej trasy jestem w stanie przedstawić niestandardową logikę szablonów do generowania odpowiedzi. Stosowanie szablonów w mechanizmie Service Worker jest nieco bardziej skomplikowane niż na moim serwerze backendu, ale Workbox pomaga wykonywać większość ciężkich zadań.

import regExpRoutes from './regexp-routes.mjs';

workbox.routing.registerRoute(
  regExpRoutes.get('index')
  // Templating logic.
);

Statyczne buforowanie zasobów

Ważną częścią historii tworzenia szablonów jest to, aby moje częściowe szablony HTML były dostępne lokalnie przez interfejs Cache Storage API i były aktualne, gdy wdrażam zmiany w aplikacji internetowej. Konserwacja pamięci podręcznej może być podatna na błędy, jeśli wykonuje się ją ręcznie, dlatego w ramach procesu kompilacji korzystam z Workbox, aby obsługiwać występowanie w pamięci podręcznej.

Informuję Workbox, które adresy URL mają być wstępnie buforowane, używając pliku konfiguracji, który wskazuje katalog zawierający wszystkie moje zasoby lokalne oraz zestaw wzorców do dopasowania. Plik jest automatycznie odczytywany przez interfejs wiersza poleceń Workspace, który jest run przy każdym odbudowywaniu witryny.

module.exports = {
  globDirectory: 'build',
  globPatterns: ['**/*.{html,js,svg}'],
  // Other options...
};

Pole robocze zapisuje migawkę zawartości każdego pliku i automatycznie wstrzykuje tę listę adresów URL oraz wersji do końcowego pliku skryptu service worker. Workbox ma teraz wszystko, czego potrzebuje, aby pliki z pamięci podręcznej były zawsze dostępne i aktualizowane. Wynik to plik service-worker.js, który zawiera coś podobnego do tych:

workbox.precaching.precacheAndRoute([
  {
    url: 'partials/about.html',
    revision: '518747aad9d7e',
  },
  {
    url: 'partials/foot.html',
    revision: '69bf746a9ecc6',
  },
  // etc.
]);

Dla osób, które korzystają z bardziej złożonego procesu kompilacji, Workbox oprócz interfejsu wiersza poleceń udostępnia zarówno wtyczkę webpack, jak i ogólny moduł węzłów.

transmisje

Teraz chcę, aby skrypt service worker natychmiast przesyłał z pamięci podręcznej część kodu HTML do aplikacji internetowej. Jest to kluczowy element „stabilnego szybkości” – zawsze od razu dostrzegam coś ważnego na ekranie. Na szczęście jest to możliwe dzięki użyciu interfejsu Streams API w skrypcie service worker.

Być może znasz już interfejs Streams API. Mój kolega, Jake Archibald, śpiewa swoje pochwały od lat. Przełożył odważną prognozę, że rok 2016 będzie rokiem strumieniowania danych z sieci. Streams API jest dziś tak samo niesamowity jak dwa lata temu, ale ma zasadniczą różnicę.

W przeszłości tylko Chrome obsługiwał strumienie, ale interfejs Streams API jest już obsługiwany. Ogólna historia jest pozytywna, a dzięki odpowiedniemu kodowi zastępczemu nic nie powstrzyma Cię przed używaniem strumieni w skrypcie service worker.

Cóż... być może jedna rzecz Cię powstrzymuje, i to niepokoi Cię, jak faktycznie działa interfejs Streams API. Zapewnia on bardzo zaawansowany zestaw podstawowych elementów, a deweloperzy, którzy potrafią z niego korzystać, mogą tworzyć złożone przepływy danych, takie jak:

const stream = new ReadableStream({
  pull(controller) {
    return sources[0]
      .then(r => r.read())
      .then(result => {
        if (result.done) {
          sources.shift();
          if (sources.length === 0) return controller.close();
          return this.pull(controller);
        } else {
          controller.enqueue(result.value);
        }
      });
  },
});

Zrozumienie wszystkich konsekwencji tego kodu może nie być jednak przeznaczone dla wszystkich. Zamiast analizować tę logikę, omówmy moje podejście do strumieniowego przesyłania danych w skryptach service worker.

Używam zupełnie nowego, wysokiego poziomu opakowania: workbox-streams. Mogę przekazywać ją zarówno z pamięci podręcznych, jak i z różnych źródeł, które mogą pochodzić z sieci. Workbox służy do koordynowania poszczególnych źródeł i łączenia ich w jedną odpowiedź strumieniową.

Dodatkowo Workbox automatycznie wykrywa, czy interfejs Streams API jest obsługiwany, a jeśli nie, tworzy równoważną odpowiedź niestrumieniową. Oznacza to, że nie musisz się martwić o pisanie wartości zastępczych, ponieważ strumienie są prawie obsługiwane przez przeglądarkę w 100%.

Buforowanie środowiska wykonawczego

Sprawdźmy, jak mój skrypt service worker postępuje z danymi środowiska wykonawczego za pomocą interfejsu Stack Exchange API. Korzystam z wbudowanej obsługi Workspace w przypadku strategii buforowania w czasie oczekiwania na ponowną weryfikację wraz z wygaśnięciem, aby zapewnić, że miejsce na dane aplikacji internetowej nie będzie ograniczone.

W Workbox konfigurowałem 2 strategie, aby obsługiwać różne źródła składające się na odpowiedź strumieniową. Wystarczyło kilka wywołań funkcji i konfiguracji, dzięki którym możemy zrobić to, co w przeciwnym razie wymagałoby setek odręcznych wierszy kodu.

const cacheStrategy = workbox.strategies.cacheFirst({
  cacheName: workbox.core.cacheNames.precache,
});

const apiStrategy = workbox.strategies.staleWhileRevalidate({
  cacheName: API_CACHE_NAME,
  plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});

Pierwsza strategia odczytuje dane wstępnie zapisane w pamięci podręcznej, takie jak nasze częściowe szablony HTML.

Druga strategia wykorzystuje logikę buforowania „Nieaktualne podczas ponownej weryfikacji”, wraz z ostatnio najrzadziej wykorzystywaną pamięcią podręczną po osiągnięciu 50 wpisów.

Teraz mogę tylko powiedzieć Workbox, jak z nich korzystać do konstruowania kompletnej odpowiedzi na żądanie strumieniowego przesyłania danych. Przekazuję tablicę źródeł jako funkcje, które są wykonywane natychmiast. Pole robocze pobiera wynik z każdego źródła i przesyła go strumieniowo do aplikacji internetowej, opóźniając go tylko wtedy, gdy następna funkcja w tablicy nie została jeszcze zakończona.

workbox.streams.strategy([
  () => cacheStrategy.makeRequest({request: '/head.html'}),
  () => cacheStrategy.makeRequest({request: '/navbar.html'}),
  async ({event, url}) => {
    const tag = url.searchParams.get('tag') || DEFAULT_TAG;
    const listResponse = await apiStrategy.makeRequest(...);
    const data = await listResponse.json();
    return templates.index(tag, data.items);
  },
  () => cacheStrategy.makeRequest({request: '/foot.html'}),
]);

Pierwsze 2 źródła to szablony częściowe wstępnie zapisane w pamięci podręcznej, odczytywane bezpośrednio z interfejsu Cache Storage API, dlatego zawsze będą dostępne natychmiast. Dzięki temu nasza implementacja skryptu service worker będzie niezawodnie szybko odpowiadać na żądania, podobnie jak kod po stronie serwera.

Nasza kolejna funkcja źródłowa pobiera dane z interfejsu Stack Exchange API i przetwarza odpowiedź do kodu HTML oczekiwanego przez aplikację internetową.

Strategia „Nieaktualne podczas ponownej weryfikacji” oznacza, że jeśli w pamięci podręcznej mam odpowiedź na to wywołanie interfejsu API, mogę natychmiast przesłać ją na stronę, aktualizując wpis w pamięci podręcznej „w tle” na czas następnego żądania.

Na koniec przesyłam strumieniowo kopię stopki z pamięci podręcznej i zamykam ostateczne tagi HTML, aby dokończyć odpowiedź.

Udostępnianie kodu umożliwia synchronizację wszystkich elementów

Przekonasz się, że niektóre elementy kodu skryptu service worker wyglądają znajomo. Zasada częściowa kodu HTML i szablonów używanych przez mechanizm Service Worker jest taka sama jak ta, z której korzysta mój moduł obsługi po stronie serwera. Udostępnianie kodu zapewnia użytkownikom spójne wrażenia, niezależnie od tego, czy odwiedzają moją aplikację internetową po raz pierwszy, czy wracają na stronę renderowaną przez skrypt service worker. To właśnie piękno izomorficznego JavaScriptu.

Dynamiczne, progresywne ulepszenia

Po zapoznaniu się zarówno z serwerem, jak i skryptem service worker moja aplikacja PWA jest jeszcze jedna, dlatego muszę się skupić: na każdej mojej stronie jest uruchamiana niewielka część kodu JavaScript, gdy po jej przesłaniu wyświetlą się w całości.

Ten kod stopniowo zwiększa wygodę użytkowników, ale nie jest kluczowy – aplikacja internetowa będzie nadal działać, nawet jeśli nie zostanie uruchomiona.

Metadane strony

Moja aplikacja używa języka JavaScipt po stronie klienta do aktualizowania metadanych strony na podstawie odpowiedzi interfejsu API. Na każdej stronie używam tego samego początkowego fragmentu kodu HTML zapisanego w pamięci podręcznej, więc w nagłówku aplikacji internetowej znajdują się ogólne tagi. Jednak dzięki koordynacji pracy między szablonami i kodem po stronie klienta mogę zaktualizować tytuł okna, korzystając z metadanych dotyczących konkretnej strony.

W ramach kodu szablonów umieszczam tag skryptu zawierający ciąg znaków o odpowiedniej zmianie znaczenia.

const metadataScript = `<script>
  self._title = '${escape(item.title)}';
</script>`;

Następnie, po załadowaniu strony, odczytuję ten ciąg znaków i aktualizuję tytuł dokumentu.

if (self._title) {
  document.title = unescape(self._title);
}

Jeśli w swojej aplikacji internetowej chcesz zaktualizować inne elementy metadanych konkretnej strony, możesz to zrobić w ten sam sposób.

Wrażenia użytkowników dotyczące trybu offline

Kolejne progresywne ulepszenie, które dostaliśmy, zwraca uwagę na nasze możliwości w trybie offline. Udało mi się utworzyć niezawodną aplikację PWA i chcem, aby użytkownicy wiedzieli, że nawet gdy są offline, mogą wczytywać wcześniej odwiedzone strony.

Najpierw używam interfejsu Cache Storage API do pobierania listy wszystkich żądań interfejsu API zapisanych w pamięci podręcznej, a potem przekształcam ją na listę adresów URL.

Pamiętasz te specjalne atrybuty danych, o których mówiłem. Każdy z nich zawiera adres URL żądania do interfejsu API potrzebny do wyświetlenia pytania? Mogę porównać te atrybuty danych z listą adresów URL w pamięci podręcznej i utworzyć tablicę ze wszystkimi niepasującymi linkami z pytaniami.

Gdy przeglądarka przechodzi w tryb offline, przeglądam listę niebuforowanych linków i przyciemniam te, które nie działają. Pamiętaj, że jest to tylko wizualna wskazówka dla użytkownika, czego może się spodziewać na tych stronach – nie wyłączam linków ani nie przeszkadzam użytkownikowi w nawigacji.

const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);

const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filter(card => {
  return !cachedUrls.includes(card.dataset.cacheUrl);
});

const offlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '0.3';
  }
};

const onlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '1.0';
  }
};

window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);

Częste problemy

Teraz omówię mój sposób tworzenia wielostronicowej PWA. Jest wiele czynników, które musisz wziąć pod uwagę, planując własną strategię. Możliwe, że dokonasz innych wyborów niż ja. Elastyczność to jedna z największych zalet tworzenia stron internetowych.

Podejmując własne decyzje architektoniczne, często spotykają się Państwo z kilkoma pułapami. Chcę zaoszczędzić trochę bólu.

Nie zapisuj całego kodu HTML w pamięci podręcznej

Odradzamy przechowywanie kompletnych dokumentów HTML w pamięci podręcznej. Po pierwsze, to strata miejsca. Jeśli Twoja aplikacja internetowa używa tej samej podstawowej struktury kodu HTML na wszystkich jej stronach, kopie tych samych znaczników będą przechowywane wielokrotnie.

Co ważniejsze, jeśli wprowadzisz zmianę w udostępnionej strukturze HTML witryny, każda ze stron zapisanych w pamięci podręcznej nadal utknie w poprzednim układzie. Wyobraź sobie, że powracający użytkownik widzi mieszankę starych i nowych stron.

Dryf serwera / skryptu service worker

Inną pułapką, której należy uniknąć, jest brak synchronizacji serwera i skryptu service worker. Podejściem było użycie izomorficznego kodu JavaScript, by ten sam kod był uruchamiany w obu miejscach. W zależności od dotychczasowej architektury serwera nie zawsze jest to możliwe.

Niezależnie od podejmowanych przez Ciebie decyzji dotyczących architektury, warto mieć strategię uruchamiania równoważnego kodu routingu i szablonów na serwerze i skrypcie service worker.

Najgorsze scenariusze

Niespójny układ lub projekt

Co się stanie, jeśli zignorujesz te pułapki? Każdy użytkownik może spotkać się z niepowodzeniami, ale w najgorszym przypadku powracający użytkownik odwiedza zapisaną w pamięci podręcznej stronę o bardzo nieaktualnym układzie – np. z nieaktualnym tekstem nagłówka lub klasą CSS, która nie jest już aktualna.

W najgorszym przypadku: nieprawidłowy routing

Użytkownik może też natrafić na adres URL obsługiwany przez Twój serwer, ale nie przez mechanizm Service Worker. Witryna pełna układów zombie i ślepych zaułków nie jest niezawodną PWA.

Jak osiągnąć sukces – wskazówki

Ale nie tylko Ty masz ten problem. Poniższe wskazówki pomogą Ci uniknąć tych problemów:

Używanie bibliotek szablonów i routingu z implementacjami w wielu językach

Używaj bibliotek szablonów i routingu, które mają implementacje JavaScript. Wiem, że nie każdy programista może pozwolić sobie na przeniesienie obecnego serwera WWW i użycie szablonów.

Jednak wiele popularnych platform do tworzenia szablonów i routingu ma implementacje w wielu językach. Jeśli znajdziesz narzędzie, które obsługuje JavaScript, a także język bieżącego serwera, jesteś o krok bliżej do zapewnienia synchronizacji skryptu service worker z serwerem.

Preferuj szablony sekwencyjne zamiast zagnieżdżonych

Zalecamy też użycie serii szablonów, które można przesyłać strumieniowo jeden po drugim. Bardziej skomplikowana logika szablonów jest w porządku, o ile tylko początkowe fragmenty kodu HTML można przesyłać tak szybko, jak to możliwe.

Zapisywanie w pamięci podręcznej treści statycznej i dynamicznej w skrypcie service worker

Aby uzyskać najlepszą wydajność, należy wstępnie buforować wszystkie kluczowe zasoby statyczne witryny. Musisz też skonfigurować logikę buforowania środowiska wykonawczego, by obsługiwać zawartość dynamiczną, np. żądania do interfejsu API. Dzięki polowi roboczemu możesz tworzyć własne rozwiązania na bazie sprawdzonych i gotowych do wdrożenia strategii produkcyjnych zamiast wdrażać je od zera.

Blokuj w sieci tylko wtedy, gdy jest to absolutnie konieczne

W związku z tym należy blokować sieć tylko wtedy, gdy nie ma możliwości przesyłania strumieniowego odpowiedzi z pamięci podręcznej. Wyświetlenie odpowiedzi interfejsu API zapisanej w pamięci podręcznej może często zapewnić lepsze wrażenia użytkownika niż czekanie na aktualne dane.

Zasoby