Ta strona używa cookies
sprawdź politykę prywatności

Rozumiem

Progressive Web App – IndexedDB i Background Sync.

27/3/2019
Bartek Cis

W tym wpisie dowiesz się jak działa IndexedDB i w jaki sposób użyć jej w Progressive Web App podczas Background Synchronization.

Czego się dzisiaj dowiesz?

Opiszę jak czym jest i jak użyć IndexedDB w aplikacji Webowej w kontekście Background Synchronization. Jest to specjalna właściwość którą możesz wykorzystać przy okazji Progressive Web Apps czyli aplikacji internetowych które działają offline.

Zanim zaczniesz

Zapoznaj się z poprzednimi wpisami z serii jeśli chcesz dowiedzieć się więcej (od podstaw) o PWA!

Plan działania

  1. Po co ci Background Sync?
  2. IndexedDB
  3. Background Sync

Seria Progressive Web App

Opisuje tutaj czym jest PWA i  jak stworzyć ją na WordPressie. Kolejne artykuły będą na bieżąco uzupełniane:

  1. Progressive Web App na WordPress – Teoria, Środowisko i Lighthouse
  2. Progressive Web App. Tworzę manifest.json i Service Worker
  3. Progressive Web App – update Cache i Push Notification
  4. Progressive Web App – indexedDB i Background Sync

Background Synchronization

Jest możliwość wysłania danych na serwer nawet gdy aplikacja nie ma połączenia z internetem. Możliwe jest to dzięki IndexedDB i Service Worker’owi.

Jeżeli użytkownik chce wysłać jakieś dane np. przez formularz kontaktowy to aplikacja wysyła je do IndexedDB która działa offline. Następnie Service Worker te dane pobierze z tej bazy danych i wyśle na serwer gdy urządzenie nawiąże połączenie z internetem.

Misja Specjalna

Żeby lepiej zilustrować moc tej funkcji wytłumaczę to na przykładzie scenki rodzajowej.

Michał Wołodyjowski

Pierwszy rycerz Rzeczypospolitej. Jego umiejętności i sława wojenna odbijają się echem wplecione w wersy pieśni śpiewanych przy ogniskach połowy znanego Świata.

Naprawdę nie chcesz stanąć przeciw niemu w pojedynku na szable…

Król Jan Kazimierz dowiedziawszy się o wkroczeniu Szwedów do Wielkopolski oraz zebraniu się oddziałów wielkopolskich pod Ujściem postanowił wysłać tam Pana Michała aby zlustrował sytuacje i jak najszybciej zdał pełny raport…

Bitwa pod Ujściem

Niestety Pan Michał dotarł tam zbyt późno. Zatrzymał się na skraju lasu i obserwował jak Wielkopolanie łamią się pod naporem Szwedów…

Powiadomić Króla

Pan Michał wyciągnął swojego smartfona aby powiadomić Króla o sytuacji.

“Kurza stopa! Nie mam zasięgu w tym gęstym lesie…”

Odpalił Messengera ale ten nawet nie chciał nawiązać połączenia… Wiedział jednak, że Król Jan Kazimierz zafascynował się ostatnio bardzo aplikacjami internetowymi które działają bez internetu. Chyba PWA albo jakoś tak się nazywają…

Słyszał, że Król zlecił wykonanie swojej osobistej strony niejakiemu Bartkowi z Bedekodzic.pl. Tamten normalnie nie wykonuje takich zleceń ale w tym wypadku nie mógł odmówić. Zlecenie od Króla to największy zaszczyt. Warunki były dwa- nowoczesny design i musi być PWA!

Król nie czyta SMSów. Nie ma nawet co marzyć aby się do niego dodzwonić. Za to ostatnio jest mocno podjarany swoją PWA 🙂 Sprawdza maila od formularza.

Michał postanowił użyć stronki PWA Króla i formularza który tam jest. Był już na niej wcześniej. Chciał sprawdzić czy ten design rzeczywiście ciekawy…

Jak stworzyć Background Sync?

A więc aby pomóc Panu Wołodyjowskiemu w jego misji wykonałem następujące kroki:

Główny wątek JS

  1. Stwórz instancję indexedDB
  2. Stwórz funkcję która będzie pobierała dane z formularza i zapisywać je w indexedDB
  3. Stwórz alternatywną funkcję która będzie wysyłać dane na serwer jeśli przeglądarka nie obsługuje SW
  4. Zarejestruj SW
  5. Jeżeli SW obsługuje Backgroud Sync to zdefiniuj event który wywoła funkcję zapisującą dane z formularza w indexedDB
  6. Jeżeli się to udało to przekaż informację do SW

Service Worker

  1. Stwórz funkcję która pobierze dane z indexedDB
  2. Stwórz funkcję która wyśle dane na serwer
  3. Stwórz funkcję która zsynchronizuje dwa powyższe kroki
  4. Odbierz informację o evencie w Service Worker’ze
  5. Wywołaj funkcję synchronizującą wewnątrz Service Worker’a

A więc jak widzisz jest sporo tych kroków ale zaraz wszystko ładnie się rozjaśni. Użyje też trochę innego syntaxu niż w poprzednich wpisach. Trochę nowszego 🙂 Ostrzegam też, że będzie sporo asynchronicznego kodu…

IndexedDB

Na początku dobrze wiedzieć czym jest ta baza danych otóż definicja opisuje ją jako niskopoziomowe API przeznaczone do tego aby przechowywać duże ilości strukturyzowanych danych (czyli zapisanych w formie obiektów) po stronie klienta.

W praktyce jest to baza danych zapisana na komputerze użytkownika która jest zintegrowana z przeglądarką. W praktyce jest to mechanizm podobny do Web Storage przeznaczony do przechowywania większej ilości danych i umożliwiająca szybkie wyszukiwanie po stronie klienta. MDN ma bardzo fajną dokumentację na ten temat.

Główny wątek JS

Kod poniżej umieszczasz w Twoim głównym pliku z JS’em

Inicjalizacja DB

To teraz będzie kod. Będzie to wszystko napisane za pomocą klasy JS. Przy każdej nowej funkcjonalności będę pokazywał odpowiednią metodę. Na końcu wkleję kompletny kod całego pliku JS.

Na początku w konstruktorze klasy dodam button który będzie wysyłał formularz:

constructor() {
    this.button = document.querySelector('#form-button');
}

A tworzenie bazy danych wygląda tak:

initializeIndexedDb() {
    // Stwórz nową bazę
    let messageToKing = window.indexedDB.open('messageDb');
    // Gdy nowa naza jest tworzona to nadaj jej strukturę
    messageToKing.onupgradeneeded = (event) => {
        let db = event.target.result;
        // Stwórz nową "tablicę" w bazie
        let messageObjStore = db.createObjectStore('messageObjStore', { autoIncrement: true });
        // Dodawaj kolejne właściwości do tablicy
        messageObjStore.createIndex('name', 'name', { unique: false });
        messageObjStore.createIndex('army', 'army', { unique: false });
        messageObjStore.createIndex('message', 'message', { unique: true });
        messageObjStore.createIndex('dateAdded', 'dateAdded', { unique: true });
    }
}

Jeśli operacja się powiedzie to możesz to sprawdzić w devToolsach:

Pobierz dane z formularza i zapisz w bazie danych

Teraz dane z formularza zostaną wrzucone do stworzonej wcześniej bazy. Według zasady DRY (don’t repeat yourself) na początku napiszę funkcję która pobiera dane z formularza:

getFormData() {
    return {
        name: document.querySelector('#formName').value,
        army: document.getElementById('#formArmy').value,
        message: document.getElementById('#formMessage').value,
        dateAdded: new Date()
    };
}

Teraz wrzucenie danych do IndexedDB:

formData() {
    // Tworzysz funkcję asynchroniczną
    return new Promise((resolve, reject) => {
        // Łączysz się z odpowiednią indexedDB
        let messageToKing = window.indexedDB.open('messageDb');
        // Jak się uda to wrzucasz dane
        messageToKing.onsuccess = event => {
            let objStore = messageToKing.result.transaction('messageObjStore', 'readwrite')
            .objectStore('messageObjStore');
            objStore.add(this.getFormData());
            resolve();
        }
        // Jak nie to nie 🙂
        messageToKing.onerror = err => {
            reject(err);
        }
    });
}

Wyślij dane bezpośrednio na serwer

To już wiesz jak stworzyć IndexedDB i wrzucić do niej jakieś dane jednak jeżeli Service Worker nie działa w danej przeglądarce to chcesz aby zostały one bezpośrednio wysłane na serwer omijając SW. Możesz to zrobić np. tak:

formDataToServer() {
    return fetch('your server url', {
        method: 'POST',
        body: JSON.stringify(this.getFormData()),
        headers:{
            'Content-Type': 'application/json'
        }
    }).then(() => {
        console.log('Wiadomośc wysłana');
    }).catch((err) => {
        console.log(`Wystąpił błąd: ${err}`);
    })
}

Oczywiście jest to dość uproszczona metoda i zakładam, że Twój JS obsługuje fetch. Jeśli nie to możesz skorzystać np. z konfiguracji Webpacka 4 którą opisałem w jednym z poprzednich wpisów.

Rejestracja Service Worker’a

Jeżeli czytałeś/aś poprzednie wpisy z serii to ten krok już znasz. Tutaj użyję trochę innego syntaxu i wezmę pod uwagę to, czy SW był już rejestrowany w przeglądarce:

registerServiceWorker() {
    // Sprawdź czy SW jest obsługiwany
    if (navigator.serviceWorker) {
        // Sprawdz czy na stronie są juz zarejestrowane jakies SW
        navigator.serviceWorker.getRegistrations()
        .then(registrations => {
            // Jezeli nie ma to zarejestruj swoj SW
            if (registrations.length === 0) {
                navigator.serviceWorker.register('serviceWorker.js');
            }})
        .then(() => {
        // Zwróc aktualną rejestrację SW
            return navigator.serviceWorker.ready;
        })
        .then(registration => {
            console.log('Tu będzie blok kodu z Background Sync');
        })
    } else {
        // Jezeli przegladarka nie obsluguje SW to wyslij dane przez
        // standardowy POST bezpośrednio na serwer
        this.button.addEventListener('click', () => {
            this.formDataToServer();
        });
    }
}

W miejscu console.log będzie kod wywołujący Background Sync. Dla przejrzystości w następnym akapicie 🙂

Backgroud Sync

W końcu 🙂 Cały kod wewnątrz promisa z poprzedniego akapitu:

.then(registration => {
    // Jeśli user kliknie w wysyłkę formularza
    this.button.addEventListener('click', (event) => {
        event.preventDefault();
        // Wywołaj funkcję zapisującą dane do indexedDB
        this.formDataToDb().then(function() {
            // Jezeli Background Sync działa to
            if(registration.sync) {
            // Zarejestruj zdarzenie to podanej nazwie
            registration.sync.register('message-to-king')
                .catch(function(err) {
                // Zwróc błąd
                return err;
                })
            }
        });
    })
})

Każde ze zdarzeń w Twojej aplikacji będzie obsługiwane przez osobny event więc ten krok należy powtórzyć w zależności od potrzeb.

Cały kod w JavaScript

Teraz zbieram wszystkie kawałki w jedną całość:

class kingsPage {
    constructor() {
        this.button = document.querySelector('#form-button');
    }

    init() {
        this.initializeIndexedDb();
        this.registerServiceWorker();
    }

    initializeIndexedDb() {
        let messageToKing = window.indexedDB.open('messageDb');
        
        messageToKing.onupgradeneeded = (event) => {
            let db = event.target.result;
            let messageObjStore = db.createObjectStore('messageObjStore', { autoIncrement: true });
            
            messageObjStore.createIndex('name', 'name', { unique: false });
            messageObjStore.createIndex('army', 'army', { unique: false });
            messageObjStore.createIndex('message', 'message', { unique: true });
            messageObjStore.createIndex('dateAdded', 'dateAdded', { unique: true });
        }
    }

    getFormData() {
        return {
            name: document.querySelector('#formName').value,
            army: document.getElementById('#formArmy').value,
            message: document.getElementById('#formMessage').value,
            dateAdded: new Date()
        };
    }

    formDataToDb() {
        return new Promise((resolve, reject) => {
            let messageToKing = window.indexedDB.open('messageDb');

            messageToKing.onsuccess = event => {
                let objStore = messageToKing.result.transaction('messageObjStore', 'readwrite')
                .objectStore('messageObjStore');
                objStore.add(this.getFormData());
                resolve();
            }

            messageToKing.onerror = err => {
                reject(err);
            }
        });
    }

    formDataToServer() {
        return fetch('your server url', {
            method: 'POST',
            body: JSON.stringify(this.getFormData()),
            headers:{
                'Content-Type': 'application/json'
            }
        }).then(() => {
            console.log('Wiadomośc wysłana');
        }).catch((err) => {
            console.log(`Wystąpił błąd: ${err}`);
        })
    }

    registerServiceWorker() {
        if (navigator.serviceWorker) {
            navigator.serviceWorker.getRegistrations()
            .then(registrations => {
                if (registrations.length === 0) {
                    navigator.serviceWorker.register('serviceWorker.js');
                }})
            .then(() => {
                return navigator.serviceWorker.ready;
            })
            .then(registration => {
                this.button.addEventListener('click', (event) => {
                    event.preventDefault();
                    this.formDataToDb().then(function() {
                    if(registration.sync) {
                        registration.sync.register('message-to-king')
                        .catch(function(err) {
                            return err;
                            })
                        }
                    });
                })
            })
        } else {
            this.button.addEventListener('click', () => {
                this.formDataToServer();
            });
        }
    }
}

const coreJs = new kingsPage();

window.addEventListener('load', () => {
    coreJs.init();
});

Service Worker

Od tego miejsca wszystko będzie się działo w zarejestrowanym pliku z Service Workerem.

Pobieranie danych z IndexedDB

A więc zacznę od pobrania wcześniej wysłanych danych:

function getDataFromDb () {
    return new Promise((resolve, reject) => {
        let db = indexedDB.open('messageDb');
        
        db.onsuccess = () => {
            // Pobierz zawartośc bazy
            db.result.transaction('messageObjStore').objectStore('messageObjStore').getAll()
            .onsuccess = (event) => {
                // Podaj zawarotśc dalej
                resolve(event.target.result);
            }
        }
        // W razie bledu wykonaj odpowiednią akcję
        db.onerror = (err) => {
            reject(err);
        }
    });
}

Wyślij dane na serwer

Następnie napiszę funkcję która wysyła dane na serwer. W zasadzie jest taka sama jak w głównym wątku JS:

function sendToServer(response) {
    return fetch('your server address', {
        method: 'POST',
        body: JSON.stringify(response),
        headers:{
            'Content-Type': 'application/json'
        }
    })
    .catch(err => {
        return err;
    });
}

Zsynchronizuj poprzednie akcje

Czyli napisz funkcję która będzie wykonywała dwie powyższe jedna po drugiej:

function synchronize() {
    return getDataFromDb()
    .then(sendToServer)
    .catch(function(err) {
        return err;
    });
}

Odbierz informację o evencie w Service Worker’ze

Teraz SW ma odbierać wszelkie eventy zdefiniowane wcześniej w głównym wątku JS:

self.onsync = event => {
    console.log(event);
}

Jak widzisz ten syntax jest inny od tego pokazanego w poprzednich wpisach serii. Analogicznie możesz postąpić z innymi metodami SW:

self.onfetch = event => {}

self.oninstall = event => {}

Funkcja synchronizująca wewnątrz Service Worker’a

Więc teraz sprawdzę czy przychodzący event jest tym o który mi chodzi. Jeśli tak to wywołam napisane wcześniej funkcje:

self.onsync = event => {
    if(event.tag === 'message-to-king') {
        event.waitUntil(synchronize());
    }
}

Jeżeli wszystko zrobiłeś/aś OK to Background Synchronization powinno działać jak należy! Jeszcze na koniec kod z SW:

self.onsync = event => {
    if(event.tag === 'message-to-king') {
        event.waitUntil(synchronize());
    }
}

function getDataFromDb () {
    return new Promise((resolve, reject) => {
        let db = indexedDB.open('messageDb');
        db.onsuccess = () => {
            db.result.transaction('messageObjStore').objectStore('messageObjStore').getAll()
            .onsuccess = event => {
                resolve(event.target.result);
            }
        }
        db.onerror = err => {
            reject(err);
        }
    });
}

function sendToServer(response) {
    return fetch('your server address', {
        method: 'POST',
        body: JSON.stringify(response),
        headers:{
            'Content-Type': 'application/json'
        }
    })
    .catch(err => {
        return err;
    });
}

function synchronize() {
    return getDataFromDb()
    .then(sendToServer)
    .catch(function(err) {
        return err;
    });
}

Testowanie Background Sync

Z poziomu devTools’ów. To powinno być Ci już znane:

Tymczasem Michał Wołodyjowski

Napisał do Króla taką wiadomość:

Po czym powiedział:

“Obym jak najszybciej odzyskał zasięg w telefonie. Król musi o tym się niezwłocznie dowiedzieć…”

Nie czekając już więcej chwycił mocno lejce i popędził konia w stronę Warszawy. Szykuje się wielka wojna…

Podsumowanie

To miał być ostatni wpis z serii o PWA ale… Będzie jeszcze jeden. Stworzę prostą aplikację w której zbiorę wszystko co do tej pory napisałem w jedną całość i to uporządkuje. Dopiszę obsługę w Back Endu w NodeJS i udostępnię to repo na GitHubie 🙂 To będzie takie kompedium wiedzy o Service Worker’ach w apkach webowych 🙂

Mam nadzieję, że zarówno ten jak i te poprzednie artykuły Ci się podobały. Wiesz już jak działają Service Workery i jakie mają możliwość początkiem 2019 roku. Teraz wiesz jak:

  1. Stworzyć plik manifest.json
  2. Zarejestrować i zainstalować SW
  3. Działa mechanizm cache w SW
  4. W jaki sposób bezpośrednio przesyłać dane między głównym wątkiem JS a SW
  5. Czym jest i jak zbudować Push Notification
  6. O co chodzi z IndexedDB
  7. Napisać kod który będzie obsługiwał Background Sync

To w zasadzie większość obecnych możliwości Service Worker’ów. 

Zachęcam Cię też do nauki Naszej historii 🙂 Możesz wierzyć lub nie ale nasi przodkowie byli naprawdę niesamowitymi ludźmi. Dlaczego nie pójść w ich ślady?

Podziel się z innymi 🙂

Cześć jestem Bartek.

Na tym blogu wprowadzę Cię w tajniki Front-Endu i programowania webowego.

Warto
Social media & sharing icons powered by UltimatelySocial