Od responsywnej witryny do PWA w jeden dzień

Podejrzewam, że sporo osób słyszało już o PWA (Progressive Web Apps), ale odświeżmy wiedzę na szybko – w największym skrócie chodzi o utworzenie aplikacji z witryny internetowej. Jeśli twoja aplikacja jest zgodna z PWA, to osoby używające tej witryny powinny być w stanie dodać skrót (ikonkę) do ekranu głównego (lub do pulpitu). Taka apka powinna również w pewnym zakresie działać bez sieci (offline), co może być osiągnięte za pomocą Service Workera. Pamiętaj, że dostosowując witrynę do zasad PWA udoskonalasz doświadczenia wszystkich osób! Nie tylko osób korzystających z urządzeń mobilnych.

W tym artykule omówię podstawy przekształcania istniejącej witryny na PWA. Z jednej strony będę mówił o podstawach, ale przy odrobinie szczęścia na podstawie poniższych informacji możesz w ciągu 1-2 dni zrobić w pełni funkcjonalne PWA. Możesz natrafić pod drodze na parę pułapek, ale spróbuję pokazać tutaj sztuczki, które pozwolą łatwiej nawigować między przeszkodami.

Ten artykuł to pierwsza część z mojej serii o PWA. Część druga dostępna po angielsku: PWA and HTTP Caching.

Czy można już używać PWA?

Zatrzymajmy się na chwilę i zastanówmy się czy to jest coś czego można i warto używać. Innymi słowy, czy jest warte twojego czasu. Moja odpowiedź brzmi: tak. Ale nie chciałbym być gołosłownym, więc podrzucę trochę odnośników, dla ciebie i być może dla twoich zwierzchniczek lub zwierzchników :-).

Google (Chrome/Android)

Google od samego początku silnie wspiera PWA. Nie jest to zbyt zaskakujące, bo to w zasadzie oni go wymyślili... z pomocą Jake'a Archibalda. Jeśli nie kojarzysz go, to dodam, że jest on osobą, która parę lat temu latała po konferencjach i pokrzykiwała na Application Cache (Application Cache to poprzednik PWA). Myślę, że w dużej mierze dzięki jego krytyce (i jego przejściu do Google) mamy teraz lepsze rozwiązanie do tworzenia aplikacji mobilnych i nie tylko.

Dodawanie skrótu do witryny zgodnej z PWA zostało zaimplementowane już w Chrome 42 a ulepszone w Chrome 57 (Paul Kinlan. „The New and Improved Add to Home screen”. Developers Google. 2017). Pierwsze wydanie Chrome for Android pojawiło się już w Android 4.0 (Mat Smith. „Google Chrome Beta arrives on Android (video)”. Engadget. 2012). Co prawda nie znalazłem dokładnej listy zgodności wersji Chrome z wersjami Android, ale udało mi się zainstalować i przetestować Chrome 64 na Android 4.1.2 (Xperia S).

Czyli Android 4.1+ powinien wystarczyć do pełnej obsługi PWA.

Microsoft (Edge/Windows 10)

Microsoft wydaje się być zdeterminowany by co najmniej dorównać Google we wspieraniu PWA. Stworzyli PWA Builder, który jest narzędziem wspomagającym tworzenie PWA. Trwają również prace nad automatycznym wyszukiwaniem PWA (za pomocą botów) i tworzenie z nich aplikacji, które mają być dostępne w Windows Store. Takie aplikacje powinny się zacząć pojawiać jeszcze w kwietniu 2018 (Paul Thurrott. „Microsoft’s Bold Plan to Bring PWAs to Windows 10”. Thurrott. 2017).

Apple (Safari/iOS)

A jak wygląda sprawa z niesfornym chłopcem Internetu? Tu może zaskoczę niektórych, ale Apple jednak zdecydował się zaimplementować Service Workera i powinien on być dostępny w niedawnej aktualizacji (Prathik S Shetty. „Progressive web apps (PWAs) are coming to a Safari near you”. Medium. 2018). Od iOS 11.3 zarówno Web App Manifest, jak i Service Worker, powinny być dostępne. Są pewne różnice w zachowaniu Service Worker Cache w wersji Apple (głównie kwestia tego, że cache może być wyczyszczony dla rzadziej używanych aplikacji), ale zasadniczo PWA działa (Maximiliano Firtman. „PWAs are coming to iOS 11.3: Cupertino, we have a problem”. Medium. 2018).

Firefox i inni

Nie wspominam o przeglądarce Mozilla Firefox powyżej, bo chociaż jest to znacząca przeglądarka, to Mozilla nie ma obecnie liczącego się systemu operacyjnego. Warto jednak wiedzieć, że kluczowe standardy są wspierane w Firefoksie, a nawet dodawanie skrótów do PWA powinno działać w Firefoksie na Android.

Więcej o kompatybilności przeglądarek można znaleźć m.in. na MDN: kompatybilność dla Service Worker API oraz kompatybilność dla Web App Manifest.

Co jest naprawdę wymagane w ramach PWA

Podstawowe listy zgodności można znaleźć na przykład na stronach Google (co jest wymagane do zainstalowania apki) oraz Microsoftu (Minimalne wymagania dla PWA).

Nie będę tutaj wchodził w techniczne szczegóły zgodności. Nie jest to niezbędne. Wystarczające są następujące elementy:

  1. HTTPS. Tego niestety nie da się ominąć. Service Worker nie będzie działał bez szyfrowania. Nawet przy testowaniu. Prawda jest jednak taka, że prawdopodobnie i tak potrzebujesz https (wygląda na to, że w najbliższym czasie szyfrowanie może być wymagane dla formularzy logowania). Jeśli nie masz certyfikatu HTTPS, to poczytaj o Let's Encrypt (a konkretniej Certbocie). Cerbot jest darmowy i dosyć prosty, ale w praktyce wymagany jest pełny dostęp administracyjny do serwera oraz rejestracji domeny internetowej dla serwera. Do testów można spokojnie używać tzw. samo-podpisanego certyfikatu i podstawowej konfiguracji HTTPS.
  2. Web App Manifest. Ten manifest to plik z podstawową definicją aplikacji (nazwa, ikona itp). Żeby wygenerować podstawowy manifest można użyć narzędzia PWA Builder. Powinien być w stanie wykryć podstawowe rzeczy (także ikony) oraz poda wskazówki czego jeszcze brakuje.
  3. Responsywność/Cross-Browser. To w zasadzie nie jest bezpośrednio wymagane przez PWA, ale pamiętaj, że PWA nie jest magicznym opakowaniem izolującym od przeglądarki. Jeśli chcesz, żeby wiele osób korzystało z twojej witryny, to musisz zapewnić zgodność z mobilnym Chrome, Edge, Firefox i Safari. Pamiętaj, że większość ruchu w Internecie pochodzi obecnie z urządzeń mobilnych, a zróżnicowanie rozdzielczości ekranów jest bardzo duże. Postaraj się, żeby witryna działała tam gdzie to możliwe.
  4. Service Worker/Offline/Cache. Żeby aplikacja działała offline potrzebujesz Service Workera. Nie musi on być od razu idealny. Wystarczy, że na pierwszej stronie pojawi się jakaś treść. Po porządnym zrobieniu cachowania za jednym zamachem udoskonalisz doświadczenia osób korzystających z aplikacji jak i odciążysz swój serwer. Wszyscy wygrywają.

Dziwności Web App Manifest

Manifest jest prostym plikiem, ale jest tu parę kruczków. Polecam używanie Lighthouse do sprawdzania zgodności ze standardem. Raport z testów za pewne urazi twoje uczucia ;-), ale poprowadzi w dobrym kierunku. Pamiętaj, że nie musisz zdobyć 100% w każdym z testów. Przeczytaj jednak wskazówki uważnie i postaraj się je zastosować.

Jednym z kruczków jest to, że co prawda można użyć SVG jako ikonę, ale Lighthouse wskaże, że w manifeście brakuje wymaganych rozmiarów ikon.

Przykładowo poniższy fragment jest zgodny ze standardem manifestu W3C:

{
  "icons": [
    {
      "src": "/icon/favicon.ico",
      "sizes": "16x16 32x32"
    },
    {
      "src": "/icon/appicon.svg",
      "sizes": "33x33"
    }
  ]
}

Ale powyższe może nie działać poprawnie. W praktyce powinno się podać listę typowych wymiarów ikon, dla których użyte ma być SVG. Czyli coś w tym stylu:

{
  "icons": [
    {
      "src": "/icon/favicon.ico",
      "sizes": "16x16 32x32"
    },
    {
      "src": "/icon/appicon.svg",
      "sizes": "48x48 72x72 96x96 114x114 128x128 144x144 192x192 256x256 512x512"
    }
  ]
}

Moim zdaniem to jest błąd, który powinien zostać poprawiony... Ale wypisanie listy wymiarów nie zaszkodzi i nie zajmie dużo czasu.

W razie gdybyś się zagubił(-a) w tym co może być w danym polu i co ono oznacza, to polecam dokumentację Web App Manifest na witrynie MDN. Opisują tam dosyć szczegółowo poszczególne pola manifestu.

Podstawowy Service Worker

Co to jest Service Worker

Service Worker to w zasadzie zestaw API (zobacz więcej na MDN). Na początek wystarczy wiedzieć, że większość dotyczy cachowania i przejmowania żądań do serwera. Tak, worker może przechwycić żądanie i odpowiedzieć prawie dowolną treścią. To właśnie dzięki temu twoja aplikacja może działać również bez użycia sieci.

Warto dodać w tym miejscu, że Service Worker Cache zastępuje wcześniejszy Application Cache. Znajomość Application Cache nie jest niezbędna do budowy PWA, ale jeśli poznałeś(-aś) wcześniej Application Cache Manifest, to Service Worker na pierwszy rzut oka może się wydawać zdecydowaną przesadą. Tak przynajmniej było w moim wypadku. Moje pierwsze wrażenia były takie, że chociaż Service Worker rozwiązuje niektóre problemy, to jest zbyt skomplikowany w użyciu... Okazało się, że nie miałem racji. Istnieją narzędzia i proste skrypty, które znacząco upraszczają sprawę i pozwalają na szybki start.

Jak rozpocząć

To może zależeć od tego z czym obecnie pracujesz. Jeśli używasz jakiegoś dużego frameworka JS (takiego jak Angular 2+), to za pewne znajdziesz narzędzie dostosowane pod dany framework lub nawet jakąś prostą opcję, która umożliwia utworzenie workera. Możesz również użyć bardziej uniwersalnego narzędzia do generowania Service Worker jakim jest sw-precache. Powinno się udać dosyć prosto użyć sw-precache, jeśli do budowania aplikacji używasz gulp lub webpack.

Chciałbym jednak wyraźnie zaznaczyć, że Service Worker nie jest zależny od konkretnego frameworka. Można go spokojnie utworzyć ręcznie i będzie działał całkiem nieźle. Jako startera można użyć bazowego przykładu Service Workera od Google. Na początek wymagane jest jedynie drobne dostosowanie skryptu.

Używanie przykładowego kodu Service Workera od Google

Bazowy przykład Service Workera od Google jest w pełni sprawny skryptem, który będzie się nadawał dla prawie dowolnej aplikacji, a przynajmniej do takich, które mają połączone skrypty i style. Większość aplikacji i tak łączy swoje skrypty w jeden lub parę plików, więc w większości wypadków powinno być dobrze.

  1. Skopiuj kod z sekcji „Service Worker's JavaScript” i zapisz go do pliku service-worker.js.
  2. Wrzuć ten plik do głównego folderu ze swoją aplikacją.
  3. W tablicy PRECACHE_URLS wpisz listę skryptów i stylów, które są niezbędne do załadowania pierwszej strony aplikacji.
  4. Zarejestruj swój skrypt workera za pomocą poniższego kodu (możesz go dokleić w dowolnym, istniejącym skrypcie):
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
}

I już. Twoja aplikacja powinna teraz działać w wersji offline. Dobrze jest poświęcić trochę czasu na testy i dalsze dostosowanie PRECACHE_URLS wedle potrzeb. Możesz na przykład chcieć załadować dodatkowe elementy, które nie pojawiają się od razu, ale mogą być potrzebne po zmianach użytkownika. Na przykład możesz dodać do listy CSS z alternatywnych skórek.

Zasięg działania Service Workera

Ważne! Service Worker działa na wszystkie podfoldery. Jeśli kod statystyczny jest w innym folderze niż API, to prawdopodobnie wszystko będzie działać jak trzeba. Jeśli jednak aplikacja jest w głównym folderze, a API w podrzędnym, to możesz mieć problem.

API w osobnym folderze

Załóżmy taką sytuację (sytuacja najprostsza):

https://example.com/my-static-app/ -- tu jest cały kod statycznej aplikacji (JS, CSS, HTML)
https://example.com/my-api/  -- tu jest API

Jeśli w powyższym przykładzie nie chcesz cachować żądań do API, to w tym wypadku umieścisz service-worker.js w folderze my-static-app. I na początek raczej nie polecałbym cachowania API.

API w podfolderze

Teraz nieco bardziej skomplikowany przypadek:

https://example.com/ -- tu jest kod statycznej aplikacji (JS, CSS, HTML)
https://example.com/my-api/  -- tu jest API

W tym wypadku service-worker.js musi być umieszczony w głównym folderze. To oznacza, że folder my-api będzie cachowany. To z kolei oznacza, że np. pierwsze żądanie do /my-api/user/login zostanie wysłane do serwera, ale następne zostanie już zwrócone z cache. Ale bez paniki. Dodanie wyjątku jest proste. Wystarczy wcześniej wyjść z obsługi zdarzenia fetch. Przy okazji można dodać pomijanie cachowania żądań typu POST itp (Chrome i tak jeszcze nie umie ich cachować, a zazwyczaj i tak chcemy cachować tylko żądania GET).

self.addEventListener('fetch', event => {
    // skip non-GET requests
    if (event.request.method !== 'GET') {
        return;
    }
    // local request
    if (event.request.url.startsWith(self.location.origin + '/my-api/')) {
        const localUrl = event.request.url.replace(self.location.origin, '');
        if (localUrl.startsWith('/my-api/')) {
            return;
        }
    }
    // ...
    // ...
});

Teoretycznie możesz chcieć cachować niektóre żądania do API (np. jeśli z API pobierasz tłumaczenia). Lepiej jest jednak na początek cachować raczej mniej, niż więcej.

Podstawy Service Worker Cache

Pre-cache kontra runtime cache

Zwróć uwagę, że w opisanym powyżej przykładzie workera znajdują się dwa odrębne rodzaje cache. Kluczem pierwszego jest precache-v1, a drugiego runtime. To jest w zasadzie głównie kwestia przyjętej konwencji, ale jednak oba rodzaje cache są napełniane w inny sposób w tym skrypcie.

  • Pre-cache jest napełniany przy starcie aplikacji (a dokładniej po rejestracji workera). Obsługiwane jest to w ramach zdarzenia install. Tutaj worker może pobrać kluczowe elementy aplikacji i udostępnić je gdy urządzenie jest poza zasięgiem sieci.
  • Runtime cache jest napełniany w locie, po zakończeniu wszelkich żądań do serwera. Obsługiwane jest to w ramach zdarzenia fetch.

Aktualizacja PWA

Jeśli pozostawisz service worker sam sobie, to już nigdy nie zaktualizujesz aplikacji swoim użytkownikom. Wszystkie pliki, które znalazły się w cache będą z niego serwowane po wieczność. Żądania nowych wersji nie trafią nigdy do serwera. Zwracam uwagę, że zmiana precache-v1 na precache-v2 może również nie być wystarczająca. Pierwszym problemem może być to, że użytkownicy nie dostaną nowego kodu workera (to zwykle jest problem ustawień cachowania po stronie serwera). Drugim problemem jest to, że przeglądarki i tak mogą używać standardowego cache po przepuszczeniu żądania przez Service Worker.

Co zrobić, żeby unikać problemów z ładowaniem nowych elementów z serwera?

  1. Jedna ze strategi to dodawanie wersji do URL. Czasem nazywane jest to metodą cache-busting. W najprostszej wersji robi się to przez dodanie wersji w postaci parametru w query-string np. ?v=1. Czyli przy ładowaniu stylów można użyć np.: <link rel="stylesheet" href="styles.css?v=1">. Wersją może być numer rewizji z SVN, znacznik czasowy z momentu budowania itp. Zależy to głównie od tego jak budujesz aplikację.
  2. Unikanie używania nagłówków do HTTP Cache. Zmiana procesu budowania aplikacji żeby dodać wszędzie wersję może być skomplikowana. Czasami może być łatwiej unikać używania standardowego cache przeglądarki przez zmianę nagłówków związanych z HTTP Cache.

Testowanie aktualizacji

Może się zdarzyć, że wpadniesz na jeden z problematycznych przypadków. Zanim jednak zagłębimy się w przypadki szczególne, warto sprawdzić czy podstawowa wersja aktualizacji po prostu działa. To jest procedura testowania aktualizacji, której osobiście używam:

  • Wyczyść całą pamięć podręczną witryny (może być niezbędne zwłaszcza po zmianach konfiguracji serwera lub workera).
    • Wciśnij CTRL+SHIFT+DELETE i wybierz tylko opcję czyszczenia pamięci podręcznej.
    • Wyrejestruj/wyczyść workera.
      • Chrome: DevTools -> Application -> Clear storage.
      • Firefox: about:debugging#workers -> unregister (wyrejestruj).
    • Ponowne czyszczenie z CTRL+SHIFT+DELETE (dla pewności).
  • Odśwież/załaduj stronę.
  • Zmień some-file.js dodając jakiś unikatowy komunikat np. alert("Update v2").
  • Upewnij się, że plik app.js został zbudowany (zakładam, że łączysz skrypty w app.js).
  • Odśwież stronę. Czy nowy alert jest widoczny? (nie powinien)
  • Zmień wersję w PRECACHE.
  • Odśwież stronę. Czy nowy alert jest widoczny? (powinien, ale w FF może się nie pojawić)
  • Spróbuj zrestartować przeglądarkę. Czy nowy alert jest widoczny? (powinien się pojawić niezależnie od przeglądarki)

Pamiętaj, że takie testy musisz wykonać również na serwerze produkcyjnym (docelowym). Ale jeśli już to zadziała, to wszystko powinno być OK.

Zwróć uwagę, że powyżej są przy okazji opisane kroki według których będzie działać aktualizacja. Rzecz jasna wykonanie pierwszego kroku nie powinno być konieczne dla przeciętnego użytkownika(-czki). Czyli w najgorszym razie restart przeglądarki powinien pozwolić załadować nową wersję aplikacji. Myślę, że to jest dopuszczalne ograniczenie.

Na ten moment nie udało mi się niestety sprawdzić działania w Safari, ale spróbuję zrobić to później i zaktualizować informacje. Jeśli uda ci się to sprawdzić wcześniej, to poproszę o informację.

Jeśli natomiast powyższa procedura nie działa w twoim wypadku, to zapraszam ponownie później :-). W następnym artykule chciałbym opisać nieco więcej o aktualizacjach PWA i zdecydowanie więcej o cachowaniu.