Dane offline

Aby zapewnić solidne działanie offline, Twoja progresywna aplikacja internetowa musi mieć funkcję zarządzania pamięcią. W rozdziale o pamięci podręcznej dowiedzieliśmy się, że pamięć podręczna to jedna z opcji zapisywania danych na urządzeniu. W tym rozdziale pokażemy Ci, jak zarządzać danymi offline, w tym trwałością danych, limitami i dostępnymi narzędziami.

Miejsce na dane

Miejsce na dane to nie tylko pliki i zasoby, ale też inne typy danych. W przypadku wszystkich przeglądarek obsługujących PWA do przechowywania danych na urządzeniu dostępne są te interfejsy API:

  • IndexedDB opcja przechowywania obiektów NoSQL dla danych ustrukturyzowanych i obiektów blob (danych binarnych).
  • WebStorage: sposób przechowywania par klucz/wartość w postaci ciągów znaków za pomocą pamięci lokalnej lub pamięci sesji. Nie jest dostępna w kontekście skryptu service worker. Ten interfejs API jest synchroniczny, więc nie zalecamy go do złożonego przechowywania danych.
  • Pamięć podręczna: omówiona w module o pamięci podręcznej.

Na obsługiwanych platformach możesz zarządzać całym miejscem na dane na urządzeniu za pomocą interfejsu Storage Manager API. Interfejsy Cache Storage API i IndexedDB zapewniają asynchroniczny dostęp do trwałej pamięci w przypadku progresywnych aplikacji internetowych i można z nich korzystać w głównym wątku, procesach roboczych i procesach usługi. Obie te technologie odgrywają kluczową rolę w zapewnianiu niezawodnego działania progresywnych aplikacji internetowych, gdy sieć jest niestabilna lub niedostępna. Kiedy jednak należy używać poszczególnych rodzajów?

Używaj interfejsu Cache Storage API w przypadku zasobów sieciowych, do których dostęp uzyskuje się przez wysłanie żądania za pomocą adresu URL, takich jak HTML, CSS, JavaScript, obrazy, filmy i dźwięk.

Do przechowywania danych strukturalnych używaj IndexedDB. Obejmuje to dane, które muszą być przeszukiwane lub łączone w sposób podobny do NoSQL, lub inne dane, np. dane dotyczące użytkownika, które niekoniecznie pasują do żądania adresu URL. Pamiętaj, że IndexedDB nie jest przeznaczona do wyszukiwania pełnotekstowego.

IndexedDB

Aby używać IndexedDB, najpierw otwórz bazę danych. Jeśli baza danych nie istnieje, zostanie utworzona nowa. IndexedDB to asynchroniczny interfejs API, ale zamiast zwracać obiekt Promise, przyjmuje wywołanie zwrotne. W poniższym przykładzie użyto biblioteki idb Jake'a Archibalda, która jest niewielką otoczką IndexedDB opartą na obietnicach. Biblioteki pomocnicze nie są wymagane do korzystania z IndexedDB, ale jeśli chcesz używać składni Promise, możesz skorzystać z biblioteki idb.

W tym przykładzie tworzymy bazę danych do przechowywania przepisów kulinarnych.

Tworzenie i otwieranie bazy danych

Aby otworzyć bazę danych:

  1. Użyj funkcji openDB, aby utworzyć nową bazę danych IndexedDB o nazwie cookbook. Bazy danych IndexedDB są wersjonowane, więc za każdym razem, gdy zmieniasz strukturę bazy danych, musisz zwiększyć numer wersji. Drugi parametr to wersja bazy danych. W przykładzie ma wartość 1.
  2. Do funkcji openDB() przekazywany jest obiekt inicjowania zawierający wywołanie zwrotne upgrade(). Funkcja wywołania zwrotnego jest wywoływana, gdy baza danych jest instalowana po raz pierwszy lub gdy jest aktualizowana do nowej wersji. Ta funkcja to jedyne miejsce, w którym mogą zachodzić działania. Działania mogą obejmować tworzenie nowych magazynów obiektów (struktur, których IndexedDB używa do porządkowania danych) lub indeksów (w których chcesz wyszukiwać). W tym miejscu powinna też nastąpić migracja danych. Zwykle funkcja upgrade() zawiera instrukcję switch bez instrukcji break, aby każdy krok był wykonywany w odpowiedniej kolejności na podstawie starej wersji bazy danych.
import { openDB } from 'idb';

async function createDB() {
  // Using https://github.com/jakearchibald/idb
  const db = await openDB('cookbook', 1, {
    upgrade(db, oldVersion, newVersion, transaction) {
      // Switch over the oldVersion, *without breaks*, to allow the database to be incrementally upgraded.
    switch(oldVersion) {
     case 0:
       // Placeholder to execute when database is created (oldVersion is 0)
     case 1:
       // Create a store of objects
       const store = db.createObjectStore('recipes', {
         // The `id` property of the object will be the key, and be incremented automatically
           autoIncrement: true,
           keyPath: 'id'
       });
       // Create an index called `name` based on the `type` property of objects in the store
       store.createIndex('type', 'type');
     }
   }
  });
}

W tym przykładzie tworzymy magazyn obiektów w bazie danych cookbook o nazwie recipes, w którym właściwość id jest kluczem indeksu magazynu, oraz tworzymy kolejny indeks o nazwie type na podstawie właściwości type.

Przyjrzyjmy się właśnie utworzonemu sklepowi z obiektami. Po dodaniu przepisów do magazynu obiektów i otwarciu Narzędzi deweloperskich w przeglądarkach opartych na Chromium lub inspektora sieci w Safari powinny być widoczne te elementy:

Safari i Chrome wyświetlające zawartość IndexedDB.

Dodawanie danych

IndexedDB używa transakcji. Transakcje grupują działania, aby były wykonywane jako jedna całość. Pomagają one zapewnić, że baza danych jest zawsze w spójnym stanie. Są one też niezbędne, jeśli masz wiele kopii aplikacji, aby zapobiec jednoczesnemu zapisywaniu tych samych danych. Aby dodać dane:

  1. Rozpocznij transakcję, ustawiając wartość parametru mode na readwrite.
  2. Uzyskaj dostęp do magazynu obiektów, do którego dodasz dane.
  3. Wywołaj funkcję add(), aby zapisać dane. Metoda otrzymuje dane w formie słownika (jako pary klucz/wartość) i dodaje je do magazynu obiektów. Słownik musi być klonowalny za pomocą klonowania strukturalnego. Jeśli chcesz zaktualizować istniejący obiekt, wywołaj metodę put().

Transakcje mają obietnicę done, która jest spełniana, gdy transakcja zostanie zakończona, lub odrzucana z błędem transakcji.

Jak wyjaśnia dokumentacja biblioteki IDB, jeśli zapisujesz dane w bazie, sygnał tx.done oznacza, że wszystkie zmiany zostały pomyślnie zapisane w bazie danych. Warto jednak poczekać na poszczególne operacje, aby zobaczyć błędy, które powodują niepowodzenie transakcji.

// Using https://github.com/jakearchibald/idb
async function addData() {
  const cookies = {
      name: "Chocolate chips cookies",
      type: "dessert",
        cook_time_minutes: 25
  };
  const tx = await db.transaction('recipes', 'readwrite');
  const store = tx.objectStore('recipes');
  store.add(cookies);
  await tx.done;
}

Po dodaniu ciasteczek przepis znajdzie się w bazie danych wraz z innymi przepisami. Identyfikator jest ustawiany i zwiększany automatycznie przez IndexedDB. Jeśli uruchomisz ten kod 2 razy, będziesz mieć 2 identyczne wpisy dotyczące plików cookie.

Pobieram dane

Aby pobrać dane z IndexedDB:

  1. Rozpocznij transakcję i określ sklep lub sklepy z obiektami oraz opcjonalnie typ transakcji.
  2. Zadzwoń do objectStore() z tej transakcji. Sprawdź, czy podana jest nazwa sklepu z obiektami.
  3. Zadzwoń pod numer get(), używając klucza, który chcesz uzyskać. Domyślnie sklep używa klucza jako indeksu.
// Using https://github.com/jakearchibald/idb
async function getData() {
  const tx = await db.transaction('recipes', 'readonly')
  const store = tx.objectStore('recipes');
// Because in our case the `id` is the key, we would
// have to know in advance the value of the id to
// retrieve the record
  const value = await store.get([id]);
}

Menedżer miejsca

Wiedza o tym, jak zarządzać pamięcią PWA, jest szczególnie ważna w przypadku prawidłowego przechowywania i przesyłania strumieniowego odpowiedzi sieciowych.

Pojemność pamięci jest współdzielona między wszystkie opcje przechowywania, w tym pamięć podręczną, IndexedDB, Web Storage, a nawet plik service worker i jego zależności. Ilość dostępnego miejsca na dane różni się jednak w zależności od przeglądarki. Prawdopodobnie nie zabraknie Ci miejsca, ponieważ w niektórych przeglądarkach witryny mogą przechowywać megabajty, a nawet gigabajty danych. Na przykład Chrome pozwala przeglądarce wykorzystywać do 80% całego miejsca na dysku, a poszczególne źródła mogą wykorzystywać do 60% całego miejsca na dysku. W przypadku przeglądarek obsługujących interfejs Storage API możesz sprawdzić, ile miejsca jest jeszcze dostępne dla Twojej aplikacji, jaki jest jej limit i jakie jest jej wykorzystanie. W tym przykładzie używamy interfejsu Storage API, aby uzyskać szacunkowy limit i wykorzystanie, a następnie obliczyć procent wykorzystanego miejsca i pozostałą liczbę bajtów. Pamiętaj, że navigator.storage zwraca instancję StorageManager. Istnieje oddzielny interfejs Storage, który łatwo pomylić z tym interfejsem.

if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> Number of bytes used.
  // quota.quota -> Maximum number of bytes available.
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`You've used ${percentageUsed}% of the available storage.`);
  const remaining = quota.quota - quota.usage;
  console.log(`You can write up to ${remaining} more bytes.`);
}

W Narzędziach deweloperskich w Chromium możesz sprawdzić przydział miejsca na dane w witrynie i jego wykorzystanie z podziałem na poszczególne elementy. Aby to zrobić, otwórz sekcję Pamięć na karcie Aplikacja.

Narzędzia deweloperskie w Chrome w sekcji Aplikacja, Wyczyść pamięć

Firefox i Safari nie oferują ekranu podsumowania, na którym można zobaczyć wszystkie limity miejsca na dane i wykorzystanie bieżącego pochodzenia.

Trwałość danych

Możesz poprosić przeglądarkę o pamięć trwałą na zgodnych platformach, aby uniknąć automatycznego usuwania danych po okresie nieaktywności lub w przypadku braku miejsca na dane. Jeśli użytkownik wyrazi zgodę, przeglądarka nigdy nie usunie danych z pamięci. Ochrona obejmuje rejestrację service workerów, bazy danych IndexedDB i pliki w pamięci podręcznej. Pamiętaj, że użytkownicy zawsze mają kontrolę i mogą w każdej chwili usunąć dane, nawet jeśli przeglądarka przyznała trwałe miejsce na dane.

Aby poprosić o stałe miejsce na dane, zadzwoń pod numer StorageManager.persist(). Tak jak wcześniej, do interfejsu StorageManager można uzyskać dostęp za pomocą usługi navigator.storage.

async function persistData() {
  if (navigator.storage && navigator.storage.persist) {
    const result = await navigator.storage.persist();
    console.log(`Data persisted: ${result}`);
}

Możesz też sprawdzić, czy w bieżącym źródle przyznano już trwałą pamięć, wywołując funkcję StorageManager.persisted(). Firefox prosi użytkownika o zgodę na korzystanie z pamięci trwałej. Przeglądarki oparte na Chromium przyznają lub odmawiają trwałości na podstawie heurystyki, aby określić znaczenie treści dla użytkownika. Jednym z kryteriów w przypadku Google Chrome jest na przykład instalacja PWA. Jeśli użytkownik zainstalował ikonę aplikacji PWA w systemie operacyjnym, przeglądarka może przyznać trwałą pamięć.

Mozilla Firefox prosi użytkownika o przyznanie uprawnień do przechowywania danych.

Obsługa interfejsu API w przeglądarce

Pamięć lokalna

Browser Support

  • Chrome: 4.
  • Edge: 12.
  • Firefox: 3.5.
  • Safari: 4.

Source

Dostęp do systemu plików

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Source

Menedżer miejsca

Browser Support

  • Chrome: 55.
  • Edge: 79.
  • Firefox: 57.
  • Safari: 15.2.

Source

Zasoby