Stopniowo ulepszaj progresywną aplikację internetową

Stale ulepszamy nasze przeglądarki z myślą o nowoczesnych przeglądarkach

W marcu 2003 roku Nick Finck i Steve Champeon zaskoczyli świat projektowania stron internetowych koncepcją progresywnego udoskonalania – strategią projektowania witryn, w której przede wszystkim kładzie się nacisk na wczytywanie najpierw treści podstawowej strony internetowej, a następnie dodawanie do treści bardziej szczegółowych i technicznych warstw prezentacji oraz funkcji. W 2003 r. progresywne ulepszenia obejmowały używanie nowoczesnych funkcji CSS, niezakłóconego JavaScriptu, a nawet tylko skalowalnej grafiki wektorowej. Stopniowe ulepszanie funkcji w 2020 roku i kolejnych latach wiąże się ze stosowaniem nowoczesnych funkcji przeglądarek.

Projektowanie witryn inkluzywnych z myślą o przyszłości i stopniowym ulepszaniem usług. Slajd tytułowy z oryginalnej prezentacji Fincka i Champeona.
Slajd: tworzenie witryn promujących integrację społeczną dzięki progresywnym ulepszeniom. (Źródło)

Nowoczesny JavaScript

Jeśli chodzi o JavaScript, obsługa przeglądarki w zakresie najnowszych podstawowych funkcji JavaScript ES 2015 jest świetna. Nowy standard obejmuje obietnice, moduły, klasy, literały szablonów, funkcje strzałek, let i const, parametry domyślne, generatory, przypisanie destrukcyjne, spoczynkowe i rozpowszechnianie, Map/Set, WeakMap/WeakSet i wiele innych. Obsługiwane są wszystkie elementy.

Tabela pomocy technicznej CanIUse dla funkcji ES6 przedstawiających obsługę wszystkich najpopularniejszych przeglądarek.
Tabela obsługi przeglądarek ECMAScript 2015 (ES6). (Źródło)

Funkcji asynchronicznych, czyli jednej z moich ulubionych funkcji w ES 2017 r. i jednej z moich ulubionych, można używać we wszystkich popularnych przeglądarkach. Słowa kluczowe async i await umożliwiają asynchroniczne, oparte na obietnicach zachowanie w czystszym stylu, co pozwala uniknąć konieczności jawnego konfigurowania łańcuchów obietnicy.

Tabela pomocy dotyczącej funkcji asynchronicznych CanIUse we wszystkich popularnych przeglądarkach.
Tabela obsługi funkcji asynchronicznych przez przeglądarkę. (Źródło)

Nawet niedawno dodane w wersji ES 2020 rozszerzenia językowe, takie jak opcjonalne łańcuch i nullish coalescing, są teraz obsługiwane bardzo szybko. Przykładowy kod znajdziesz poniżej. Jeśli chodzi o podstawowe funkcje JavaScriptu, trawa nie może być znacznie bardziej zielona niż obecnie.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
Słynny obraz tła zieloną trawą dla systemu Windows XP.
Jeśli chodzi o podstawowe funkcje JavaScriptu, trawa jest zielona. (Zrzut ekranu z usługą Microsoft używany z uprawnieniami).

Przykładowa aplikacja: Fugu Greetings

W tym artykule pracuję z prostą aplikacją PWA o nazwie Fugu Greetings (GitHub). Nazwa tej aplikacji to tylko wskazówka dla Project Fugu 🐡. Chcemy, aby internet miał wszystkie 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 pozwala tworzyć wirtualne kartki z życzeniami i wysyłać je do bliskich. Jest to przykład podstawowych koncepcji PWA. Jest niezawodne i w pełni umożliwia pracę w trybie offline, dzięki czemu możesz z niej korzystać nawet wtedy, gdy nie masz połączenia z siecią. Jest też możliwy do zainstalowania na ekranie głównym urządzenia i płynnie integruje się z systemem operacyjnym jako samodzielna aplikacja.

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

Stopniowe ulepszanie

Przy okazji nadszedł czas, by porozmawiać o progresywnym udoskonalaniu. Słowniczek dokumentów internetowych MDN definiuje to pojęcie w następujący sposób:

Stopniowe wprowadzanie udoskonaleń to filozofia projektowania, która pozwala zapewnić jak największej liczbie użytkowników dostęp do kluczowych treści i funkcji, a jednocześnie zapewnia najwyższą jakość podczas korzystania tylko z najnowocześniejszych przeglądarek, które obsługują cały wymagany kod.

Wykrywanie funkcji służy zwykle do określania, czy przeglądarki obsługują bardziej nowoczesne funkcje. Natomiast elementy polyfill często stosuje się, aby dodawać brakujące funkcje w JavaScript.

[…]

Stopniowe ulepszanie to przydatna technika, która pozwala programistom skupić się na tworzeniu jak najlepszych witryn, jednocześnie umożliwiając im działanie na wielu nieznanych klientach użytkownika. Łagodne pogorszenie jest powiązane, ale nie jest tym samym, i często wygląda na to, że idzie w przeciwnym kierunku do stopniowego ulepszania. W rzeczywistości oba podejścia są słuszne i często się uzupełniają.

Współtwórcy MDN

Rozpoczynanie każdej kartki od zera może być naprawdę kłopotliwe. Warto więc mieć funkcję, która pozwala użytkownikom importować obrazy, a potem od niej zacząć. W przypadku tradycyjnego podejścia do tego celu należałoby użyć elementu <input type=file>. Najpierw trzeba utworzyć element, ustawić jego type na 'file' i dodać typy MIME do właściwości accept, a potem automatycznie go „kliknąć” i nasłuchać zmian. Po wybraniu obrazu zostanie on zaimportowany bezpośrednio do obszaru roboczego.

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 import, prawdopodobnie powinna być dostępna funkcja import, która pozwoli użytkownikom zapisywać kartki z życzeniami lokalnie. Tradycyjnym sposobem zapisywania plików jest utworzenie linku kotwiczącego z atrybutem download i adresu URL obiektu blob jako href. Możesz go też automatycznie „kliknąć”, aby rozpocząć pobieranie, i aby zapobiec wyciekom pamięci, warto unieważnić adres URL obiektu bloba.

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

Moment. Wiedząc, że nie masz „pobranych” kartek z życzeniami, zostały one zapisane. Zamiast wyświetlać okno dialogowe „zapisz”, w którym można wybrać lokalizację pliku, przeglądarka bezpośrednio pobrała pocztówkę bez działań ze strony użytkownika i umieściła ją w folderze Pobrane pliki. Nie jest to dobre rozwiązanie.

A gdyby istniał lepszy sposób? A gdyby można było po prostu otworzyć plik lokalny, edytować go, a potem zapisać zmiany w nowym pliku lub z powrotem do pierwotnie otwartego pliku? Okazało się, że jest tak. Interfejs File System Access API pozwala otwierać i tworzyć pliki oraz katalogi, a także je modyfikować i zapisywać .

Jak więc wykryć interfejs API? Interfejs File System Access API ujawnia 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. Poniżej pokazano, jak to zrobić.

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'),
    ]);
  }
};

Ale zanim przejdę do szczegółów interfejsu File System Access API, odpowiem na krótko o wzorcu progresywnego ulepszania. W przeglądarkach, które obecnie nie obsługują interfejsu File System Access API, wczytuję starsze skrypty. Poniżej możesz zobaczyć karty sieciowe przeglądarek Firefox i Safari.

Inspektor sieci Safari pokazujący wczytywanie starszych plików.
Karta sieci w przeglądarce Safari.
Narzędzia dla programistów Firefox przedstawiające ładowanie starszych plików.
Karta sieci Narzędzi dla programistów w przeglądarce Firefox.

Jednak w Chrome, która obsługuje ten interfejs API, ładowane są tylko nowe skrypty. Jest to możliwe dzięki dynamicznemu import(), którego obsługuje wszystkie nowoczesne przeglądarki. Jak wspomniałem, trawa jest teraz całkiem zielona.

Narzędzia deweloperskie w Chrome przedstawiające nowoczesne wczytywane pliki.
Karta sieci w Narzędziach deweloperskich w Chrome.

Interfejs File System Access API

Omówiliśmy już ten problem. Teraz przyjrzymy się rzeczywistej implementacji opartej na interfejsie File System Access API. Aby zaimportować obraz, wywołuję window.chooseFileSystemEntries() i przekazuję mu właściwość accepts, gdzie chcę wyszukać pliki graficzne. Obsługiwane są zarówno rozszerzenia plików, jak i typy MIME. Spowoduje to utworzenie nicka pliku, z którego mogę uzyskać faktyczny 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 prawie tak samo, ale tym razem muszę przekazać do metody chooseFileSystemEntries() parametr typu 'save-file'. Pojawi się okno dialogowe zapisywania plików. Nie było to konieczne, gdy plik został otwarty, ponieważ domyślną wartością jest 'open-file'. Ustawiam parametr accepts podobnie jak wcześniej, ale tym razem są to tylko obrazy PNG. Znowu otrzymuję uchwyt pliku, ale zamiast pobrać plik, tym razem tworzę strumień z możliwością zapisu, wywołując createWritable(). Następnie zapisuję w pliku obiekt blob, czyli obraz pocztówki. Na koniec zamykam strumień z możliwością zapisu.

Wszystko zawsze może zawieść – na dysku może brakować miejsca, może wystąpić błąd zapisu lub odczytu albo po prostu użytkownik anuluje okno dialogowe 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 stopniowym ulepszeniom interfejsu File System Access API mogę otwierać plik tak jak wcześniej. Zaimportowany plik zostanie rysowany bezpośrednio w obszarze roboczym. Mogę wprowadzić zmiany i zapisać je w oknie dialogowym zapisywania, w którym mogę wybrać nazwę i lokalizację zapisu. Teraz plik jest gotowy – możesz go zachować na zawsze.

Aplikacja Fugu Greetings z oknem otwierającym plik.
Okno otwierania pliku.
Aplikacja Fugu Greetings zawiera teraz zaimportowany obraz.
Zaimportowany obraz.
Aplikacja Fugu Greetings ze zmodyfikowanym obrazem.
Zapisywanie zmodyfikowanego obrazu w nowym pliku.

Interfejsy Web Share i Web Share Target API

Poza przechowywaniem na zawsze może chciałbym udostępnić swoją kartkę z życzeniami. Do tego pozwalają mi interfejsy Web Share API i Web Share Target API. Urządzenia mobilne oraz systemy operacyjne na komputery mają wbudowane mechanizmy udostępniania. Poniżej znajduje się na przykład arkusz udostępniania Safari na komputerze działający w systemie macOS, który został wywołany na podstawie artykułu na moim blogu. Po kliknięciu przycisku Udostępnij artykuł możesz udostępnić link do artykułu znajomemu, na przykład w aplikacji Wiadomości w systemie macOS.

Arkusz udostępniania przeglądarki Safari na komputerze w systemie macOS uruchamiany po kliknięciu przycisku Udostępnij w artykule
Interfejs Web Share API w przeglądarce Safari na komputerze w systemie macOS

Odpowiedni kod jest bardzo prosty. Wywołuję navigator.share() i przekazuję opcjonalnie title, text i url w obiekcie. A co, jeśli chcę załączyć obraz? Interfejs Web Share API na poziomie 1 nie obsługuje jeszcze tej funkcji. Dobra wiadomość jest taka, że w ramach udostępniania internetowego poziomu 2 pojawiły się nowe 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 aplikacji do kart z życzeniami Fugu. Najpierw muszę przygotować obiekt data z tablicą files składającą się z 1 obiektu blob, a potem title i text. Dalej zgodnie ze sprawdzoną metodą używam nowej metody navigator.canShare(), która działa zgodnie z jej nazwą: informuje mnie, czy obiekt data, który próbuję udostępnić, może być technicznie udostępniany przez przeglądarkę. Jeśli navigator.canShare() poinformuje mnie, że dane można udostępnić, mogę tak jak poprzednio wywołać usługę navigator.share(). Ponieważ wszystko może zawieść, znów używam blokady 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);
  }
};

Tak jak poprzednio, używam stopniowego ulepszania. Jeśli w obiekcie navigator istnieją zarówno 'share', jak i 'canShare', tylko wtedy przechodzę do przodu i wczytuję share.mjs za pomocą dynamicznego import(). W przeglądarkach takich jak Safari na komórki, które spełniają tylko jeden z tych warunków, nie wczytuje się funkcja.

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

Gdy w aplikacji Fugu Greetings kliknę przycisk Udostępnij w obsługiwanej przeglądarce, takiej jak Chrome na Androidzie, otworzy się wbudowany arkusz udostępniania. Mogę na przykład wybrać Gmaila, a wyświetli się widżet tworzenia wiadomości z załączonym obrazem.

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

Interfejs API selektora kontaktów

Teraz opowiem o kontaktach, czyli w książce adresowej lub aplikacji do zarządzania kontaktami na urządzeniu. Tworząc pocztówkę, nie zawsze łatwo jest wpisać czyjeś imię i nazwisko. Mój przyjaciel chce na przykład pisać jego imię cyrylicą. Używam niemieckiej klawiatury QWERTZ i nie wiem, jak wpisać imię. Ten problem można rozwiązać za pomocą interfejsu Contact Picker API. Mam już znajomego w aplikacji do obsługi kontaktów na telefonie, więc mogę przeglądać kontakty w internecie za pomocą interfejsu API selektora kontaktów.

Najpierw muszę określić listę właściwości, do których chcę uzyskać dostęp. W tym przypadku chcę widzieć tylko imiona i nazwiska, a w innych – numery telefonów, adresy e-mail, ikony awatara lub adresy pocztowe. Następnie konfiguruję obiekt options i ustawiam multiple na true, aby móc wybrać więcej niż 1 wpis. Na koniec mogę wywołać funkcję navigator.contacts.select(), która zwraca żądane 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);
  }
};

Teraz pewnie znasz już wzorzec: wczytuję plik tylko wtedy, gdy interfejs API jest faktycznie obsługiwany.

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

W Fugu Greeting, gdy kliknę przycisk Kontakty i wybiorę 2 najlepsze znajome osoby, Šергей мидайлович Брин i 劳伦斯·爱德·"拉里"·佩奇 pojawiają się tylko ich adresy e-mail i ich adresy e-mail oraz ich adresy e-mail są ograniczone. Ich imiona są następnie rysowane na mojej kartce z życzeniami.

Selektor kontaktów pokazujący nazwy 2 kontaktów w książce adresowej.
Wybranie 2 nazwisk za pomocą selektora kontaktów w książce adresowej.
Nazwy dwóch wcześniej wybranych kontaktów narysowanych na kartce z życzeniami.
Dwie imiona i nazwiska zostaną narysowane na kartce z życzeniami.

Interfejs Asynchronous Clipboard API

Następny krok to kopiowanie i wklejanie. Jednym z naszych ulubionych działań jako programistów jest kopiowanie i wklejanie. Jestem autorem kartki z życzeniami i chciałabym czasami zrobić to samo. Mogę wkleić obraz do kartki z życzeniami, nad którą pracuję, lub skopiować moją kartkę z życzeniami, żeby móc ją edytować z innego miejsca. 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 jako parametr tablicę elementów schowka. Zasadniczo każdy element schowka to obiekt z wartością obiektu blob, a kluczem jest typ obiektu blob.

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

Aby wkleić treść, muszę włączyć pętlę nad elementami schowka uzyskanymi przez wywołanie funkcji navigator.clipboard.read(). Powodem jest to, że wiele elementów schowka może znajdować się w schowku w różnych reprezentacjach. Każdy element schowka ma pole types z informacjami o typach MIME dostępnych zasobów. Wywołuję metodę getType() elementu schowka, która przekazuje 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);
  }
};

I obecnie można to powiedzieć. 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 macOS i skopiuję go do schowka. Gdy kliknę Wklej, aplikacja Fugu Greetings pyta, czy zezwolić jej na odczytywanie tekstu i obrazów ze schowka.

Aplikacja Fugu Greetings wyświetla prośbę o przyznanie uprawnień dostępu do schowka.
Prośba o przyznanie uprawnień do schowka.

Na koniec, po zaakceptowaniu uprawnienia, obraz jest wklejany do aplikacji. Obrót działa też w drugą stronę. Kopiuję pocztówkę do schowka. Gdy otwieram Podgląd i klikam kolejno Plik i Nowy ze schowka, kartka z życzeniami jest wklejana do nowego obrazu bez tytułu.

Aplikacja Podgląd w systemie macOS z wklejonym obrazem bez nazwy.
Obraz wklejony w aplikacji podglądu macOS.

Interfejs API Badging

Innym przydatnym interfejsem API jest Badging API. Aplikacja PWA Fugu Greetings ma oczywiście ikonę aplikacji, którą użytkownicy mogą umieścić w docku z aplikacjami lub na ekranie głównym. Zabawnym i łatwym sposobem zademonstrowania interfejsu API jest (ab) użycie go w Fugu Greetings jako licznika pociągnięć piórem. Dodałem detektor zdarzeń, który za każdym razem, gdy wystąpi zdarzenie pointerdown, zwiększa licznik pociągnięć piórem, a następnie ustawia zaktualizowaną plakietkę ikony. Po każdym wyczyszczeniu obszaru roboczego licznik się resetuje, a plakietka jest usuwana.

let strokes = 0;

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

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

Ta funkcja jest udoskonaleniem progresywnym, więc sposób wczytywania działa bez zmian.

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

W tym przykładzie narysowaliśmy liczby od jednego do siedmiu, używając 1 kreski na liczbę. Licznik znaczków na ikonie wynosi teraz 7.

Liczby od jednego do siedmiu narysowane na kartce z życzeniami – każda z nich wymaga tylko jednego przekreślenia piórem.
Rysowanie liczb od 1 do 7 7 kreskami pióra.
Ikona plakietki w aplikacji Fugu Greetings z liczbą 7.
Licznik pociągnięć piórem w formie plakietki ikony aplikacji.

Interfejs Periodic Background Sync API

Chcesz zacząć każdy dzień od czegoś nowego? Aplikacja Fugu Greetings oferuje inspirację – na początek może Cię zainspirować nowym obrazem tła. Aplikacja używa do tego interfejsu Periodic Background Sync API.

Pierwszym krokiem jest register okresowego zdarzenia synchronizacji w rejestracji skryptu service worker. Nasłuchuje tagu synchronizacji o nazwie 'image-of-the-day' i ma minimalny odstęp czasu, który wynosi 1 dzień, więc użytkownik może otrzymywać nowy obraz tła 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);
  }
};

Drugim krokiem jest nasłuchanie zdarzenia periodicsync w skrypcie service worker. Jeśli tag zdarzenia to 'image-of-the-day', czyli ten, który został wcześniej zarejestrowany, obraz dnia jest pobierany za pomocą funkcji getImageOfTheDay(), a wynik jest rozpowszechniany do wszystkich klientów, dzięki czemu mogą oni aktualizować swoje obszary robocze i pamięci podręczne.

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, 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 skryptu service worker. W przeglądarkach, które nie są obsługiwane, nie wczytuje się żadna z nich. Zwróć uwagę, jak w skrypcie service worker zamiast dynamicznego elementu import() (który jeszcze nie jest obsługiwany w kontekście skryptu service worker), używam klasycznej wersji 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');
}

Naciśnięcie przycisku Tapeta w aplikacji Fugu Greetings powoduje wyświetlenie obrazu dnia, który jest aktualizowany codziennie przez interfejs Periodic Background Sync API.

Aplikacja Fugu Greetings z nowym obrazem kartki z życzeniami dnia.
Naciśnięcie przycisku Tapeta spowoduje wyświetlenie obrazu dnia.

Interfejs Notification Triggers API

Czasami, nawet gdy masz dużo inspiracji, potrzebujesz zachęty do dokończenia rozpoczętej kartki z życzeniami. Ta funkcja jest dostępna w ramach interfejsu Notification Triggers API. Jako użytkownik mogę podać godzinę, o której mam być poproszony o dokończenie tworzenia kartki z życzeniami. W tym czasie otrzymam powiadomienie, że moja kartka z życzeniami czeka.

Po wyświetleniu prośby o czas docelowy aplikacja planuje wysłanie powiadomienia za pomocą showTrigger. Może to być element TimestampTrigger z wybraną wcześniej datą docelową. Przypomnienie będzie wyzwalane lokalnie i nie będzie wymagane po stronie 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 wszystkie pozostałe doświadczenia, jest to progresywne ulepszenie, więc kod jest wczytywany tylko warunkowo.

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

Gdy zaznaczam pole Przypomnienie w Fugu Greetings, wyświetla się pytanie, kiedy chcę otrzymać przypomnienie o dokończeniu kartki z życzeniami.

Aplikacja Fugu Greetings wyświetla użytkownikowi prośbę, kiedy chce otrzymać przypomnienie o dokończeniu kartki.
Ustawianie lokalnego powiadomienia z przypomnieniem o dokończeniu kartki z życzeniami.

Zaplanowane powiadomienie wyświetla się tak samo jak wszystkie inne powiadomienia, ale nie wymaga połączenia sieciowego.

Centrum powiadomień macOS z wyświetlonym powiadomieniem z aplikacji Fugu Greetings.
Wywołane powiadomienie pojawi się w Centrum powiadomień macOS.

Wake Lock API

Chcę też dołączyć Wake Lock API. Czasem wystarczy patrzeć w ekran wystarczająco długo, aby pocałować Cię inspiracja. Najgorsze, co może się wtedy stać, to wyłączenie ekranu. Interfejs Wake Lock API może temu zapobiec.

Pierwszym krokiem jest uzyskanie blokady uśpienia na urządzeniu navigator.wakelock.request method(). Przekazuję do niego ciąg 'screen', aby otrzymać blokadę wybudzania ekranu. Dodaję detektor zdarzeń, który będzie informował o wyłączeniu blokady uśpienia. Może się to zdarzyć, jeśli na przykład zmieni się widoczność karty. Jeśli tak się stanie, spróbuję włączyć blokadę uśpienia, gdy karta znowu stanie się widoczna.

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 udoskonalenie progresywne, więc wystarczy je wczytać tylko wtedy, gdy przeglądarka obsługuje interfejs API.

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

W sekcji Pozdrowienia z Fugu znajduje się pole wyboru Bezsenność, które po zaznaczeniu tej opcji nie wyłącza ekranu.

Pole wyboru dotyczące bezsenności, jeśli jest zaznaczone, powoduje, że ekran jest włączony.
Pole wyboru Bezsenność powoduje, że aplikacja nie śpi.

Interfejs Idle Detection API

Czasami, nawet jeśli patrzysz w ekran godzinami, jest to po prostu bezużyteczne i ciężko wpadniesz na najprostszy pomysł z kartką z życzeniami. Interfejs Idle Detection API umożliwia aplikacji wykrywanie czasu bezczynności użytkownika. Jeśli użytkownik będzie nieaktywny zbyt długo, aplikacja zresetuje się do stanu początkowego i wyczyści obszar roboczy. Ten interfejs API jest obecnie objęty uprawnieniami dotyczącymi powiadomień, ponieważ wiele produkcyjnych przypadków użycia wykrywania bezczynności dotyczy powiadomień – na przykład po to, aby wysyłać powiadomienie tylko na urządzenie, którego użytkownik aktualnie używa.

Po upewnieniu się, że uprawnienia do powiadomień są przyznane, utworzę instancję detektora bezczynnego. Rejestruję detektor zdarzeń, który nasłuchuje zmian związanych z bezczynnością, w tym użytkownika i stanu 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. Określam wartość progową wykrywania 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 wczytuje się ten kod tylko wtedy, gdy przeglądarka go obsługuje.

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

W aplikacji Fugu Greetings obszar roboczy jest usuwany, gdy pole wyboru Efemeryczne jest zaznaczone, a użytkownik zbyt długo jest nieaktywny.

w aplikacji Fugu Greetings wyczyszczone obszary robocze, gdy użytkownik pozostawał bezczynny przez zbyt długi czas.
Jeśli pole wyboru Efemeryczny jest zaznaczone, a użytkownik był nieaktywny przez zbyt długi czas, obszar roboczy zostanie wyczyszczony.

Zakończenie

Uff, co za przejażdżka. Tak wiele interfejsów API w jednej przykładowej aplikacji. I pamiętaj, że nigdy nie każę użytkownikowi płacić za funkcje, których przeglądarka nie obsługuje. Korzystając z progresywnych ulepszeń, dbam o wczytywanie tylko odpowiedniego kodu. A ponieważ w przypadku HTTP/2 żądania są tanie, ten wzorzec powinien dobrze sprawdzać się w wielu aplikacjach. W przypadku bardzo dużych aplikacji może być jednak warto rozważyć korzystanie z pakietu.

Panel Network (Sieć) w Chrome DevTools pokazujący tylko żądania dotyczące plików z kodem obsługiwanym przez bieżącą przeglądarkę.
Karta Sieć w Narzędziach deweloperskich w Chrome zawiera tylko żądania plików z kodem obsługiwanym przez bieżącą przeglądarkę.

Aplikacja może wyglądać trochę inaczej w zależności od przeglądarki, ponieważ nie wszystkie platformy obsługują wszystkie funkcje. Jednak jej główna funkcjonalność jest zawsze tam dostępna, stopniowo ulepszana zgodnie z możliwościami danej przeglądarki. Te funkcje 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.

Aplikacja Fugu Greetings uruchomiona w Chrome na Androidzie, pokazująca wiele dostępnych funkcji.
Aplikacja Fugu Greetings uruchomiona w Chrome na Androidzie.
Aplikacja Fugu Greetings uruchomiona w przeglądarce Safari na komputery, z mniejszą liczbą dostępnych funkcji.
Fugu Greetings w przeglądarce Safari na komputerze.
Aplikacja Fugu Greetings uruchomiona w przeglądarce Chrome na komputery, pokazująca 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 na GitHubie.
Aplikacja Fugu Greetings na GitHubie.

Zespół Chromium pracuje nad tym, aby trawa była bardziej ekologiczna w przypadku zaawansowanych interfejsów API Fugu. Stopniowo ulepszając aplikację, dbam o dobre wrażenia użytkowników. Ale osoby używające przeglądarek, które obsługują więcej interfejsów API platform internetowych, czują się jeszcze lepiej. Z niecierpliwością czekamy na efekty Twoich postępów w aplikacjach.

Podziękowania

Jestem wdzięczna Christian Liebel i Hemanthowi HM, którzy stworzyli pozdrowienia z Fugu. Ten artykuł napisali Joe Medley i Kayce Basques. Jake Archibald pomógł mi poznać sytuację z dynamicznymi elementami import() w kontekście skryptu service worker.