Jak wykorzystać tryb offline do zwiększenia możliwości twojej aplikacji

Jak wykorzystać tryb offline do zwiększenia możliwości twojej aplikacji

Niedawno pisaliśmy na naszym blogu o PWA, czyli nowej fali aplikacji webowych które sposobem działania przypominają to, co znamy z natywnych aplikacji mobilnych. Jednym z najczęściej przewijających się punktów dotyczących PWA jest wsparcie dla trybu offline i odporność takiej aplikacji na wahania sieci. Brzmi nieźle, ale jakie korzyści w praktyce możemy zaoferować naszym użytkownikom dodając do naszej aplikacji możliwość korzystania z niej bez połączenia z Internetem?

Pacjentem którym dzisiaj będziemy się zajmowali będzie prosty klon Twittera – aplikacja społecznościowa służąca do wysyłania w świat krótkich wiadomości tekstowych. Prezentuje się ona następująco:

Niestety, w momencie kiedy użytkownik nie ma dostępu do internetu, jedyne co zobaczy po wejściu na stronę tej aplikacji to komunikat podobnej treści:

Czy nie byłoby dobrze, żeby w momencie braku połączenia z internetem użytkownik nie musiał rezygnować z naszej strony? Korzyści odniosłyby dwie strony – użytkownik nie musiałby tracić wiadomości którą chciał koniecznie w tym momencie opublikować, a my… nie stracilibyśmy użytkownika.

W dzisiejszym poście zobaczymy, jak uniknąć tego typu sytuacji i umożliwić użytkownikowi wysyłanie postów nawet wtedy, kiedy pracuje w trybie offline.

Tworzymy fundamenty

Zacznijmy od stworzenia fundamentów pod naszą aplikację. Najpierw struktura katalogów, która prezentuje się następująco:

Poza node_modules w którym znajdują się zależności instalowane poprzez npm’a znajdziemy tutaj trzy główne katalogi w których umieścimy kod naszej aplikacji.

  • public – część kliencka, zawierająca skrypty, style i obrazki
  • server – część serwerowa, w której zawierać się będzie procesowanie wiadomości i serwowanie strony głównej
  • views – widoki aplikacji

Głównym plikiem który chcemy uruchamiać w momencie startu prac nad aplikacją jest server.js – jest to entry point, w którym zdefiniowana jest prosta konfiguracja naszego serwera, oraz lokalizacja widoków i zasobów statycznych. Rozszerzenie pliku, czyli js, sugeruje, że nie będziemy tutaj używać ASP.NET’a, Railsów ani Pythona, a skupimy się na technologiach związanych z JavaScriptem – w naszym przypadku będą to Node.js oraz Express

Nasz entry point wygląda w ten sposób:

Konfiguracja aplikacji w Expressie jest stosunkowo prosta do zrozumienia – nasz serwer będzie nasłuchiwał na porcie 3000, używał widoków znajdujących się w folderze views, silnikiem renderującym nasze widoki będzie puga pliki statyczne będą hostowane z serweru public. Wchodząc na adres localhost:3000 (a właściwie wykonując zapytanie GET pod takim adresem) w odpowiedzi otrzymamy wyrenderowany widok o nazwie index.

Nasz pierwszy widok (views/index.pug) wygląda w ten sposób:

Fanów czystego HTML’a może to nieco szokować, jednak zapewniam, że po kilku dniach spędzonych z taką składnią ochota na tagi w ostrych nawiasach mija bezpowrotnie.

Co zawiera nasz widok? Jest to prosta struktura strony z jednym plikiem css dołączanym w sekcji head, oraz formularzem który będzie nam służył do wysyłania wiadomości. Sam plik styles.css (public/styles/styles.css) wygląda tak:

Do ustawienia prawidłowej formy całej strony używam flexboxa – poza tym nie znajdziemy tutaj niczego szczególnie skomplikowanego.

Całość spina plik package.json, w którym zadeklarowaliśmy następujące zależności:

Express to nasz framework wykorzystywany po stronie serwerowej, nodemon to narzędzie które dzięki śledzeniu zmian w plikach zapewni nam zawsze aktualną wersję naszej aplikacji, a pug to silnik widoków o którym pisałem już wcześniej.

Aplikację uruchamiamy komendą:

W tym momencie naszym oczom powinien się ukazać widok formularza który wygląda tak jak na pierwszym dołączonym do tego posta obrazku.

Wysyłanie postów na serwer

Próbując wysłać wiadomość można zauważyć, że poza odświeżeniem strony w naszej aplikacji nie dzieje się nic szczególnego. Zgadza się – nie obsłużyliśmy przecież w żaden sposób wysyłania danych z naszego formularza. W jaki sposób możemy to zrobić?

Zacznijmy od części klienckiej. Na końcu naszego głównego widoku, czyli index.pug dodajmy linijkę dzięki której będziemy dołączać do niego tzw. partial view ze skryptami:

W folderze views utwórzmy teraz folder partials, a w nim plik _scripts.pug, który na razie wygląda w sposób następujący:

Dołączamy tutaj nasz pierwszy skrypt JS’a. Sam plik znajduje się w folderze public/js/app.js. Funkcjonalność którą zaraz zrealizujemy polega na przechwyceniu zdarzenia związanego z wysłaniem formularza, wysłaniem requestu do naszego serwera z treścią wiadomości gdzie zapiszemy posta do bazy, a następnie wyczyścimy po stronie klienta pole tekstowe i odświeżymy listę postów.

Na początku deklarujemy trzy zmienne odwołujące się do elementów DOM, takich jak nasz formularz, pole tekstowe oraz główny tag (root) w którym znajdzie się lista postów.

Do formularza dodajemy nowy event listener który będzie wywoływany w momencie wystąpienia eventu submit – czyli wysłania formularza. Najpierw poprzez preventDefault blokujemy domyślne wysyłanie formularza, następnie pobieramy z inputu wartość wpisanej przez użytkownika wiadomości i wykonujemy metodę add nieistniejącego jeszcze PostService’u. Metoda add zwróci nam promise, czyli rezultat wywołania pewnej asynchronicznej operacji (dodanie posta do bazy), a sam promise da nam możliwość zarejestrowania kolejnej funkcji gdzie wyczyścimy tekst wpisany przez użytkownika.

PostService którego aktualnie nam brakuje możemy utworzyć w nowym pliku – obok app.js utwórzmy PostService.js i dodajmy tam klasę z naszą metodą add:

Używając Fetch API buduję odpowiednie zapytanie ustawiając najpierw typ danych jakie wysyłam (JSON), następnie przygotowuję posta w odpowiednim formacie i wysyłam go pod adres /posts. Jeśli odpowiedź z serwera będzie pozytywna, to post którego dodałem zostaje najpierw poprawnie sformatowany, a następnie dodany na początek listy postów funkcją unshift (aby zachować odpowiednią kolejność postów).

Teraz musimy jeszcze dołączyć nasz PostService.js do strony w pliku _scripts.pug, a następnie możemy odświeżyć stronę i sprawdzić jak całość działa w praktyce. Wpisujemy więc dowolną wiadomość w pole tekstowe, klikamy przycisk i… nic się nie dzieje. Sprawdźmy więc co stało się z naszym zapytaniem. Pomogą nam w tym DevToolsy – w Chrome będzie to zakładka Network:

Widzimy, że request został wysłany poprawnie, jednak serwer zwrócił nam odpowiedź 404 – zasób do którego się odwołujemy nie istnieje pod adresem /posts. To oczywiście prawda, ponieważ wciąż brakuje nam obsługi takiej funkcjonalności po stronie serwera.

Zapisywanie i odczytywanie postów z serwera

Status 404 oznacza tyle, że w routerze Expressa, czyli mechanizmie obsługującym przekierowywanie zapytań w konkretne miejsca naszej aplikacji nie zarejestrowaliśmy takiego route’a jak “/posts”. Aby to zrobić, wróćmy do naszego pliku server.js i zaktualizujmy go.

Potrzebujemy dwóch rzeczy – po pierwsze, musimy nauczyć naszą aplikację odpowiedniego parsowania zapytań z danymi w formacie JSON – służy do tego middleware body-parser (do zainstalowania przez npm’a). Po drugie, musimy zaktualizować router naszej aplikacji:

Pod ścieżką “/posts” zarejestrujemy nowy middleware w naszej Expressowej aplikacji, którym będzie router obsługujący zapytania związane z zapisywaniem i odczytywaniem listy postów.

Utwórzmy teraz nowy plik w folderze server – posts.js i zaktualizujmy jego zawartość:

Rejestrujemy tutaj dwa podstawowe typy zapytań do naszego serwera – zapytania GET pod adresem “/” mają zwracać listę wszystkich zapisanych postów jako JSON, natomiast zapytanie POST pod tą samą ścieżką umożliwi nam zapisanie nowego posta do bazy (w naszym przypadku będzie to kolekcja postów trzymana w pamięci serwera). Warto zauważyć, że ścieżki jakich tutaj używamy są relatywne do tego, pod jakim adresem rejestrujemy nasz middleware – nie używamy więc pełnej ścieżki “/posts”, tylko “/”.

Korzystamy w tym miejscu z nowej biblioteki – dateformat – więc należy zainstalować ją poleceniem npm install.

Jeśli chodzi o nasze źródło danych, to zdefiniujemy je w pliku server/cache.js:

Zobaczmy, jak po dodaniu tych kilku elementów wygląda wysyłanie wiadomości na serwer:

Zapytania POST działają – otrzymujemy odpowiedź ze statusem 200, a w zakładce Preview widzimy nasz post sformatowany w odpowiedni sposób. Możemy dodatkowo podglądnąć wszystkie wiadomości zapisane na serwerze wykonując zapytanie GET pod adresem localhost:3000/posts:

Wygląda to naprawdę dobrze! Zadbajmy teraz o to, żeby użytkownik był w stanie zobaczyć to wszystko nie tylko w narzędziach programistycznych, ale i w przyjaznym interfejsie na stronie głównej.

Renderowanie postów

Na początku będziemy potrzebowali czegoś, co na podstawie kolekcji postów będzie w stanie wygenerować kod HTML który wstrzykniemy w odpowiednie miejsce na stronie. Czymś takim będzie PostRenderer:

W tej klasie najważniejszą metodą jest renderPosts, która przyjmuje listę postów jako tablicę, buduje na jej podstawie odpowiedni kawałek kodu HTML i aktualizuje głównego node’a listy którego przekazujemy do konstruktora.

Dodajmy nasz renderer do pliku app.js tak, żeby można było na jego podstawie renderować posty otrzymywane z serwera:

Nasz renderer przyjmie więc element w którym chcemy renderować listę, a następnie zostanie wstrzyknięty jako zależność do PostService’u który również musi zostać teraz zaktualizowany:

Najpierw konstruktor:

Konstruktor rozszerzamy o dodanie referencji do naszego renderera, oraz o pobranie listy wszystkich postów.

W momencie tworzenia naszego serwisu wyślemy więc od razu zapytanie po wszystkie posty, następnie sformatujemy je odpowiednio i posortujemy używając metody sortPosts:

Aby posty były renderowane nie tylko w momencie tworzenia, ale też po dodaniu każdego nowego, musimy jeszcze zaktualizować metodę add:

Po utworzeniu renderera, oraz zaktualizowaniu naszego serwisu zobaczymy na stronie głównej wszystkie utworzone do tej pory posty, oraz posty które właśnie dodajemy:

A co z przejściem w tryb offline? W tej kwestii nie zmieniło się do tej pory zbyt wiele…

Wykorzystujemy naszą wiedzę o PWA

Wykorzystując wiedzę o PWA którą zdobyliśmy w poście o właśnie tej technologii, jesteśmy w stanie rozbudować naszą aplikację o nowe funkcjonalności:

  • manifest, w którym zdefiniujemy kilka podstawowych właściwości naszej aplikacji takich jak nazwa, ikonki czy kolor przewodni
  • service worker, dzięki któremu będziemy w stanie serwować użytkownikom zapisaną w cache’u wersję strony
  • wysyłanie postów w momencie kiedy użytkownik nie będzie podłączony do sieci

Nasz manifest wygląda w sposób następujący (odpowiednie ikonki przygotowałem wcześniej i umieściłem w katalogu public/images):

Manifest umieszczamy w katalogu public i dołączamy w sekcji head naszego index.pug, razem z informacjami o kolorze naszej strony:

Service Workera rejestrujemy natomiast w pliku partials/_scripts.pug:

Oczywiście to sama rejestracja – właściwy SW znajduje się w pliku public/worker.js:

W momencie wystąpienia eventu install nasz Service Worker spróbuje dodać wszystkie zadeklarowane powyżej adresy do cache’a, a następnie będzie w stanie zwracać wyniki zapytań pod te same adresy o ile wcześniej będą już zapisane. Dzięki temu będziemy mogli pracować z naszą stroną w trybie offline (sprawdź sam), jednak funkcjonalność wysyłania postów wciąż nie będzie działać tak jak powinna.

Dodawanie postów w trybie offline

Dzięki Fetch API będziemy w stanie zareagować na zapytania których wysłanie nie powiodło się. Konstrukcja wywołania fetch różni się nieco od klasycznego promise’a tym, że fragment “catch” wykona się jedynie w momencie kiedy klient nie był w stanie wysłać zapytania (odpowiedź od serwera ze statusem 404 będzie traktowana jako odpowiedź pozytywna):

The Promise returned from fetch() won’t reject on HTTP error status even if the response is an HTTP 404 or 500. Instead, it will resolve normally (with ok status set to false), and it will only reject on network failure or if anything prevented the request from completing.

Możemy więc założyć, że jeśli po wysłaniu zapytania poprzez fetch zwrócony promis będzie odrzucony, to stało się to najprawdopodobniej przez problemy z siecią. W tym miejscu obsłużymy dodawanie postów w trybie offline.

Najpierw zmodyfikujmy odpowiednio metodę add w PostService:

W przypadku kiedy wysłanie zapytania się powiedzie, chcielibyśmy wyrenderować posty w normalny sposób.  Kiedy wystąpią problemy z siecią zapiszemy nasz post w nieco inny sposób (addOffline):

Post zapisany bez dostępu do sieci nie zostanie wysłany na serwer, ale zamiast tego zostanie zapisany w źródle danych które w tym momencie mamy dostępne. Może to być baza danych (IndexedDB), albo tak jak w naszym przypadku – zwykła Local Storage. Posty zapiszemy tam pod kluczem OFFLINE_POSTS_KEY ustawianym w konstruktorze, a pobierać je będziemy w taki sposób:

Jeśli na Local Storage znajdują się posty (zapisane jako JSON), to zwrócimy je wszystkie – w przeciwnym wypadku zwrócimy pustą tablicę.

Tyle jeśli chodzi o dodawanie postów, teraz zobaczmy jakie zmiany pojawią się w konstruktorze:

Definiujemy nasz klucz pod którym będziemy trzymać posty zapisywane offline, a następnie pobieramy je do zmiennej offlinePosts. Wysyłamy tak samo jak poprzednio zapytanie na serwer, aby pobrać posty, jednak tutaj musimy obsłużyć dwie ścieżki – w trybie online musimy połączyć kolekcję postów otrzymaną z serwera z postami zapisanymi offline, a w trybie offline interesują nas jedynie posty zapisane na Local Storage. W obu przypadkach całą kolekcję postów musimy wyrenderować.

Wprowadźmy teraz zmiany w wyświetlaniu postów – chcemy, żeby użytkownik wiedział które zostały dodane offline. W tym celu zmodyfikujmy nasz PostRenderer – do metody getPostTemplate dodajmy parametr offline, który może teraz być częścią pojedynczego posta. W zależności od tego parametru dodamy lub usuniemy w naszym szablonie nową klasę dla postów dodanych offline:

Nie zapomnijmy, żeby w metodzie renderPosts przekazywać post.offline do metody getPostTemplate:

Klasę mamy, jednak nie dodaliśmy dla niej żadnego stylu. Zróbmy to teraz w pliku styles.css:

Posty dodawane offline zamiast tła będą mieć ramkę w szarym kolorze.

Zobaczmy teraz, jak wygląda dodawanie postów (tryb offline możemy wymuszać w zakładce Network – znajdziemy tam checkbox z napisem offline który zasymuluje brak połączenia z internetem). Dodawanie postów wygląda teraz tak:

 

Wygląda to całkiem nieźle! Niezależnie od tego, czy jesteśmy podłączeni do sieci czy nie, jesteśmy w stanie wysyłać posty! Poprawmy teraz nieco doświadczenie użytkownika w taki sposób, żeby wiedział co się dzieje z jego połączeniem w momencie wysyłania posta.

Nie ma sieci, ale spokojnie – wszystko działa tak jak powinno!

Dodajmy do naszego projektu nowy komponent – będzie to PostErrorHandler sterujący wyświetlaniem komunikatów błędu:

Nie zapomnij dodać go do listy skryptów w _scripts.pug!

Zmodyfikujmy też app.js tak, aby korzystać z naszego nowego mechanizmu:

Tworzymy instancję naszego error handlera, a następnie przekazujemy go do PostService’u – wykorzystamy go tam w przypadku kiedy nie uda się pobrać i zapisać postów.

Zmiany PostService dotyczą konstruktora i metody add – używamy tam naszego utworzonego “error handlera”:

Komunikat który będziemy wyświetlać wygląda tak:

Nie zamieszczam go jednak bezpośrednio w layoucie, tylko tworzę partial _errors (podobny do _scripts) i umieszczam go w środku. Główny layout wygląda teraz w ten sposób:

Dla naszego komunikatu potrzebujemy jeszcze trochę css’ów:

Zobaczmy, jak to działa.

W przypadku pobierania postów użytkownicy zobaczą przyjazny komunikat informujący o braku połączenia z internetem oraz o tym, że wciąż mogą tworzyć nowe posty:

Po każdym wysłaniu posta informacja ta będzie aktualizowana na bieżąco – będziemy ją pokazywać w momencie kiedy post został zapisany na Local Storage, oraz ukrywać kiedy wiadomość zapisano na serwerze:

Nasza aplikacja zyskała więc supermoce – do aplikacji webowej dodaliśmy funkcjonalność dodawania postów w trybie offline!

Dla nas, autorów takiej aplikacji oznacza to potencjalny wzrost użytkowników wysyłających wiadomości – w końcu nie są oni zmuszeni do bycia podłączonym do internetu. W przypadku kiedy naszego użytkownika najdzie ochota na wysłanie wiadomości w środku lasu, albo w trakcie podróży pociągiem to jest w stanie to zrobić.

Z drugiej strony, dla użytkowników oznacza to większą możliwość interakcji ze swoimi znajomymi, mniej stresu związanego z niewysłaną wiadomością przez brak połączenia oraz szybszy czas ładowania takiej strony.

Niektórzy mogą jednak zauważyć, że brakuje tutaj ostatniej, kluczowej funkcjonalności.

It’s challenge time!

W dzisiejszym poście zobaczyliśmy, jak dzięki takim technologiom jak service worker możemy dodać do naszej aplikacji funkcjonalność wysyłania postów w trybie offline. Teraz czas na ćwiczenie dla chętnych!

Zauważyliście, że aktualnie użytkownik nie ma możliwości wysyłania postów które przetrzymujemy lokalnie – to właśnie jest wyzwanie w którym możecie się zmierzyć sami! Repozytorium z aktualnym stanem aplikacji znajduje się tutaj – https://github.com/psmyrdek/PostOffline – zachęcamy do zmierzenia się z zadaniem dla chętnych, ponieważ nic tak nie rozwija jak praktyka i praca we własnym zakresie. Dajcie znać w komentarzach, jeśli wystąpią jakieś problemy albo będziecie chcieli się pochwalić działającym kodem!

Do następnego!