Porozmawiajmy o architekturze.
Omówię ważny, ale potencjalnie niezrozumiały temat: architekturę aplikacji internetowej, a w szczególności to, jak decyzje dotyczące architektury wpływają na tworzenie progresywnej aplikacji internetowej.
„Architektura” może brzmieć niejasno i nie od razu może być jasne, dlaczego jest to ważne. Jednym ze sposobów na przemyślenie architektury jest zadanie sobie tych pytań: jaki kod HTML jest wczytywany, gdy użytkownik odwiedza stronę w mojej witrynie? A co się wczytuje, gdy użytkownik przechodzi na inną stronę?
Odpowiedzi na te pytania nie zawsze są proste, a gdy zaczniesz myśleć o progresywnych aplikacjach internetowych, mogą stać się jeszcze bardziej skomplikowane. Moim celem jest przedstawienie jednej z możliwych architektur, która moim zdaniem jest skuteczna. W tym artykule będę oznaczać decyzje, które podjąłem, jako „moje podejście” do tworzenia progresywnej aplikacji internetowej.
Możesz swobodnie korzystać z mojego podejścia podczas tworzenia własnej aplikacji PWA, ale pamiętaj, że zawsze istnieją inne, równie dobre rozwiązania. Mam nadzieję, że zobaczenie, jak wszystkie elementy do siebie pasują, zainspiruje Cię i poczujesz, że możesz dostosować ten proces do swoich potrzeb.
Aplikacja PWA Stack Overflow
Do tego artykułu dołączam aplikację Stack Overflow PWA. Spędzam dużo czasu na czytaniu i udzielaniu się na Stack Overflow. Chciałem stworzyć aplikację internetową, która ułatwiłaby przeglądanie najczęstszych pytań na dany temat. Jest on oparty na publicznym interfejsie Stack Exchange API. Jest to rozwiązanie typu open source. Więcej informacji znajdziesz w projekcie na GitHubie.
aplikacje wielostronicowe (MPA),
Zanim przejdziemy do szczegółów, zdefiniujmy kilka pojęć i wyjaśnijmy elementy technologii, na której opiera się ta funkcja. Najpierw omówię to, co nazywam „aplikacjami wielostronicowymi” lub „MPA”.
MPA to wymyślna nazwa tradycyjnej architektury używanej od początku istnienia internetu. Za każdym razem, gdy użytkownik przechodzi na nowy adres URL, przeglądarka stopniowo renderuje kod HTML właściwy dla danej strony. Nie ma próby zachowania stanu strony ani treści między nawigacjami. Za każdym razem, gdy odwiedzasz nową stronę, zaczynasz od nowa.
Jest to przeciwieństwo modelu aplikacji jednostronicowej (SPA) do tworzenia aplikacji internetowych, w którym przeglądarka uruchamia kod JavaScript, aby zaktualizować istniejącą stronę, gdy użytkownik odwiedza nową sekcję. Zarówno aplikacje SPA, jak i MPA są równie dobrymi modelami, ale w tym poście chciałem omówić koncepcje PWA w kontekście aplikacji wielostronicowej.
Niezawodna szybkość
Pewnie zdarzyło Ci się usłyszeć, jak ja (i wiele innych osób) używam określenia „progresywna aplikacja internetowa” lub „PWA”. Niektóre informacje podstawowe możesz już znać z innych miejsc w tej witrynie.
PWA to aplikacja internetowa, która zapewnia użytkownikom najwyższą jakość i naprawdę zasługuje na miejsce na ekranie głównym. Akronim „FIRE” (Fast, Integrated, Reliable, Engaging) podsumowuje wszystkie cechy, które należy wziąć pod uwagę podczas tworzenia PWA.
W tym artykule skupię się na podzbiorze tych atrybutów: Szybkość i Niezawodność.
Szybkość: chociaż „szybkość” oznacza różne rzeczy w różnych kontekstach, omówię korzyści związane z szybkością ładowania jak najmniejszej ilości danych z sieci.
Niezawodność: sama szybkość jednak nie wystarczy. Aby aplikacja internetowa działała jak PWA, musi być niezawodna. Musi być na tyle odporna, aby zawsze coś wczytywać, nawet jeśli jest to tylko dostosowana strona błędu, niezależnie od stanu sieci.
Niezawodna szybkość: na koniec nieco zmodyfikuję definicję PWA i przyjrzę się temu, co oznacza stworzenie czegoś, co jest niezawodnie szybkie. Nie wystarczy, że aplikacja jest szybka i niezawodna tylko wtedy, gdy sieć ma małe opóźnienia. Niezawodna szybkość oznacza, że aplikacja internetowa działa szybko i stabilnie niezależnie od warunków sieciowych.
Technologie wspomagające: skrypty service worker i interfejs Cache Storage API
Aplikacje PWA wysoko stawiają poprzeczkę, jeśli chodzi o szybkość i odporność. Na szczęście platforma internetowa oferuje pewne elementy, które pozwalają osiągnąć taką wydajność. Mam na myśli skrypty service worker i interfejs Cache Storage API.
Możesz utworzyć skrypt service worker, który nasłuchuje żądań przychodzących, przekazuje niektóre z nich do sieci i zapisuje kopię odpowiedzi do wykorzystania w przyszłości za pomocą interfejsu Cache Storage API.
Gdy następnym razem aplikacja internetowa wyśle to samo żądanie, skrypt service worker może sprawdzić pamięć podręczną i zwrócić zapisaną wcześniej odpowiedź.
Unikanie sieci, gdy tylko jest to możliwe, jest kluczowym elementem zapewniania niezawodnej i szybkiej wydajności.
„Izomorficzny” JavaScript
Chcę jeszcze omówić koncepcję, która jest czasem nazywana „izomorficznym” lub „uniwersalnym” JavaScriptem. Mówiąc prościej, chodzi o to, że ten sam kod JavaScriptu może być udostępniany różnym środowiskom wykonawczym. Podczas tworzenia PWA chciałem udostępnić kod JavaScriptu między serwerem backendu a skryptem service worker.
Istnieje wiele prawidłowych sposobów udostępniania kodu w ten sposób, ale moje podejście polegało na użyciu modułów ES jako ostatecznego kodu źródłowego. Następnie transpilowałem i połączyłem te moduły dla serwera i skryptu service worker, używając kombinacji 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ę, przyjrzyjmy się, jak faktycznie utworzyłem moją aplikację PWA Stack Overflow. Zacznę od omówienia serwera backendu i wyjaśnię, jak pasuje on do ogólnej architektury.
Szukałem połączenia dynamicznego backendu ze statycznym hostingiem i postanowiłem użyć platformy Firebase.
Firebase Cloud Functions automatycznie uruchamia środowisko oparte na Node, gdy pojawia się żądanie przychodzące, i integruje się z popularną platformą Express HTTP, którą już znam. Zapewnia też gotowy hosting wszystkich zasobów statycznych mojej witryny. Zobaczmy, jak serwer obsługuje żądania.
Gdy przeglądarka wysyła żądanie nawigacji do naszego serwera, przechodzi ono przez ten proces:
Serwer kieruje żądanie na podstawie adresu URL i używa logiki szablonów do utworzenia pełnego dokumentu HTML. Używam kombinacji danych z interfejsu Stack Exchange API oraz częściowych fragmentów HTML, które serwer przechowuje lokalnie. Gdy skrypt service worker wie, jak odpowiedzieć, może zacząć przesyłać strumieniowo kod HTML z powrotem do aplikacji internetowej.
Warto przyjrzeć się bliżej 2 elementom tego obrazu: routingowi i szablonom.
Routing
Jeśli chodzi o routing, moje podejście polegało na użyciu natywnej składni routingu platformy Express. Jest on na tyle elastyczny, że może dopasowywać proste prefiksy adresów URL, a także adresy URL, które zawierają parametry jako część ścieżki. Tutaj tworzę mapowanie między nazwami tras a wzorcem Express, do którego mają być dopasowywane.
const routes = new Map([
['about', '/about'],
['questions', '/questions/:questionId'],
['index', '/'],
]);
export default routes;
Następnie mogę odwoływać się do tego mapowania bezpośrednio z kodu serwera. Gdy zostanie znalezione dopasowanie do danego wzorca Express, odpowiedni moduł obsługi odpowiada za pomocą logiki szablonu właściwej dla pasującej trasy.
import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
// Templating logic.
});
Szablony po stronie serwera
Jak wygląda logika szablonów? Zdecydowałem się na podejście, które polegało na łączeniu fragmentów kodu HTML w sekwencję, jeden po drugim. Ten model dobrze sprawdza się w przypadku transmisji na żywo.
Serwer natychmiast odsyła początkowy szablon HTML, a przeglądarka może od razu wyrenderować tę częściową stronę. Gdy serwer zbierze pozostałe źródła danych, przesyła je do przeglądarki, aż dokument będzie kompletny.
Aby zobaczyć, co mam na myśli, spójrz na kod Express 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();
});
Dzięki użyciu metody write() obiektu response i odwołaniu się do przechowywanych lokalnie szablonów częściowych mogę natychmiast rozpocząć strumień odpowiedzi bez blokowania zewnętrznego źródła danych. Przeglądarka pobiera ten początkowy kod HTML i natychmiast renderuje czytelny interfejs oraz komunikat o wczytywaniu.
Kolejna część naszej strony korzysta z danych z interfejsu Stack Exchange API. Aby uzyskać te dane, nasz serwer musi wysłać żądanie sieciowe. Aplikacja internetowa nie może wyrenderować niczego innego, dopóki nie otrzyma odpowiedzi i jej nie przetworzy, ale przynajmniej użytkownicy nie patrzą na pusty ekran, gdy czekają.
Gdy aplikacja internetowa otrzyma odpowiedź z interfejsu Stack Exchange API, wywołuje niestandardową funkcję szablonu, aby przekształcić dane z interfejsu API na odpowiedni kod HTML.
Język szablonów
Szablony mogą być zaskakująco kontrowersyjnym tematem, a to, co wybrałem, to tylko jedno z wielu podejść. Warto zastąpić je własnym rozwiązaniem, zwłaszcza jeśli masz starsze powiązania z istniejącą platformą szablonów.
W moim przypadku najlepiej było polegać na literałach szablonu JavaScriptu, z pewną logiką podzieloną na funkcje pomocnicze. Jedną z zalet tworzenia aplikacji wielostronicowej jest to, że nie trzeba śledzić aktualizacji stanu i ponownie renderować kodu HTML, więc w moim przypadku sprawdziło się podstawowe podejście, które generowało statyczny kod HTML.
Oto przykład tworzenia szablonu dynamicznej części HTML indeksu aplikacji internetowej. Podobnie jak w przypadku tras logika szablonów jest przechowywana w module ES, który można zaimportować zarówno na serwerze, jak i w skrypcie 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 szablonu są napisane w czystym języku JavaScript i warto w razie potrzeby podzielić logikę na mniejsze funkcje pomocnicze. Tutaj przekazuję każdy z elementów zwróconych 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, ustawiony na adres URL interfejsu Stack Exchange API, którego potrzebuję, aby wyświetlić odpowiednie pytanie. Pamiętaj o tym. Wrócę do tego później.
Wracając do modułu obsługi tras, po zakończeniu tworzenia szablonu przesyłam strumieniowo ostatnią część kodu HTML strony do przeglądarki i kończę strumień. Jest to sygnał 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();
});
To była krótka prezentacja konfiguracji mojego serwera. Użytkownicy, którzy odwiedzają moją aplikację internetową po raz pierwszy, zawsze otrzymają odpowiedź z serwera, ale gdy użytkownik powróci do mojej aplikacji internetowej, zacznie odpowiadać skrypt service worker. Zajmijmy się tą kwestią.
Skrypt service worker
Ten diagram powinien wyglądać znajomo – wiele z omówionych wcześniej elementów znajduje się tutaj w nieco innym układzie. Prześledźmy proces żądania, biorąc pod uwagę skrypt service worker.
Nasz skrypt service worker obsługuje przychodzące żądanie nawigacji dla danego adresu URL i, podobnie jak mój serwer, używa kombinacji logiki routingu i szablonów, aby określić, jak odpowiedzieć.
Podejście jest takie samo jak wcześniej, ale z innymi elementami podstawowymi niskiego poziomu, takimi jak fetch() i interfejs Cache Storage API. Używam tych źródeł danych do utworzenia odpowiedzi HTML, którą skrypt service worker przekazuje z powrotem do aplikacji internetowej.
Workbox
Zamiast zaczynać od zera z elementami podstawowymi niskiego poziomu, utworzę skrypt service worker na podstawie zestawu bibliotek wysokiego poziomu o nazwie Workbox. Zapewnia solidną podstawę dla logiki buforowania, routingu i generowania odpowiedzi przez skrypt service worker.
Routing
Podobnie jak w przypadku kodu po stronie serwera, skrypt service worker musi wiedzieć, jak dopasować przychodzące żądanie do odpowiedniej logiki odpowiedzi.
Moje podejście polegało na przetłumaczeniu każdej trasy Express na odpowiadające jej wyrażenie regularne przy użyciu przydatnej biblioteki regexparam. Po wykonaniu tego tłumaczenia 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 w routerze Workbox. W każdej ścieżce mogę podać niestandardową logikę szablonu, aby wygenerować odpowiedź. Tworzenie szablonów w skrypcie service worker jest nieco bardziej skomplikowane niż na serwerze backendu, ale Workbox pomaga w wykonaniu wielu trudnych zadań.
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templating logic.
);
Przechowywanie zasobów statycznych w pamięci podręcznej
Jednym z kluczowych elementów tworzenia szablonów jest upewnienie się, że moje częściowe szablony HTML są dostępne lokalnie za pomocą interfejsu Cache Storage API i są aktualizowane, gdy wdrażam zmiany w aplikacji internetowej. Ręczne zarządzanie pamięcią podręczną może być podatne na błędy, dlatego używam Workbox do obsługi wstępnego buforowania w ramach procesu kompilacji.
Za pomocą pliku konfiguracji wskazuję Workbox, które adresy URL mają być wstępnie zapisywane w pamięci podręcznej. Wskazuję katalog zawierający wszystkie moje zasoby lokalne oraz zestaw wzorców do dopasowania. Ten plik jest automatycznie odczytywany przez interfejs wiersza poleceń Workbox, który jest uruchamiany za każdym razem, gdy przebudowuję witrynę.
module.exports = {
globDirectory: 'build',
globPatterns: ['**/*.{html,js,svg}'],
// Other options...
};
Workbox robi zrzut zawartości każdego pliku i automatycznie wstawia listę adresów URL i wersji do mojego ostatecznego pliku skryptu service worker. Workbox ma teraz wszystko, czego potrzebuje, aby wstępnie zapisane w pamięci podręcznej pliki były zawsze dostępne i aktualne. Efektem będzie plik service-worker.js, który zawiera coś podobnego do tego:
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746a9ecc6',
},
// etc.
]);
Dla osób, które używają bardziej złożonego procesu kompilacji, Workbox ma zarówno wtyczkę webpack, jak i ogólny moduł węzła, a także interfejs wiersza poleceń.
Streaming
Następnie chcę, aby skrypt service worker natychmiast przesłał ten wstępnie zapisany w pamięci podręcznej częściowy kod HTML z powrotem do aplikacji internetowej. To kluczowy element „niezawodnej szybkości” – zawsze od razu widzę na ekranie coś istotnego. Na szczęście jest to możliwe dzięki użyciu interfejsu Streams API w skrypcie service worker.
Być może słyszeliście już o interfejsie Streams API. Mój kolega Jake Archibald od lat zachwala tę funkcję. Odważnie przewidywał, że rok 2016 będzie rokiem transmisji internetowych. Interfejs Streams API jest dziś tak samo świetny jak 2 lata temu, ale z jedną kluczową różnicą.
Wtedy tylko Chrome obsługiwał strumienie, ale obecnie interfejs Streams API jest obsługiwany przez więcej przeglądarek. Ogólnie rzecz biorąc, sytuacja jest pozytywna, a dzięki odpowiedniemu kodowi zastępczemu nic nie stoi na przeszkodzie, aby już dziś używać strumieni w skrypcie service worker.
Cóż… może być jedna rzecz, która Cię powstrzymuje, a jest nią zrozumienie, jak działa interfejs Streams API. Udostępnia on bardzo zaawansowany zestaw elementów podstawowych, a deweloperzy, którzy potrafią go używać, 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);
}
});
},
});
Jednak zrozumienie wszystkich konsekwencji tego kodu może nie być dla wszystkich. Zamiast analizować tę logikę, porozmawiajmy o moim podejściu do strumieniowania skryptu service worker.
Używam zupełnie nowego kodu towarzyszącego wysokiego poziomu, workbox-streams.
Dzięki temu mogę przekazywać je w różnych źródłach strumieniowych, zarówno z pamięci podręcznej, jak i danych środowiska wykonawczego, które mogą pochodzić z sieci. Workbox zajmuje się koordynowaniem poszczególnych źródeł i łączeniem 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ź bez strumieniowania. Oznacza to, że nie musisz się martwić o pisanie kreacji zastępczych, ponieważ strumienie są coraz bliżej 100% obsługi przez przeglądarki.
Buforowanie w środowisku wykonawczym
Zobaczmy, jak mój skrypt service worker radzi sobie z danymi czasu działania z interfejsu Stack Exchange API. Korzystam z wbudowanej obsługi strategii buforowania stale-while-revalidate w Workbox oraz z wygaśnięcia, aby pamięć aplikacji internetowej nie rosła bez ograniczeń.
W Workbox skonfigurowałem 2 strategie obsługi różnych źródeł, które będą tworzyć odpowiedź strumieniową. Dzięki kilku wywołaniom funkcji i konfiguracji Workbox pozwala nam zrobić to, co w innym przypadku wymagałoby setek wierszy kodu napisanego ręcznie.
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, które zostały wstępnie zapisane w pamięci podręcznej, np. nasze częściowe szablony HTML.
Druga strategia implementuje logikę buforowania stale-while-revalidate, wraz z wygaśnięciem pamięci podręcznej najmniej używanych elementów po osiągnięciu 50 wpisów.
Teraz, gdy mam już te strategie, muszę tylko poinformować Workbox, jak ich używać do tworzenia pełnej odpowiedzi strumieniowej. Przekazuję tablicę źródeł jako funkcje, a każda z tych funkcji zostanie natychmiast wykonana. Workbox pobiera wynik z każdego źródła i przesyła go do aplikacji internetowej po kolei, opóźniając tylko wtedy, gdy następna funkcja w tablicy nie została jeszcze ukoń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 wstępnie zapisane w pamięci podręcznej częściowe szablony odczytywane bezpośrednio z interfejsu Cache Storage API, dzięki czemu zawsze będą dostępne natychmiast. Dzięki temu nasza implementacja skryptu service worker będzie szybko i niezawodnie odpowiadać na żądania, podobnie jak kod po stronie serwera.
Nasza następna funkcja źródłowa pobiera dane z interfejsu Stack Exchange API i przetwarza odpowiedź na kod HTML, którego oczekuje aplikacja internetowa.
Strategia stale-while-revalidate oznacza, że jeśli mam wcześniej zapisaną w pamięci podręcznej odpowiedź na to wywołanie interfejsu API, będę mógł natychmiast przesłać ją strumieniowo na stronę, aktualizując wpis w pamięci podręcznej „w tle” na potrzeby następnego żądania.
Na koniec przesyłam strumieniowo zapisaną w pamięci podręcznej kopię stopki i zamykam końcowe tagi HTML, aby zakończyć odpowiedź.
Udostępnianie kodu zapewnia synchronizację
Zauważysz, że niektóre fragmenty kodu skryptu service worker wyglądają znajomo. Częściowy kod HTML i logika szablonów używane przez skrypt service worker są identyczne z tymi, których używa moduł obsługi po stronie serwera. Dzięki temu użytkownicy mają spójne wrażenia, niezależnie od tego, czy odwiedzają moją aplikację internetową po raz pierwszy, czy wracają na stronę wyrenderowaną przez skrypt service worker. Na tym polega moc izomorficznego JavaScriptu.
dynamiczne, progresywne ulepszenia;
Omówiliśmy już serwer i skrypt service worker mojej aplikacji PWA, ale pozostał jeszcze jeden element logiki: niewielka ilość kodu JavaScript, który jest uruchamiany na każdej stronie po jej pełnym przesłaniu.
Ten kod stopniowo ulepsza wrażenia użytkownika, ale nie jest kluczowy – aplikacja internetowa będzie działać, nawet jeśli nie zostanie uruchomiony.
Metadane strony
Moja aplikacja używa kodu JavaScript po stronie klienta do aktualizowania metadanych strony na podstawie odpowiedzi interfejsu API. Ponieważ używam tego samego początkowego fragmentu zapisanego w pamięci podręcznej kodu HTML dla każdej strony, aplikacja internetowa kończy z ogólnymi tagami w nagłówku dokumentu. Jednak dzięki koordynacji między szablonami a kodem po stronie klienta mogę aktualizować tytuł okna, korzystając z metadanych na wybranych stronach.
W ramach kodu szablonu moje podejście polega na dołączeniu tagu skryptu zawierającego prawidłowo ucieczkowy ciąg znaków.
const metadataScript = `<script>
self._title = '${escape(item.title)}';
</script>`;
Następnie, gdy strona się załaduje, odczytuję ten ciąg znaków i aktualizuję tytuł dokumentu.
if (self._title) {
document.title = unescape(self._title);
}
Jeśli chcesz zaktualizować inne metadane na wybranych stronach w swojej aplikacji internetowej, możesz postępować w ten sam sposób.
Offline UX
Inne dodane przeze mnie progresywne ulepszenie służy do zwrócenia uwagi na nasze możliwości offline. Mam niezawodną aplikację PWA i chcę, aby użytkownicy wiedzieli, że gdy są offline, nadal mogą wczytywać wcześniej odwiedzone strony.
Najpierw używam interfejsu Cache Storage API, aby uzyskać listę wszystkich wcześniej zapisanych w pamięci podręcznej żądań do interfejsu API, i przekształcam ją w listę adresów URL.
Pamiętasz te specjalne atrybuty danych, o których mówiłem, z których każdy zawiera adres URL żądania do interfejsu API potrzebnego do wyświetlenia pytania? Mogę porównać dane z różnych kanałów te atrybuty danych z listą adresów URL w pamięci podręcznej i utworzyć tablicę wszystkich linków do pytań, które nie pasują.
Gdy przeglądarka przechodzi w tryb offline, przeglądam listę linków, które nie są zapisane w pamięci podręcznej, i przyciemniam te, które nie będą działać. Pamiętaj, że to tylko wizualna wskazówka dla użytkownika, czego może się spodziewać na tych stronach. Nie wyłączam linków ani nie uniemożliwiam użytkownikowi 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);
Typowe problemy
Przedstawiłem już moje podejście do tworzenia wielostronicowej aplikacji PWA. Przy opracowywaniu własnego podejścia musisz wziąć pod uwagę wiele czynników i możesz dokonać innych wyborów niż ja. Ta elastyczność jest jedną z największych zalet tworzenia aplikacji na potrzeby internetu.
Podczas podejmowania decyzji dotyczących architektury możesz napotkać kilka typowych pułapek. Chcę Ci pomóc ich uniknąć.
Nie zapisuj w pamięci podręcznej pełnego kodu HTML
Nie zalecamy przechowywania w pamięci podręcznej całych dokumentów HTML. Po pierwsze, to marnowanie miejsca. Jeśli aplikacja internetowa używa tej samej podstawowej struktury HTML na każdej stronie, będziesz przechowywać kopie tych samych znaczników.
Co ważniejsze, jeśli wprowadzisz zmianę w udostępnionej strukturze HTML witryny, każda z tych wcześniej zapisanych w pamięci podręcznej stron będzie nadal miała stary układ. Wyobraź sobie frustrację powracającego użytkownika, który widzi mieszankę starych i nowych stron.
Rozbieżność między serwerem a skryptem service worker
Inną pułapką, której należy unikać, jest brak synchronizacji serwera i skryptu service worker. Moje podejście polegało na użyciu izomorficznego JavaScriptu, dzięki czemu ten sam kod był uruchamiany w obu miejscach. W zależności od istniejącej architektury serwera nie zawsze jest to możliwe.
Niezależnie od decyzji dotyczących architektury należy mieć strategię uruchamiania równoważnego kodu routingu i tworzenia szablonów na serwerze i w skrypcie service worker.
Najgorsze scenariusze
Niespójny układ lub projekt
Co się stanie, jeśli zignorujesz te pułapki? Możliwe są różne błędy, ale najgorszy scenariusz jest taki, że powracający użytkownik odwiedza stronę z pamięci podręcznej z bardzo nieaktualnym układem – na przykład z nieaktualnym tekstem nagłówka lub z nazwami klas CSS, które nie są już prawidłowe.
Najgorszy scenariusz: uszkodzone kierowanie
Użytkownik może też natrafić na adres URL, który jest obsługiwany przez serwer, ale nie przez skrypt service worker. Witryna pełna nieużywanych układów i ślepych zaułków nie jest niezawodną aplikacją PWA.
Wskazówki zapewniające sukces
Nie musisz jednak robić tego samodzielnie. Te wskazówki pomogą Ci uniknąć tych pułapek:
Używanie bibliotek szablonów i routingu, które mają implementacje w wielu językach
Spróbuj użyć bibliotek szablonów i routingu, które mają implementacje JavaScriptu. Wiem, że nie każdy deweloper może sobie pozwolić na luksus migracji z obecnego serwera WWW i języka szablonów.
Jednak wiele popularnych platform do tworzenia szablonów i routingu ma implementacje w kilku językach. Jeśli znajdziesz taki, który działa zarówno z JavaScriptem, jak i językiem Twojego obecnego serwera, będziesz o krok bliżej do synchronizacji skryptu service worker z serwerem.
Preferuj szablony sekwencyjne, a nie zagnieżdżone
Następnie zalecam użycie serii szablonów sekwencyjnych, które można przesyłać strumieniowo jeden po drugim. Nie ma problemu, jeśli dalsze części strony używają bardziej skomplikowanej logiki szablonów, o ile możesz jak najszybciej przesłać początkową część kodu HTML.
Przechowywanie w pamięci podręcznej treści statycznych i zawartości dynamicznej w skrypcie service worker
Aby uzyskać najlepszą wydajność, należy wstępnie zapisać w pamięci podręcznej wszystkie najważniejsze zasoby statyczne witryny. Należy też skonfigurować logikę buforowania w czasie działania, aby obsługiwać zawartość dynamiczną, np. żądania do interfejsu API. Dzięki Workbox możesz korzystać z dobrze przetestowanych, gotowych do wykorzystania w środowisku produkcyjnym strategii, zamiast wdrażać wszystko od podstaw.
Blokuj w sieci tylko wtedy, gdy jest to bezwzględnie konieczne
W związku z tym należy blokować sieć tylko wtedy, gdy nie można przesłać odpowiedzi z pamięci podręcznej. Natychmiastowe wyświetlenie odpowiedzi interfejsu API z pamięci podręcznej często zapewnia lepsze wrażenia użytkownika niż oczekiwanie na nowe dane.