Stopniowo ulepszaj progresywną aplikację internetową

Tworzenie stron pod nowoczesne przeglądarki i stopniowe ulepszanie ich tak, jak w 2003 roku

Opublikowano: 29 czerwca 2020 r.

W marcu 2003 r. Nick FinckSteve Champeon zaskoczyli świat projektowania stron internetowych koncepcją stopniowego ulepszania, czyli strategią projektowania stron internetowych, która polega na tym, że najpierw wczytywana jest podstawowa zawartość strony, a potem stopniowo dodawane są bardziej złożone i zaawansowane technicznie warstwy prezentacji i funkcji. W 2003 r. progresywne ulepszanie polegało na używaniu nowoczesnych (jak na tamte czasy) funkcji CSS, dyskretnego JavaScriptu, a nawet skalowalnej grafiki wektorowej. Progresywne ulepszanie w 2020 roku i później polega na korzystaniu z nowoczesnych funkcji przeglądarki.

Elastyczne projektowanie witryn z myślą o przyszłości dzięki progresywnemu ulepszaniu. Slajd tytułowy z oryginalnej prezentacji Fincka i Champeona.

Nowoczesny JavaScript

Jeśli chodzi o JavaScript, sytuacja z obsługą przeglądarek w przypadku najnowszych podstawowych funkcji ES 2015 jest bardzo dobra. Nowy standard obejmuje obietnice, moduły, klasy, literały szablonu, funkcje strzałkowe, letconst, parametry domyślne, generatory, przypisanie destrukcyjne, rest i spread, Map/Set, WeakMap/WeakSet i wiele innych. Wszystkie są obsługiwane.

Tabela obsługi CanIUse dla funkcji ES6 pokazująca obsługę we wszystkich głównych przeglądarkach.
Tabela obsługi ECMAScript 2015 (ES6) w przeglądarkach. (Źródło)

Funkcje asynchroniczne, funkcja ES 2017 i jedna z moich ulubionych, może być używana we wszystkich głównych przeglądarkach. Słowa kluczowe asyncawait umożliwiają zapisywanie asynchronicznego zachowania opartego na obietnicach w bardziej przejrzysty sposób, dzięki czemu nie trzeba jawnie konfigurować łańcuchów obietnic.

Tabela obsługi funkcji asynchronicznych w CanIUse pokazująca obsługę we wszystkich głównych przeglądarkach.
Tabela obsługi funkcji asynchronicznych w przeglądarkach. (Źródło)

Nawet najnowsze dodatki do języka ES2020, takie jak opcjonalne łańcuchowaniełączenie z użyciem operatora ??, są obsługiwane bardzo szybko. Jeśli chodzi o podstawowe funkcje JavaScriptu, trudno o coś lepszego.

Na przykład:

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
Kultowy obraz tła z zieloną trawą z systemu Windows XP.
W przypadku podstawowych funkcji JavaScriptu wszystko jest w porządku. (Zrzut ekranu produktu Microsoft, użyty za zgodą).

Przykładowa aplikacja: Fugu Greetings

W tym dokumencie pracuję z progresywną aplikacją internetową o nazwie Fugu Greetings (GitHub). Nazwa tej aplikacji to ukłon w stronę projektu Fugu 🐡, którego celem jest zapewnienie internetowi 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 z życzeniami i wysyłanie ich do bliskich. Jest to przykład podstawowych pojęć związanych z PWA. Jest niezawodna i w pełni dostępna offline, więc możesz z niej korzystać nawet wtedy, gdy nie masz dostępu do sieci. Można ją też zainstalować na ekranie głównym urządzenia i bezproblemowo zintegrować z systemem operacyjnym jako samodzielną aplikację.

Progresywna aplikacja internetowa Fugu Greetings z rysunkiem przypominającym logo społeczności PWA.
Przykładowa aplikacja Fugu Greetings.

Stopniowe ulepszanie

Gdy to już mamy za sobą, możemy porozmawiać o stopniowym ulepszaniu. Słownik MDN Web Docs definiuje to pojęcie w ten sposób:

Stopniowe ulepszanie to filozofia projektowania, która zapewnia podstawową zawartość i funkcjonalność jak największej liczbie użytkowników, a najlepsze możliwe wrażenia 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 obsługują nowsze funkcje, a elementy polyfill są często używane do dodawania brakujących funkcji za pomocą JavaScriptu.

[…]

Progresywne ulepszanie to przydatna technika, która pozwala programistom stron internetowych skupić się na tworzeniu jak najlepszych witryn, a jednocześnie sprawić, by działały one na wielu nieznanych agentach użytkownika. Stopniowe wycofywanie jest powiązane z progresywnym ulepszaniem, ale nie jest tym samym i często jest postrzegane jako działanie w przeciwnym kierunku. W rzeczywistości obie metody są prawidłowe i często mogą się uzupełniać.

Współpracownicy MDN

Tworzenie każdej kartki od zera może być bardzo uciążliwe. Dlaczego więc nie wprowadzić funkcji, która umożliwia użytkownikom importowanie obrazu i rozpoczynanie pracy od tego momentu? W tradycyjnym podejściu użyłbyś elementu <input type=file>. Najpierw tworzysz element, ustawiasz jego atrybut type na 'file' i dodajesz typy MIME do właściwości accept, a następnie programowo „klikasz” go i nasłuchujesz zmian. Gdy wybierzesz obraz, zostanie on zaimportowany bezpośrednio na obszar roboczy.

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 jest dostępna funkcja importowania, powinna być też funkcja eksportowania, aby użytkownicy mogli zapisywać kartki z życzeniami lokalnie. Tradycyjny sposób zapisywania plików polega na utworzeniu linku zakotwiczenia z atrybutem download i adresem URL bloba jako href. Możesz też programowo „kliknąć” ten element, aby rozpocząć pobieranie, i nie zapomnieć o odwołaniu adresu URL obiektu blob, aby zapobiec wyciekom pamięci.

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 zaraz. W myślach nie „pobierasz” kartki z życzeniami, tylko ją „zapisujesz”. Zamiast wyświetlać okno dialogowe „Zapisz”, które pozwala wybrać miejsce zapisania pliku, przeglądarka bezpośrednio pobrała kartkę z życzeniami bez interakcji ze strony użytkownika i umieściła ją w folderze Pobrane. To nie jest najlepsze rozwiązanie.

A gdyby istniał lepszy sposób? A gdyby można było po prostu otworzyć plik lokalny, edytować go, a następnie zapisać zmiany w nowym pliku lub w oryginalnym pliku, który został otwarty na początku? Okazuje się, że tak. Interfejs File System Access API umożliwia otwieranie i tworzenie plików oraz katalogów, a także ich modyfikowanie i zapisywanie .

Jak wykryć funkcję interfejsu API? 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 ta metoda jest dostępna.

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 przejdę do szczegółów interfejsu File System Access API, chcę szybko omówić wzorzec stopniowego ulepszania. W przeglądarkach, które nie obsługują interfejsu File System Access API, wczytuję starsze skrypty.

Inspektor sieci w Safari pokazujący wczytywanie starszych plików.
Narzędzia dla programistów w Firefoxie pokazujące wczytywanie starszych plików.

W Chrome, przeglądarce obsługującej ten interfejs API, wczytywane są tylko nowe skrypty. Jest to możliwe dzięki dynamicznemu import(), który jest obsługiwany przez wszystkie nowoczesne przeglądarki. Jak już wspomniałem, trawa jest teraz dość zielona.

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

File System Access API

Teraz, gdy już to wyjaśniłem, czas przyjrzeć się rzeczywistej implementacji opartej na interfejsie File System Access API. Aby zaimportować obraz, wywołuję funkcję window.chooseFileSystemEntries() i przekazuję jej właściwość accepts, w której określam, że chcę pliki obrazów. Obsługiwane są zarówno rozszerzenia plików, jak i typy MIME. W rezultacie 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 wygląda podobnie, ale tym razem muszę przekazać parametr typu 'save-file' do metody chooseFileSystemEntries(). Wyświetli się okno dialogowe zapisywania pliku. Gdy plik jest otwarty, nie jest to konieczne, ponieważ 'open-file' jest domyślnym ustawieniem. Parametr accepts ustawiam podobnie jak wcześniej, ale tym razem ograniczam go tylko do obrazów PNG. Ponownie otrzymuję uchwyt pliku, ale zamiast pobierać plik, tym razem tworzę strumień do zapisu, wywołując createWritable(). Następnie zapisuję w pliku obiekt blob, czyli obraz kartki z życzeniami. Na koniec zamykam strumień zapisu.

Zawsze może wystąpić błąd: dysk może być pełny, może wystąpić błąd zapisu lub odczytu albo użytkownik może po prostu zamknąć okno pliku. Dlatego zawsze umieszczam wywołania w instrukcji 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 progresywnemu ulepszaniu za pomocą interfejsu File System Access API mogę otwierać pliki tak jak wcześniej. Zaimportowany plik zostanie narysowany bezpośrednio na obszarze roboczym. Mogę wprowadzić zmiany i zapisać je w oknie dialogowym zapisu, w którym mogę wybrać nazwę i lokalizację pliku. Plik jest teraz gotowy do przechowywania przez wieczność.

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

Interfejsy Web Share i Web Share Target API

attempt-right

Oprócz przechowywania jej na zawsze mogę też chcieć ją udostępnić. Umożliwiają to Web Share API i Web Share Target API. Systemy operacyjne na urządzenia mobilne, a ostatnio także na komputery, mają wbudowane mechanizmy udostępniania.

Na przykład arkusz udostępniania Safari na macOS na komputerze jest wywoływany, gdy użytkownik kliknie Udostępnij artykuł na moim blogu. Możesz udostępnić link do artykułu znajomemu za pomocą aplikacji Wiadomości na macOS.

Aby to zrobić, wywołuję funkcję navigator.share() i przekazuję do niej opcjonalne parametry title, texturl w obiekcie. A co, jeśli chcę załączyć obraz? Poziom 1 interfejsu Web Share API nie obsługuje jeszcze tej funkcji. Dobra wiadomość jest taka, że w Web Share Level 2 dodano funkcje 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 Fugu Greeting card. Najpierw muszę przygotować obiekt data z tablicą files składającą się z 1 obiektu blob, a następnie titletext. Następnie, zgodnie ze sprawdzoną metodą, używam nowej metody navigator.canShare(), która robi to, co sugeruje jej nazwa: informuje mnie, czy obiekt data, który próbuję udostępnić, może być technicznie udostępniony przez przeglądarkę. Jeśli navigator.canShare() powie mi, że dane mogą być udostępniane, mogę zadzwonić do navigator.share() tak jak wcześniej. 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);
  }
};

Jak wcześniej, stosuję stopniowe udoskonalenia. Jeśli w obiekcie navigator występują zarówno atrybut 'share', jak i 'canShare', przechodzę dalej i wczytuję atrybut share.mjs za pomocą dynamicznego atrybutu import(). W przeglądarkach takich jak Safari na urządzeniach mobilnych, które spełniają tylko jeden z tych 2 warunków, nie wczytuję tej funkcji.

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

W Fugu Greetings, jeśli kliknę przycisk Udostępnij w obsługiwanej przeglądarce, np. Chrome na Androidzie, otworzy się wbudowany arkusz udostępniania. Mogę na przykład wybrać Gmaila, a wtedy pojawi się widżet do pisania e-maili z załączonym obrazem.

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

Contact Picker API

Teraz chcę porozmawiać o kontaktach, czyli o książce adresowej urządzenia lub aplikacji do zarządzania kontaktami. Podczas pisania kartki z życzeniami nie zawsze łatwo jest poprawnie napisać imię i nazwisko. Na przykład mój znajomy Siergiej woli, żeby jego imię było zapisywane cyrylicą. Używam niemieckiej klawiatury QWERTZ i nie wiem, jak wpisać jego imię. Jest to problem, który może rozwiązać interfejs API selektora kontaktów. Ponieważ mam znajomego zapisanego w aplikacji Kontakty na telefonie, mogę uzyskać dostęp do kontaktów z poziomu internetu za pomocą interfejsu Contacts Picker API.

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

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);
  }
};

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

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

W aplikacji Fugu Greeting, gdy kliknę przycisk Kontakty i wybiorę dwóch najlepszych przyjaciół: Сергея Михайловича Брина劳伦斯·爱德华·"拉里"·佩奇, zobaczysz, ż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ą następnie rysowane na mojej kartce z życzeniami.

Selektor kontaktów z nazwami dwóch kontaktów z książki adresowej.
Wybieranie dwóch nazw za pomocą selektora kontaktów z książki adresowej.
Imiona i nazwiska 2 wcześniej wybranych osób z kontaktów narysowane na kartce z życzeniami.
Następnie oba imiona są rysowane na kartce z życzeniami.

Asynchronous Clipboard API

Kolejna funkcja to kopiowanie i wklejanie. Jedną z naszych ulubionych czynności jako programistów jest kopiowanie i wklejanie. Jako autor kartek okolicznościowych czasami chcę zrobić to samo. Mogę chcieć wkleić obraz do kartki z życzeniami, nad którą pracuję, lub skopiować kartkę, aby móc kontynuować jej edycję w innym miejscu. Async Clipboard API obsługuje zarówno tekst, jak i obrazy. Opowiem Ci, jak dodałem obsługę kopiowania i wklejania do aplikacji Fugu Greetings.

Aby skopiować coś do schowka systemowego, muszę zapisać w nim dane. Metoda navigator.clipboard.write() przyjmuje jako parametr tablicę elementów schowka. Każdy element schowka to w zasadzie obiekt z obiektem blob jako wartością i jego typem jako kluczem.

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

Aby wkleić elementy, muszę przejść w pętli po elementach schowka, które uzyskuję, wywołując funkcję navigator.clipboard.read(). Dzieje się tak dlatego, że w schowku może być wiele elementów w różnych formatach. Każdy element schowka ma pole types, które informuje o typach MIME dostępnych zasobów. Wywołuję metodę getType() elementu schowka, przekazując uzyskany wcześniej 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 muszę chyba dodawać, że 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 macOS i kopiuję go do schowka. Gdy kliknę Wklej, aplikacja Fugu Greetings zapyta mnie, czy chcę zezwolić jej na dostęp do tekstu i obrazów w schowku.

Aplikacja Fugu Greetings wyświetlająca prośbę o przyznanie uprawnień do schowka.
Prośba o uprawnienia dostępu do schowka.

Gdy zaakceptujesz uprawnienia, obraz zostanie wklejony do aplikacji. Działa to też w drugą stronę. Skopiuj kartkę z życzeniami do schowka. Gdy otworzę Podgląd i kliknę Plik, a potem Nowy z schowka, kartka z życzeniami zostanie wklejona do nowego obrazu bez tytułu.

Aplikacja Podgląd w systemie macOS z obrazem bez nazwy, który został właśnie wklejony.
Obraz wklejony do aplikacji Podgląd w systemie macOS.

Badging API

Kolejnym przydatnym interfejsem API jest Badging API. Jako instalowalna aplikacja PWA Fugu Greetings ma oczywiście ikonę, którą użytkownicy mogą umieścić w docku aplikacji lub na ekranie głównym. Ciekawym sposobem na zaprezentowanie interfejsu API jest użycie go w aplikacji Fugu Greetings jako licznika pociągnięć piórem. Dodałem detektor zdarzeń, który zwiększa licznik pociągnięć piórem za każdym razem, gdy wystąpi zdarzenie pointerdown, a następnie ustawia zaktualizowaną plakietkę ikony. Gdy obszar roboczy zostanie wyczyszczony, licznik zostanie zresetowany, a odznaka usunięta.

let strokes = 0;

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

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

Ta funkcja jest ulepszeniem progresywnym, więc logika ładowania jest taka sama jak zwykle.

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

W tym przykładzie narysowałem liczby od 1 do 7, używając jednego pociągnięcia pióra na każdą liczbę. Licznik powiadomień na ikonie wynosi teraz 7.

Liczby od 1 do 7 narysowane na kartce z życzeniami jednym pociągnięciem pióra.
Narysowanie liczb od 1 do 7 za pomocą 7 pociągnięć pióra.
Ikona plakietki w aplikacji Fugu Greetings z liczbą 7.
Licznik pociągnięć piórem w postaci plakietki z ikoną aplikacji.

Interfejs Periodic Background Sync API

Chcesz zaczynać każdy dzień od czegoś nowego? Ciekawą funkcją aplikacji Fugu Greetings jest to, że każdego ranka może ona inspirować Cię nowym obrazem tła, od którego możesz zacząć tworzenie kartki z życzeniami. W tym celu aplikacja korzysta z interfejsu Periodic Background Sync API.

Pierwszym krokiem jest zarejestrowanie zdarzenia synchronizacji okresowej w rejestracji service workera. Nasłuchuje tagu synchronizacji o nazwie 'image-of-the-day' i ma minimalny interwał wynoszący 1 dzień, dzięki czemu użytkownik może otrzymywać nowe tło co 24 godziny.

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);
  }
};

Drugi krok to nasłuchiwanie zdarzenia periodicsync w usłudze Service Worker. 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 obszary robocze 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 prawdziwe ulepszenie progresywne, więc kod jest wczytywany tylko wtedy, gdy interfejs API jest obsługiwany przez przeglądarkę. Dotyczy to zarówno kodu klienta, jak i kodu service worker. W przeglądarkach, które nie obsługują tej funkcji, nie jest wczytywana żadna z nich. 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 kartki z życzeniami, która jest aktualizowana codziennie za pomocą interfejsu Periodic Background Sync API.

Naciśnięcie przycisku Tapeta powoduje wyświetlenie obrazu dnia.

Notification Triggers API

Czasami nawet przy dużej dawce inspiracji potrzebujesz impulsu, aby dokończyć rozpoczętą kartkę z życzeniami. Jest to funkcja włączana przez interfejs Notification Triggers API. Jako użytkownik mogę podać godzinę, o której chcę otrzymać przypomnienie o dokończeniu kartki z życzeniami. Gdy nadejdzie ten czas, otrzymam powiadomienie, że moja kartka z życzeniami czeka na mnie.

Po wyświetleniu prośby o podanie docelowego czasu aplikacja zaplanuje powiadomienie z symbolem showTrigger. Może to być TimestampTrigger z wcześniej wybraną datą docelową. Powiadomienie o przypomnieniu zostanie wywołane lokalnie, bez potrzeby korzystania z sieci lub 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 elementów, które pokazałem do tej pory, jest to ulepszenie progresywne, więc kod jest ładowany tylko warunkowo.

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

Gdy w Fugu Greetings zaznaczę pole wyboru Przypomnienie, pojawi się prośba o podanie daty, w której mam otrzymać przypomnienie o ukończeniu kartki z życzeniami.

Aplikacja Fugu Greetings z monitem z pytaniem, kiedy użytkownik chce otrzymać przypomnienie o ukończeniu kartki z życzeniami.
Planowanie powiadomienia lokalnego, które przypomni o dokończeniu kartki z życzeniami.

Gdy zaplanowane powiadomienie zostanie wywołane w Fugu Greetings, jest wyświetlane tak jak każde inne powiadomienie, ale jak pisałem wcześniej, nie wymaga połączenia z siecią.

Wywołane powiadomienie pojawi się w Centrum powiadomień systemu macOS.

Wake Lock API

Chcę też uwzględnić Wake Lock API. Czasami wystarczy wpatrywać się w ekran tak długo, aż natchnienie cię pocałuje. Najgorsze, co może się wtedy zdarzyć, to wyłączenie ekranu. API Wake Lock może temu zapobiec.

Pierwszym krokiem jest uzyskanie blokady wybudzania za pomocą metody navigator.wakelock.request method(). Przekazuję ciąg znaków 'screen', aby uzyskać blokadę uśpienia ekranu. Następnie dodaję detektor zdarzeń, aby otrzymywać powiadomienia o zwolnieniu blokady wybudzania. Może się to zdarzyć na przykład wtedy, gdy zmieni się widoczność karty. Jeśli tak się stanie, po ponownym wyświetleniu karty mogę ponownie uzyskać blokadę wybudzania.

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, to ulepszenie progresywne, więc muszę je wczytywać tylko wtedy, gdy przeglądarka obsługuje interfejs API.

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

W Fugu Greetings znajduje się pole wyboru Insomnia, które po zaznaczeniu utrzymuje ekran włączony.

Jeśli pole wyboru bezsenności jest zaznaczone, ekran pozostaje włączony.
Pole wyboru Bezsenność zapobiega przejściu aplikacji w stan uśpienia.

Idle Detection API

Czasami nawet wielogodzinne wpatrywanie się w ekran nie przynosi żadnego efektu i nie masz najmniejszego pomysłu, co zrobić z kartką z życzeniami. Interfejs Idle Detection API umożliwia aplikacji wykrywanie czasu bezczynności użytkownika. Jeśli użytkownik jest zbyt długo nieaktywny, aplikacja wraca do stanu początkowego i czyści obszar roboczy. Ten interfejs API jest chroniony przez uprawnienie do powiadomień, ponieważ wiele przypadków użycia wykrywania bezczynności w wersji produkcyjnej jest związanych z powiadomieniami, np. wysyłanie powiadomienia tylko na urządzenie, z którego użytkownik aktywnie korzysta.

Po upewnieniu się, że uprawnienia do powiadomień zostały przyznane, tworzę instancję detektora bezczynności. Rejestruję detektor zdarzeń, który nasłuchuje zmian stanu bezczynności, w tym stanu 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, obszar roboczy zostanie wyczyszczony. Ustawiam próg bezczynności na 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, wczytuję ten kod tylko wtedy, gdy przeglądarka go obsługuje.

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

W aplikacji Fugu Greetings obszar rysowania jest czyszczony, gdy pole wyboru Ephemeral jest zaznaczone, a użytkownik jest nieaktywny zbyt długo.

Aplikacja Fugu Greetings z wyczyszczonym obszarem roboczym po zbyt długim okresie nieaktywności użytkownika.
Jeśli pole wyboru Tymczasowy jest zaznaczone, a użytkownik jest nieaktywny zbyt długo, obszar roboczy zostanie wyczyszczony.

Zakończenie

Uff, co za przejażdżka. W jednej aplikacji przykładowej jest tak wiele interfejsów API. Pamiętaj, że nigdy nie obciążam użytkownika kosztem pobierania funkcji, której jego przeglądarka nie obsługuje. Dzięki stopniowemu udoskonalaniu mam pewność, że wczytywany jest tylko odpowiedni kod. A ponieważ w przypadku protokołu HTTP/2 żądania są tanie, ten wzorzec powinien dobrze sprawdzać się w wielu aplikacjach, chociaż w przypadku bardzo dużych aplikacji warto rozważyć użycie narzędzia do łączenia plików.

Karta Sieć w Narzędziach deweloperskich w Chrome, na której widać tylko żądania plików z kodem obsługiwanym przez przeglądarkę.

Aplikacja może wyglądać nieco inaczej w każdej przeglądarce, ponieważ nie wszystkie platformy obsługują wszystkie funkcje, ale podstawowe funkcje są zawsze dostępne – są one stopniowo ulepszane w zależności od możliwości danej przeglądarki. Te możliwości mogą się zmieniać nawet w tej samej przeglądarce, w zależności od tego, czy aplikacja jest uruchomiona jako zainstalowana aplikacja, czy na karcie przeglądarki.

Fugu Greetings działająca w Chrome na Androidzie, która pokazuje wiele dostępnych funkcji.
Fugu Greetings uruchomiona w Safari na komputerze, która pokazuje mniej dostępnych funkcji.
Fugu Greetings w przeglądarce Chrome na komputerze z wieloma dostępnymi funkcjami.

Możesz utworzyć rozwidlenie Fugu na GitHubie.

Zespół Chromium intensywnie pracuje nad ulepszeniem zaawansowanych interfejsów Fugu API. Stosując podczas tworzenia aplikacji progresywne ulepszanie, dbam o to, aby każdy użytkownik miał dobre, solidne podstawowe wrażenia, ale osoby korzystające z przeglądarek obsługujących więcej interfejsów API platformy internetowej miały jeszcze lepsze wrażenia. Z niecierpliwością czekam na to, jak wykorzystasz w swoich aplikacjach progresywne ulepszanie.

Podziękowania

Jestem wdzięczny Christianowi LiebelowiHemanthowi HM, którzy przyczynili się do powstania Fugu Greetings. Ten dokument został sprawdzony przez Joego MedleyaKayce Basques. Jake Archibald pomógł mi zrozumieć sytuację z dynamicznym import() w kontekście service workera.