Opublikowano 4 min. czytania

PWA (Progressive Web App) jest aplikacją webową, która w założeniu ma działać w sposób zbliżony do natywnej aplikacji instalowanej w systemie. Z tego artykułu dowiesz się jak w 15 minut sprawić, aby Twoja aplikacja spełniała wszystkie minimalne wymogi PWA i dało się ją zainstalować na urządzeniu.

HTTPS

Pierwszym wymogiem jest serwowanie Twojej strony przez HTTPS. Jest to konieczne do zarejestrowania service worker'a. Najpopularniejszą, bezpłatną metodą jest podpięcie certyfikatu SSL od Let's Encrypt.

Manifest

To plik JSON, który determinuje zachowanie aplikacji po jej instalacji. Powinien zawierać przede wszystkim nazwę aplikacji, ścieżki do ikon i URL startowy. Kompletna specyfikacja tego pliku jest dostępna na stronie W3C.

Przykładowy manifest.json dla aplikacji PWA może wyglądać następująco:

{
    "name": "Smakowity.pl",
    "short_name": "Smakowity.pl",
    "description": "Przepisy i artykuły kulinarne.",
    "icons": [{
            "src": "/android-chrome-192x192.png",
            "type": "image/png",
            "sizes": "192x192"
        },
        {
            "src": "/android-chrome-512x512.png",
            "type": "image/png",
            "sizes": "512x512"
        }
    ],
    "start_url": "/?source=pwa",
    "display": "standalone",
    "scope": "/",
    "background_color": "#FFF",
    "theme_color": "#FFF"
}

Tak przygotowany manifest należy podlinkować w sekcji <head>:

<link rel="manifest" href="/manifest.json">

Service Worker

W rzeczywistości jest bardzo ważnym plikiem JavaScript, który odpowiada za przetwarzanie żądań HTTPS i zwracanie odpowiedzi. Jego główną ideą jest zapewnienie aplikacji możliwości działania offline. Przechodzą przez niego wszystkie żądania wywoływane przez aplikację, w tym także te do innych domen!

Tworzymy plik service-worker.js w głównym, publicznym katalogu aplikacji i zdefiniuj na jego początku nazwę cache'u i URL-e do zawartości, która powinna być dostępna offline:

const CACHE_NAME = 'smakowity-v1';
const URLS_TO_CACHE = [
    '/offline.html',
    '/images/logo.png'
]

Następnie definiujemy trzy zdarzenia (eventy) w ramach których service worker wykonuje swoje działania: install, activate oraz fetch.

Zdarzenie "install"

Jest wywoływane po zarejestrowaniu service worker'a. W tym miejscu zazwyczaj dodaje się zasoby do cache'u.

self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open(CACHE_NAME)
        .then(function(cache) {
            return cache.addAll(URLS_TO_CACHE);
        })
    );
});

Zdarzenie "activate"

Jest wywoływane po instalacji service worker'a. Najczęściej w tym miejscu czyści się cache ze starych danych używanych w poprzedniej wersji skryptu.

self.addEventListener('activate', (e) => {
    e.waitUntil(
        // Pobieranie wszystkich dostępnych cache'y
        caches.keys().then((keyList) => {
            return Promise.all(keyList.map((key) => {
                // Usunięcie starych cache'y
                if (CACHE_NAME.indexOf(key) === -1) {
                    return caches.delete(key);
                }
            }));
        })
    );
});

Zdarzenie "fetch"

Jest wywoływane podczas żądania pobrania jakiegokolwiek zasobu, zarówno w trybie online, jak i offline.

self.addEventListener('fetch', event => {
    // Obsługujemy tylko żądania w ramach naszej domeny, a resztę przepuszczamy
    if (event.request.url.startsWith(self.location.origin)) {
        event.respondWith(
            caches.match(event.request).then(cache => {
                // Jeśli żądany zasób jest w cache'u - zwracamy go
                if (cache) {
                    return cache;
                }

                return caches.open(CACHE_NAME).then(cache => {
                    // W trybie offline zwracamy zawartość strony offline.html
                    if ( ! navigator.onLine) {
                        return caches.match('/offline.html');
                    }

                    return fetch(event.request).then(response => {
                        return response;
                    }).catch(error => {
                        console.warn(error);
                    })
                })
            })
        );
    }
});

W powyższym przykładzie, w trybie offline, zwracana jest zawartość strony /offline.html, na której może znajdować się informacja dla użytkownika o konieczności przejścia w tryb online, aby móc dalej korzystać z aplikacji. Dzięki temu spełniamy niezbędny wymóg, jakim jest zwracanie statusu 200 przez każdy URL aplikacji w trybie offline. Należy przy tym pamiętać, że wszystkie elementy szablonu tej strony (pliki CSS, JS, grafiki itd.) również powinny znaleźć się w cache'u (czyli muszą być zdefiniowane w stałej URLS_TO_CACHE).

Jest to oczywiście namiastka obsługi trybu offline. W założeniu aplikacja PWA powinna w tym trybie być funkcjonalna w jak największej części, czyli np. wyświetlać cały interfejs i synchronizować dane po przejściu w tryb online.

Rejestracja service worker'a

Do głównego kodu JS strony należy dodać:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker
        .register('/service-worker.js')
        .then((registration) => {
            console.log('Service Worker has been registered with scope: ' + registration.scope);
        })
        .catch((error) => {
            console.error('Service Worker registration failed: ' + error);
        });
}

Responsywność

Strona musi być responsywna i dostosowana do urządzeń mobilnych. Kompletne minimum jakie należy tutaj spełnić to obecność tagu <meta name="viewport"> z width lub initial-scale i dostosowanie zawartości do szerokości viewport'u.

Audyt w Lighthouse

Poprawność wdrożenia PWA można sprawdzić za pomocą narzędzia Lighthouse, który wskaże co ewentualnie jeszcze powinno zostać poprawione.

Google przygotowało także checklistę dla PWA, którą warto przejrzeć w kontekście rozbudowy PWA o nieobowiązkowe funkcje.