Stopniowo ulepszaj progresywną aplikację internetową

Tworzenie aplikacji na potrzeby nowoczesnych przeglądarek i stale ulepszanie ich tak, jakbyśmy byli w 2003 r.

W marcu 2003 r. Nick FinckSteve Champeon zaskoczyli świat projektowania stron internetowych koncepcją ulepszania progresywnego, czyli strategii projektowania stron internetowych, która kładzie nacisk na wczytywanie najpierw głównej zawartości strony, a potem stopniowo dodaje do niej coraz bardziej zaawansowane technicznie warstwy prezentacji i funkcji. W 2003 r. ulepszanie stron stopniowo polegało na korzystaniu z obecnie nowoczesnych funkcji CSS, nieinwazyjnego JavaScriptu, a nawet tylko skalowalnej grafiki wektorowej. Progresywne ulepszanie w 2020 roku i później polega na korzystaniu z funkcjonalności nowoczesnych przeglądarek.

projektowanie stron internetowych z myślą o przyszłości z użyciem progresywnego ulepszania; Slajd tytułowy z pierwotnej prezentacji Fincka i Champeona.
Slajd: Projektowanie stron internetowych z uwzględnieniem potrzeb osób z ograniczeniami – przyszłość progresywnego ulepszania. (źródło)

Nowoczesny kod JavaScript

A gdy mowa o JavaScript, przeglądarki świetnie obsługują najnowsze funkcje JavaScriptu z rdzenia ES 2015. Nowy standard obejmuje obietnice, moduły, klasy, literały szablonów, funkcje strzałki, letconst, parametry domyślne, generatory, przypisanie destrukturyzowane, rest i spread, Map/Set, WeakMap/WeakSet i wiele innych. Wszystkie są obsługiwane.

Tabela CanIUse z informacjami o obsługiwanych funkcjach ES6 we wszystkich głównych przeglądarkach.
Tabela obsługi przeglądarek przez ECMAScript 2015 (ES6). (źródło)

Funkcje asynchroniczne, które są funkcją ES 2017 i jedną z moim ulubionych, można stosować we wszystkich popularnych przeglądarkach. Słowa kluczowe asyncawait umożliwiają asynchroniczne działanie oparte na obietnicach, które można zapisać w bardziej przejrzysty sposób, bez konieczności jawnej konfiguracji łańcuchów obietnic.

Tabela CanIUse z informacjami o obsługiwaniu funkcji asynchronicznych we wszystkich głównych przeglądarkach.
Tabela obsługi przeglądarek przez funkcje asynchroniczne. (źródło)

Nawet bardzo niedawne dodatki do języka ES 2020, takie jak opcjonalne łańcuchowanienullish coalescing, zostały szybko wdrożone. Poniżej znajdziesz przykładowy kod. Jeśli chodzi o podstawowe funkcje JavaScriptu, nie ma już lepszego miejsca niż to, w którym jesteś.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
Znane tło z zielonej trawy z systemu Windows XP.
Jeśli chodzi o podstawowe funkcje JavaScriptu, jest to zielona trawa. (zrzut ekranu z usługi Microsoft, użyty za zgodą)

Przykładowa aplikacja: Fugu Greetings

W tym artykule korzystam z prostej aplikacji internetowej o nazwie Fugu Greetings (GitHub). Nazwa tej aplikacji nawiązuje do projektu Fugu 🐡, który miał na celu zapewnienie przeglądarce wszystkich możliwości aplikacji na Androida, iOS i komputery. Więcej informacji o projekcie znajdziesz na jego stronie docelowej.

Fugu Greetings to aplikacja do rysowania, która umożliwia tworzenie wirtualnych kartek okolicznościowych i wysyłanie ich do bliskich. Przykłady podstawowych pojęć PWA. Jest niezawodny i w pełni umożliwia pracę offline, więc możesz z niego korzystać nawet wtedy, gdy nie masz dostępu do sieci. Można go też zainstalować na ekranie głównym urządzenia i płynnie zintegrować z systemem operacyjnym jako samodzielną aplikację.

Fugu Greetings PWA z rysunkiem przypominającym logo społeczności PWA.
Aplikacja Fugu Greetings.

Stopniowe ulepszanie

Teraz czas na omówienie progresywnego ulepszania. W glosariuszu dokumentacji MDN dotyczącej stron internetowych określa się ten termin w ten sposób:

ulepszanie stopniowe to filozofia projektowania, która zapewnia podstawową zawartość i funkcjonalność dla jak największej liczby użytkowników, a zarazem zapewnia jak najlepszą obsługę tylko użytkownikom najnowocześniejszych przeglądarek, które mogą uruchomić cały wymagany kod.

Wykrywanie funkcji jest zwykle używane do określania, czy przeglądarki mogą obsługiwać bardziej nowoczesne funkcje, podczas gdy polyfills są często używane do dodawania brakujących funkcji za pomocą JavaScript.

[…]

Progresywne ulepszanie to przydatna technika, która pozwala deweloperom stron internetowych skupić się na tworzeniu jak najlepszych witryn, jednocześnie dbając o to, aby działały one na wielu nieznanych użytkownikach. Uprzejma degradacja jest powiązana z postępnym ulepszaniem, ale nie jest tym samym i często jest postrzegana jako działanie w przeciwnym kierunku. W rzeczywistości obie metody są prawidłowe i często mogą się wzajemnie uzupełniać.

Autorzy MND

Tworzenie każdej kartki z początku może być bardzo uciążliwe. Dlaczego więc nie wprowadzić funkcji, która pozwoli użytkownikom importować obrazy i od razu z nich korzystać? W przypadku tradycyjnego podejścia do tego problemu należałoby użyć elementu <input type=file>. Najpierw utwórz element, ustaw jego type na 'file' i dodaj typy MIME do właściwości accept, a następnie programowo „kliknij” element i odczytaj zmiany. Gdy wybierzesz obraz, zostanie on zaimportowany bezpośrednio na kanwę.

const importImage = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

Jeśli istnieje funkcja importowania, powinna też być dostępna funkcja eksportowania, aby użytkownicy mogli zapisywać kartki okolicznościowe lokalnie. Tradycyjny sposób zapisywania plików polega na tworzeniu linku kotwicy z atrybutem download i adresem URL bloba jako parametrem href. Musisz też programowo „kliknąć” go, aby wywołać pobieranie. Aby zapobiec wyciekom pamięci, nie zapomnij cofnąć adresu URL obiektu blob.

const exportImage = async (blob) => {
  const a = document.createElement('a');
  a.download = 'fugu-greeting.png';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

Ale zaczekaj chwilę. W sensie mentalnym nie „pobierasz” kartki z życzeniami, tylko ją „zapisujesz”. Zamiast wyświetlić okno „Zapisz”, w którym można wybrać miejsce na dysku, przeglądarka pobrała kartkę okolicznościową bezpośrednio bez interakcji z użytkownikiem i umieściła ją bezpośrednio w folderze Pobrane. To nie jest dobre.

A co jeśli istnieje lepszy sposób? Co jeśli można by otworzyć lokalny plik, edytować go, a następnie zapisać zmiany albo w nowym pliku, albo w pierwotnym pliku, który został otwarty na początku? Okazuje się, że jest. Interfejs File System Access API umożliwia otwieranie i tworzenie plików oraz katalogów, a także ich modyfikowanie i zapisywanie .

Jak wykryć interfejs API na podstawie funkcji? Interfejs File System Access API udostępnia nową metodę window.chooseFileSystemEntries(). W związku z tym muszę warunkowo wczytywać różne moduły importu i eksportu w zależności od tego, czy dana metoda jest dostępna. Poniżej znajdziesz instrukcje.

const loadImportAndExport = () => {
  if ('chooseFileSystemEntries' in window) {
    Promise.all([
      import('./import_image.mjs'),
      import('./export_image.mjs'),
    ]);
  } else {
    Promise.all([
      import('./import_image_legacy.mjs'),
      import('./export_image_legacy.mjs'),
    ]);
  }
};

Zanim jednak omówię interfejs API dostępu do systemu plików, chciałbym krótko omówić wzorce stopniowego ulepszania. W przeglądarkach, które obecnie nie obsługują interfejsu File System Access API, ładuję starsze skrypty. Poniżej możesz zobaczyć karty sieci w Firefox i Safari.

Inspektor sieci Safari pokazujący wczytywanie starszych plików
Karta Sieć w Safari Web Inspector.
Narzędzia dla deweloperów w Firefoxie pokazujące wczytywanie starszych plików.
Karta Sieć w narzędziach dla deweloperów w Firefoxie.

W Chrome, czyli przeglądarce obsługującej interfejs API, wczytywane są tylko nowe skrypty. Jest to możliwe dzięki dynamicznym import(), które są obsługiwane przez wszystkie nowoczesne przeglądarki. Jak już wspomniałem, obecnie jest bardzo zielono.

Narzędzia deweloperskie w Chrome pokazujące wczytywanie nowoczesnych plików.
Karta Sieć w Narzędziach deweloperskich w Chrome.

Interfejs File System Access API

Teraz, gdy już to wyjaśniliśmy, przyjrzyjmy się rzeczywistej implementacji na podstawie interfejsu File System Access API. Aby zaimportować obraz, wywołuję funkcję window.chooseFileSystemEntries(), przekazując jej właściwość accepts, w której podaję, że chcę plików z obrazami. Obsługiwane są zarówno rozszerzenia plików, jak i typy MIME. W efekcie otrzymuję uchwyt pliku, z którego mogę pobrać rzeczywisty plik, wywołując getFile().

const importImage = async () => {
  try {
    const handle = await window.chooseFileSystemEntries({
      accepts: [
        {
          description: 'Image files',
          mimeTypes: ['image/*'],
          extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'],
        },
      ],
    });
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Eksportowanie obrazu przebiega prawie tak samo, ale tym razem muszę przekazać parametr typu 'save-file' metodzie chooseFileSystemEntries(). Otworzyło się okno zapisywania pliku. W przypadku otwartego pliku nie było to konieczne, ponieważ domyślnie jest to 'open-file'. Parametr accepts ustawiam podobnie jak wcześniej, ale tym razem ograniczam go tylko do obrazów PNG. Ponownie otrzymuję uchwyt pliku, ale zamiast pobierania pliku tym razem tworzymy strumień do zapisu, wywołując funkcję createWritable(). Następnie zapisuję bloba, czyli obraz kartki okolicznościowej, do pliku. Na koniec zamykam strumień do zapisu.

Wszystko może się nie udać: może zabraknąć miejsca na dysku, wystąpić błąd zapisu lub odczytu albo użytkownik może po prostu zamknąć okno. Dlatego zawsze otaczam wywołania instrukcją try...catch.

const exportImage = async (blob) => {
  try {
    const handle = await window.chooseFileSystemEntries({
      type: 'save-file',
      accepts: [
        {
          description: 'Image file',
          extensions: ['png'],
          mimeTypes: ['image/png'],
        },
      ],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Dzięki stopniowemu ulepszaniu za pomocą interfejsu File System Access API mogę otworzyć plik tak jak wcześniej. Zaimportowany plik jest nanoszony bezpośrednio na obszar roboczy. Mogę wprowadzić zmiany i ostatecznie zapisać je w oknie zapisu, w którym mogę wybrać nazwę i miejsce przechowywania pliku. Plik jest teraz gotowy do przechowywania na zawsze.

Aplikacja Fugu Greetings z oknem otwierania pliku.
Okno otwierania pliku.
Aplikacja Fugu Greetings z zaimportowanym obrazem.
Zaimportowany obraz.
Aplikacja Fugu Greetings z zmodyfikowanym obrazem.
Zapisywanie zmodyfikowanego obrazu w nowym pliku.

Web Share API i Web Share Target API

Poza przechowywaniem na wieczność może chcieć udostępnić swoją kartkę z życzeniami. Umożliwiają to interfejsy Web Share API i Web Share Target API. W systemach operacyjnych na urządzenia mobilne i komputery pojawiły się wbudowane mechanizmy udostępniania. Poniżej widać na przykład okno udostępniania w Safari na komputerze z macOS, które zostało wywołane z artykułu na naszym blogu. Po kliknięciu przycisku Udostępnij artykuł możesz udostępnić link do artykułu znajomemu, na przykład za pomocą aplikacji Wiadomości w macOS.

Plik udostępniania w Safari na komputery Mac w systemie macOS wywołany przez przycisk Udostępnij w artykule
Web Share API w przeglądarce Safari na komputerze z systemem macOS.

Kod, który to umożliwia, jest dość prosty. Wywołuję funkcję navigator.share() i przekazuję jej opcjonalne parametry title, texturl w obiekcie. Co jednak, jeśli chcę załączyć obraz? Poziom 1 interfejsu Web Share API nie obsługuje jeszcze tej funkcji. Dobra wiadomość jest taka, że w ramach poziomu 2 udostępniania plików w przeglądarce dodano możliwość udostępniania plików.

try {
  await navigator.share({
    title: 'Check out this article:',
    text: `"${document.title}" by @tomayac:`,
    url: document.querySelector('link[rel=canonical]').href,
  });
} catch (err) {
  console.warn(err.name, err.message);
}

Pokażę Ci, jak to zrobić w przypadku aplikacji kart okolicznościowych Fugu. Najpierw muszę przygotować obiekt data z tablicą files zawierającą jeden blob, a następnie title i text. Następnie, zgodnie ze sprawdzoną metodą, używam nowej metody navigator.canShare(), która działa zgodnie z nazwą: informuje mnie, czy obiekt data, który próbuję udostępnić, może być udostępniany przez przeglądarkę. Jeśli navigator.canShare() powie mi, że dane można udostępnić, mogę zadzwonić do navigator.share(), jak poprzednio. Ponieważ wszystko może się nie udać, ponownie używam bloku try...catch.

const share = async (title, text, blob) => {
  const data = {
    files: [
      new File([blob], 'fugu-greeting.png', {
        type: blob.type,
      }),
    ],
    title: title,
    text: text,
  };
  try {
    if (!(navigator.canShare(data))) {
      throw new Error("Can't share data.", data);
    }
    await navigator.share(data);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Podobnie jak wcześniej, używam stopniowych ulepszeń. Jeśli w obiekcie navigator występują zarówno atrybuty 'share', jak i 'canShare', dopiero wtedy wczytuję share.mjs za pomocą dynamicznego atrybutu import(). W przypadku przeglądarek takich jak Safari na urządzeniach mobilnych, które spełniają tylko 1 z 2 warunków, funkcja nie wczytuje się.

const loadShare = () => {
  if ('share' in navigator && 'canShare' in navigator) {
    import('./share.mjs');
  }
};

W aplikacji Fugu Greetings, jeśli kliknę przycisk Udostępnij w obsługiwanej przeglądarce, takiej jak Chrome na Androidzie, otwiera się wbudowana karta udostępniania. Mogę na przykład wybrać Gmaila, a widżet edytora e-maila otworzy się z dołączonym obrazem.

Ekran udostępniania na poziomie systemu operacyjnego z różnymi aplikacjami, do których można udostępnić obraz.
Wybieranie aplikacji, której chcesz udostępnić plik.
Widżet tworzenia e-maila w Gmailu z załączonym obrazem.
Plik zostanie dołączony do nowego e-maila w edytorze Gmaila.

Interfejs Contact Picker API

Teraz chcę porozmawiać o kontaktach, czyli książce adresowej na urządzeniu lub aplikacji do zarządzania kontaktami. Podczas pisania kartki z życzeniami nie zawsze łatwo jest poprawnie zapisać czyjeś imię i nazwisko. Mam na przykład znajomego o imieniu Sergey, który woli, aby jego imię było zapisywane cyrylicą. Używam niemieckiej klawiatury QWERTZ i nie mam pojęcia, jak wpisać ich nazwę. Ten problem można rozwiązać za pomocą interfejsu Contact Picker API. Mam kontakt do znajomego zapisany w aplikacji Kontakty na telefonie, więc mogę go wybrać w interfejsie Contacts Picker API w przeglądarce.

Najpierw muszę określić listę usług, do których chcę uzyskać dostęp. W tym przypadku interesują mnie tylko nazwy, ale w innych przypadkach mogę potrzebować numerów telefonów, adresów e-mail, awatarów, ikon lub adresów pocztowych. Następnie konfiguruję obiekt options i ustawiam wartość multiple na true, aby można było wybrać więcej niż 1 element. Na koniec mogę wywołać funkcję navigator.contacts.select(), która zwraca odpowiednie właściwości kontaktów wybranych przez użytkownika.

const getContacts = async () => {
  const properties = ['name'];
  const options = { multiple: true };
  try {
    return await navigator.contacts.select(properties, options);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Zapewne już znasz ten schemat: Wczytuję plik tylko wtedy, gdy interfejs API jest obsługiwany.

if ('contacts' in navigator) {
  import('./contacts.mjs');
}

W Fugu Greeting, gdy klikam przycisk Kontakty i wybieram moich dwóch najlepszych kumpli, Сергей Михайлович Брин劳伦斯·爱德华·"拉里"·佩奇, widać, że selektor kontaktów wyświetla tylko ich imiona i nazwiska, ale nie adresy e-mail ani inne informacje, takie jak numery telefonów. Ich imiona są potem napisane na mojej karcie okolicznościowej.

Selektor kontaktów pokazujący nazwy 2 kontaktów w książce adresowej.
Wybieranie 2 nazwisków za pomocą selektora kontaktów z książki adresowej.
imiona 2 wybranych wcześniej kontaktów na karcie okolicznościowej;
Te 2 nazwiska są następnie nanoszone na kartkę z życzeniami.

Interfejs Asynchronous Clipboard API

Kolejna czynność to kopiowanie i wklejanie. Jako deweloperzy oprogramowania uwielbiamy kopiować i wklejać. Jako autor kartek okolicznościowych czasami chcę zrobić to samo. Możesz wkleić obraz na kartce okolicznościowej, nad którą pracujesz, lub skopiować kartkę, aby kontynuować jej edytowanie w innym miejscu. Interfejs Async Clipboard API obsługuje zarówno tekst, jak i obrazy. Pokażę Ci, jak dodać obsługę kopiowania i wklejania w aplikacji Fugu Greetings.

Aby skopiować coś do schowka systemowego, muszę coś w nim zapisać. Metoda navigator.clipboard.write() przyjmuje tablicę elementów schowka jako parametr. Każdy element schowka to obiekt, którego wartością jest blob, a kluczem typ bloba.

const copy = async (blob) => {
  try {
    await navigator.clipboard.write([
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Aby wkleić, muszę przejść przez elementy schowka, które uzyskuję przez wywołanie navigator.clipboard.read(). Dzieje się tak, ponieważ na schowku może znajdować się wiele elementów w różnych reprezentacjach. Każdy element skopiowany do schowka ma pole types, które zawiera typy MIME dostępnych zasobów. Wywołuję metodę getType() elementu skopiowanego do schowka, przekazując wcześniej uzyskany typ MIME.

const paste = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      try {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          return blob;
        }
      } catch (err) {
        console.error(err.name, err.message);
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Nie trzeba chyba tego tłumaczyć. Robię to tylko w obsługiwanych przeglądarkach.

if ('clipboard' in navigator && 'write' in navigator.clipboard) {
  import('./clipboard.mjs');
}

Jak to działa w praktyce? Mam otwarty obraz w aplikacji Podgląd na komputerze Mac i kopiuję go do schowka. Gdy klikam Wklej, aplikacja Fugu Greetings pyta, czy chcę zezwolić jej na dostęp do tekstu i obrazów ze schowka.

Aplikacja Fugu Greetings z prośbą o dostęp do schowka.
Prośba o dostęp do schowka.

Gdy użytkownik zaakceptuje uprawnienia, obraz zostanie wklejony do aplikacji. Działa to też w drugą stronę. Poczekaj, skopiuję kartkę z życzeniami na schowek. Gdy otworzysz Podgląd, klikniesz Plik, a potem Nowy z Schowka, karta z życzeniami zostanie wklejona do nowego obrazu bez tytułu.

Aplikacja Podgląd na macOS z wklejony obraz bez tytułu.
Obraz wklejony do aplikacji Podgląd w macOS.

Badging API

Innym przydatnym interfejsem API jest Badging API. Jako instalowana aplikacja PWA Fugu Greetings ma oczywiście ikonę, którą użytkownicy mogą umieścić na pasku aplikacji lub ekranie głównym. Ciekawym i łatwym sposobem na zademonstrowanie interfejsu API jest jego wykorzystanie w programie Fugu Greetings jako licznika pociągnięć pióra. Dodałem detektor zdarzeń, który zwiększa licznik pociągnięć pióra za każdym razem, gdy wystąpi zdarzenie pointerdown, a następnie ustawia zaktualizowany znacznik ikony. Gdy wyczyścisz płótno, licznik zostanie zresetowany, a plakietka zostanie usunięta.

let strokes = 0;

canvas.addEventListener('pointerdown', () => {
  navigator.setAppBadge(++strokes);
});

clearButton.addEventListener('click', () => {
  strokes = 0;
  navigator.setAppBadge(strokes);
});

Ta funkcja jest ulepszaniem stopniowym, więc logika wczytywania jest taka sama jak zwykle.

if ('setAppBadge' in navigator) {
  import('./badge.mjs');
}

W tym przykładzie rysuję cyfry od 1 do 7, używając pojedynczego pociągnięcia pióra na każdą cyfrę. Licznik na ikonie ma teraz wartość 7.

Liczba od 1 do 7 narysowana na karcie okolicznościowej jednym pociągnięciem pióra.
Rycowanie cyfr od 1 do 7 za pomocą 7 piórem.
Ikona plakietki w aplikacji Fugu Greetings z liczbą 7.
Licznik pociągnięć piórkiem w postaci plakietki z ikoną aplikacji.

Interfejs Periodic Background Sync API

Chcesz zaczynać każdy dzień od czegoś nowego? Ciekawa funkcja aplikacji Fugu Greetings polega na tym, że każdego ranka może ona inspirować Cię nowym obrazem tła, aby ułatwić Ci rozpoczęcie tworzenia kartki z życzeniami. W tym celu aplikacja używa interfejsu Periodic Background Sync API.

Pierwszym krokiem jest register okresowego zdarzenia synchronizacji w rejestracji usługi. Wysłuchuje tag synchronizacji o nazwie 'image-of-the-day'. Ma minimalny interwał wynoszący 1 dzień, dzięki czemu użytkownik może co 24 godziny otrzymywać nowy obraz tła.

const registerPeriodicBackgroundSync = async () => {
  const registration = await navigator.serviceWorker.ready;
  try {
    registration.periodicSync.register('image-of-the-day-sync', {
      // An interval of one day.
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Drugim krokiem jest nasłuchiwanie zdarzenia periodicsync w usługach działających w tle. Jeśli tag zdarzenia to 'image-of-the-day', czyli ten, który został zarejestrowany wcześniej, obraz dnia jest pobierany za pomocą funkcji getImageOfTheDay(), a wynik jest propagowany do wszystkich klientów, aby mogli zaktualizować swoje kanwy i pamięć podręczną.

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        const blob = await getImageOfTheDay();
        const clients = await self.clients.matchAll();
        clients.forEach((client) => {
          client.postMessage({
            image: blob,
          });
        });
      })()
    );
  }
});

Jest to naprawdę stopniowe ulepszenie, ponieważ kod jest wczytywany tylko wtedy, gdy przeglądarka obsługuje daną usługę API. Dotyczy to zarówno kodu klienta, jak i kodu usługi. W nieobsługiwanych przeglądarkach żaden z nich nie jest wczytywany. Zwróć uwagę, że w skrypcie service worker zamiast dynamicznego elementu import() (który nie jest jeszcze obsługiwany w kontekście skryptu service worker) używam klasycznego elementu importScripts().

// In the client:
const registration = await navigator.serviceWorker.ready;
if (registration && 'periodicSync' in registration) {
  import('./periodic_background_sync.mjs');
}
// In the service worker:
if ('periodicSync' in self.registration) {
  importScripts('./image_of_the_day.mjs');
}

W aplikacji Fugu Greetings naciśnięcie przycisku Tapeta powoduje wyświetlenie obrazka z kartki okolicznościowej na dany dzień, który jest aktualizowany codziennie za pomocą interfejsu API do okresowej synchronizacji w tle.

Aplikacja Fugu Greetings z nowym obrazem kartki z życzeniami na dany dzień.
Kliknięcie przycisku Tapeta powoduje wyświetlenie zdjęcia dnia.

Notification Triggers API

Czasami nawet przy dużej inspiracji potrzebna jest pomoc, aby dokończyć rozpoczętą kartkę z życzeniami. Ta funkcja jest włączana przez interfejs Notification Triggers API. Jako użytkownik mogę określić czas, w którym chcę otrzymać przypomnienie o dokończeniu kartki okolicznościowej. W tym czasie otrzymam powiadomienie, że moja kartka z życzeniami jest gotowa.

Po wyświetleniu prompta z prośbą o wyznaczenie czasu aplikacja zaplanowała powiadomienie z użyciem showTrigger. Może to być TimestampTrigger z wybraną wcześniej datą docelową. Powiadomienie o przypomnieniu zostanie wywołane lokalnie, bez udziału sieci ani serwera.

const targetDate = promptTargetDate();
if (targetDate) {
  const registration = await navigator.serviceWorker.ready;
  registration.showNotification('Reminder', {
    tag: 'reminder',
    body: "It's time to finish your greeting card!",
    showTrigger: new TimestampTrigger(targetDate),
  });
}

Podobnie jak w przypadku wszystkich innych funkcji, które do tej pory pokazałem, jest to udoskonalenie stopniowe, więc kod jest wczytywany tylko warunkowo.

if ('Notification' in window && 'showTrigger' in Notification.prototype) {
  import('./notification_triggers.mjs');
}

Gdy zaznaczę pole wyboru Przypomnienie w Fugu Greetings, pojawia się komunikat z pytaniem, kiedy mam otrzymać przypomnienie o dokończeniu kartki okolicznościowej.

Aplikacja Fugu Greetings z prośbą o wybranie przez użytkownika, kiedy ma otrzymać przypomnienie o dokończeniu kartki okolicznościowej.
Zaplanowanie lokalnego powiadomienia z przypomnieniem o dokończeniu kartki okolicznościowej.

Gdy zaplanowane powiadomienie zostanie uruchomione w Fugu Greetings, wyświetli się ono tak samo jak każde inne powiadomienie, ale jak już wspomniałem, nie wymaga połączenia z internetem.

Centrum powiadomień w systemie macOS wyświetlające powiadomienie z aplikacji Fugu Greetings.
Wywołane powiadomienie pojawi się w Centrum powiadomień macOS.

Wake Lock API

Chcę też uwzględnić interfejs Wake Lock API. Czasami wystarczy tylko wpatrywać się w ekran, aż pojawi się inspiracja. Najgorsze, co może się stać, to wyłączenie ekranu. Interfejs Wake Lock API może temu zapobiec.

Pierwszym krokiem jest uzyskanie blokady aktywacji za pomocą funkcji navigator.wakelock.request method(). Przekazuję mu ciąg znaków 'screen', aby uzyskać blokadę ekranu. Następnie dodaję detektor zdarzenia, aby otrzymywać powiadomienia o zwolnieniu blokady aktywacji. Może się tak zdarzyć np. wtedy, gdy zmieni się widoczność karty. Jeśli tak się stanie, gdy karta znów będzie widoczna, mogę ponownie uzyskać blokadę aktywacji.

let wakeLock = null;
const requestWakeLock = async () => {
  wakeLock = await navigator.wakeLock.request('screen');
  wakeLock.addEventListener('release', () => {
    console.log('Wake Lock was released');
  });
  console.log('Wake Lock is active');
};

const handleVisibilityChange = () => {
  if (wakeLock !== null && document.visibilityState === 'visible') {
    requestWakeLock();
  }
};

document.addEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('fullscreenchange', handleVisibilityChange);

Tak, jest to stopniowe ulepszenie, więc muszę je załadować tylko wtedy, gdy przeglądarka obsługuje interfejs API.

if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
  import('./wake_lock.mjs');
}

W Fugu Greetings jest pole wyboru Insomnia, które po zaznaczeniu utrzymuje ekran w stanie czuwania.

Zaznaczenie pola wyboru bezsenność powoduje, że ekran nie wyłącza się.
Zaznaczone pole wyboru Insomnia sprawia, że aplikacja nie śpi.

Interfejs Idle Detection API

Czasami nawet godzinami wpatrywanie się w ekran nie przynosi rezultatów i nie masz najmniejszego pojęcia, co zrobić z kartką z życzeniami. Interfejs Idle Detection API umożliwia aplikacji wykrywanie czasu bezczynności użytkownika. Jeśli użytkownik przez zbyt długi czas nie wykonuje żadnych czynności, aplikacja wraca do stanu początkowego i wyczyści płótno. Ten interfejs API jest obecnie dostępny tylko po udzieleniu uprawnień do wysyłania powiadomień, ponieważ wiele przypadków użycia wykrywania nieaktywności w produkcji jest związanych z powiadomieniami, na przykład wysyłaniem powiadomienia tylko na urządzenie, z którego użytkownik w danym momencie aktywnie korzysta.

Po upewnieniu się, że uprawnienia do powiadomień zostały przyznane, tworzymy instancję detektora bezczynności. Rejestruję detektor zdarzeń, który wykrywa zmiany stanu bezczynności, w tym stan użytkownika i ekranu. Użytkownik może być aktywny lub nieaktywny, a ekran może być odblokowany lub zablokowany. Jeśli użytkownik jest nieaktywny, płótno się czyści. Detektor bezczynności ma próg 60 sekund.

const idleDetector = new IdleDetector();
idleDetector.addEventListener('change', () => {
  const userState = idleDetector.userState;
  const screenState = idleDetector.screenState;
  console.log(`Idle change: ${userState}, ${screenState}.`);
  if (userState === 'idle') {
    clearCanvas();
  }
});

await idleDetector.start({
  threshold: 60000,
  signal,
});

Jak zawsze ładuję ten kod tylko wtedy, gdy przeglądarka go obsługuje.

if ('IdleDetector' in window) {
  import('./idle_detection.mjs');
}

W aplikacji Fugu Greetings puste pole Ephemeral (Efemerydalność) jest zaznaczone, a użytkownik zbyt długo nie wykonuje żadnych czynności.

Aplikacja Fugu Greetings z pustym ekranem po zbyt długim czasie bezczynności użytkownika
Gdy pole wyboru Tymczasowy jest zaznaczone, a użytkownik zbyt długo nie był aktywny, płótno zostanie wyczyszczone.

Zakończenie

Uff, co za przejażdżka. Tyle interfejsów API w jednej przykładowej aplikacji. Pamiętaj, że nigdy nie zmuszam użytkownika do płacenia za pobieranie funkcji, której nie obsługuje jego przeglądarka. Dzięki stopniowemu ulepszaniu mogę mieć pewność, że wczytuje się tylko odpowiedni kod. W przypadku protokołu HTTP/2 żądania są tanie, więc ten schemat powinien dobrze działać w wielu aplikacjach, chociaż w przypadku naprawdę dużych aplikacji warto rozważyć użycie pakietów.

Panel sieciowy w narzędziach programistycznych Chrome, który pokazuje tylko żądania plików z kodem obsługiwanym przez bieżącą przeglądarkę
Karta Sieć w Narzędziach deweloperskich w Chrome, która pokazuje tylko żądania plików z kodem obsługiwanym przez bieżącą przeglądarkę.

Aplikacja może wyglądać nieco inaczej w różnych przeglądarkach, ponieważ nie wszystkie platformy obsługują wszystkie funkcje. Główne funkcje są jednak zawsze dostępne – są one stopniowo ulepszane zgodnie z możliwościami danej przeglądarki. Pamiętaj, że te funkcje mogą się zmieniać nawet w tym samym przeglądarce w zależności od tego, czy aplikacja działa jako zainstalowana aplikacja czy na karcie przeglądarki.

Fugu Greetings w Chrome na Androida, pokazujące wiele dostępnych funkcji
Fugu Greetings w Chrome na Androida.
Fugu Greetings w Safari na komputerze, z mniejszą liczbą dostępnych funkcji
Fugu Greetings w Safari na komputerze.
Fugu Greetings w Chrome na komputery, pokazujące wiele dostępnych funkcji
Fugu Greetings w Chrome na komputerze.

Jeśli interesuje Cię aplikacja Fugu Greetings, znajdź ją i rozwiń na GitHubie.

Repozytorium Fugu Greetings w GitHub.
Aplikacja Fugu Greetings w GitHub.

Zespół Chromium ciężko pracuje nad tym, aby interfejsy Fugu API były jeszcze lepsze. Stosując ulepszanie stopniowe podczas tworzenia aplikacji, zapewniam wszystkim użytkownikom dobre, solidne wrażenia z korzystania z aplikacji, ale użytkownicy korzystający z przeglądarek obsługujących więcej interfejsów API platformy internetowej mają jeszcze lepsze wrażenia. Z niecierpliwością czekam na to, jak wykorzystasz stopniowe ulepszanie w swoich aplikacjach.

Podziękowania

Dziękuję Christianowi LiebelowiHemanthowi HM za ich wkład w projekt Fugu Greetings. Ten artykuł został sprawdzony przez Joe Medley i Kayce Basques. Jake Archibald pomógł mi ustalić sytuację z dynamicznym import() w kontekście usługi.