Migliorare progressivamente la tua app web progressiva

Sviluppo per browser moderni e miglioramento progressivo come se fosse il 2003

A marzo 2003, Nick Finck e Steve Champeon hanno stupito il mondo del web design con il concetto di miglioramento progressivo, una strategia per il web design che dà la priorità al caricamento dei contenuti di base della pagina web e che poi aggiunge progressivamente livelli di presentazione e funzionalità più sfumati e tecnicamente rigorosi sopra i contenuti. Nel 2003, il miglioramento progressivo riguardava l'uso, all'epoca, delle moderne funzionalità CSS, del codice JavaScript non invasivo e persino solo Scalable Vector Graphics. Il miglioramento progressivo nel 2020 e oltre riguarda l'utilizzo delle funzionalità dei browser moderni.

Design web inclusivo per il futuro con miglioramento progressivo. Slide del titolo della presentazione originale di Finck e Champeon.
Slide: Web design inclusivo per il futuro con miglioramento progressivo (Fonte)

JavaScript moderno

A proposito di JavaScript, la situazione del supporto dei browser per le funzionalità di JavaScript ES 2015 di base è ottima. Il nuovo standard include promesse, moduli, classi, valori letterali di modello, funzioni freccia, let e const, parametri predefiniti, generatori, assegnazione destrutturante, resto e distribuzione, Map/Set, WeakMap/WeakSet e molti altri. Sono supportati tutti.

La tabella di supporto di CanIUse per le funzionalità ES6 che mostra il supporto in tutti i principali browser.
Tabella di supporto dei browser per ECMAScript 2015 (ES6). (Fonte)

Le funzioni asincrone, una funzionalità di ES 2017 e una delle mie preferite, possono essere utilizzate in tutti i principali browser. Le parole chiave async e await consentono di scrivere il comportamento asincrono basato sulla promessa con uno stile più chiaro, evitando di configurare esplicitamente le catene di promesse.

La tabella di supporto di CanIUse per le funzioni asincrone che mostra il supporto in tutti i principali browser.
La tabella di supporto dei browser per le funzioni asincrone. (Fonte)

Anche le aggiunte più recenti al linguaggio ES 2020, come optional chaining e nullish coalescing hanno raggiunto il supporto molto rapidamente. Di seguito è riportato un esempio di codice. Per quanto riguarda le funzionalità di base di JavaScript, non c'è molto di più da fare.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
L'iconica immagine di sfondo verde con erba di Windows XP.
L'erba è verde quando si tratta delle funzionalità principali di JavaScript. (Screenshot del prodotto Microsoft, utilizzato con autorizzazione.)

L'app di esempio: Fugu Greetings

Per questo articolo, lavoro con una semplice PWA, chiamata Fugu Greetings (GitHub). Il nome di questa app è un omaggio a Project Fugu 🐡, un progetto volto a offrire al web tutte le funzionalità delle applicazioni per Android/iOS/computer. Per saperne di più sul progetto, visita la relativa pagina di destinazione.

Fugu Greetings è un'app di disegno che ti consente di creare cartoline di auguri virtuali e inviarle ai tuoi cari. È un esempio di concetti fondamentali delle PWA. È affidabile e completamente offline, quindi puoi utilizzarlo anche se non hai una rete. Inoltre, è installabile nella schermata Home di un dispositivo e si integra perfettamente con il sistema operativo come applicazione autonoma.

La PWA Fugu Greetings con un disegno simile al logo della community PWA.
L'app di esempio Fugu Greetings.

Miglioramento progressivo

Dopodiché, è il momento di parlare del miglioramento progressivo. Il Glossario di MDN Web Docs definisce il concetto come segue:

Il miglioramento progressivo è una filosofia di progettazione che fornisce una base di contenuti e funzionalità essenziali al maggior numero possibile di utenti, offrendo al contempo la migliore esperienza possibile solo agli utenti dei browser più moderni in grado di eseguire tutto il codice richiesto.

Il rilevamento delle funzionalità viene generalmente utilizzato per determinare se i browser possono gestire funzionalità più moderne, mentre i polyfill vengono spesso utilizzati per aggiungere funzionalità mancanti con JavaScript.

[…]

Il miglioramento progressivo è una tecnica utile che consente agli sviluppatori web di concentrarsi sullo sviluppo dei migliori siti web possibili, facendoli funzionare su più agenti utente sconosciuti. Il degrado elegante è correlato, ma non è la stessa cosa e spesso è considerato opposto al miglioramento progressivo. In realtà, entrambi gli approcci sono validi e spesso possono essere complementari.

Collaboratori di MDN

Iniziare ogni biglietto di auguri da zero può essere molto complicato. Perché non avere una funzionalità che consenta agli utenti di importare un'immagine e iniziare da lì? Con un approccio tradizionale, avresti utilizzato un elemento <input type=file> per farlo. Innanzitutto, devi creare l'elemento, impostare type su 'file' e aggiungere i tipi MIME alla proprietà accept, quindi "fai clic" su di esso in modo programmatico e ascolta le modifiche. Quando selezioni un'immagine, questa viene importata direttamente nella tela.

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

Se è presente una funzionalità di importazione, probabilmente dovrebbe essere presente anche una funzionalità di esportazione per consentire agli utenti di salvare le cartoline localmente. Il modo tradizionale per salvare i file è creare un link di ancoraggio con un attributo download e un URL del blob come href. Dovresti anche "fare clic" su di esso in modo programmatico per attivare il download e, per evitare perdite di memoria, non dimenticare di revocare l'URL dell'oggetto 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();
};

Ma aspetta un attimo. Mentalmente, non hai "scaricato" una cartolina, ma la hai "salvata". Anziché mostrarti una finestra di dialogo "Salva" che ti consente di scegliere dove inserire il file, il browser ha scaricato direttamente la cartolina senza interazione dell'utente e l'ha inserita direttamente nella cartella Download. Non è un buon segno.

E se ci fosse un modo migliore? E se potessi semplicemente aprire un file locale, modificarlo e quindi salvare le modifiche in un nuovo file o nel file originale che avevi aperto inizialmente? E infatti esiste. L'API File System Access consente di aprire e creare file e directory, nonché di modificarli e salvarli.

Quindi, come faccio a rilevare le caratteristiche di un'API? L'API File System Access espone un nuovo metodo window.chooseFileSystemEntries(). Di conseguenza, devo caricare in modo condizionale diversi moduli di importazione ed esportazione a seconda che questo metodo sia disponibile o meno. Di seguito ho mostrato come fare.

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

Prima di entrare nei dettagli dell'API Accesso al file system, vorrei evidenziare brevemente il pattern di miglioramento progressivo. Nei browser che al momento non supportano l'API File System Access, carico gli script precedenti. Di seguito puoi vedere le schede della rete di Firefox e Safari.

Safari Web Inspector che mostra il caricamento dei file precedenti.
Scheda della rete di Safari Web Inspector.
Strumenti per sviluppatori di Firefox che mostrano il caricamento dei file precedenti.
Scheda di rete degli Strumenti per sviluppatori di Firefox.

Tuttavia, su Chrome, un browser che supporta l'API, vengono caricati solo i nuovi script. Questo è reso possibile grazie al import() dinamico, supportato da tutti i browser moderni. Come dicevo prima, l'erba è piuttosto verde in questi giorni.

Strumenti per sviluppatori di Chrome che mostrano il caricamento dei file moderni.
Scheda Rete di Chrome DevTools.

L'API File System Access

Ora che ho risolto il problema, è il momento di esaminare l'implementazione effettiva basata sull'API File System Access. Per importare un'immagine, chiamo window.chooseFileSystemEntries() e passo una proprietà accepts in cui indico che voglio i file immagine. Sono supportate sia le estensioni dei file sia i tipi MIME. Ne deriva un handle di file, da cui posso recuperare il file effettivo chiamando 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);
  }
};

L'esportazione di un'immagine è quasi la stessa, ma questa volta devo passare un parametro di tipo 'save-file' al metodo chooseFileSystemEntries(). Viene visualizzata una finestra di dialogo per il salvataggio del file. Con il file aperto, non era necessario perché 'open-file' è il valore predefinito. Ho impostato il parametro accepts in modo simile a prima, ma questa volta limitato solo alle immagini PNG. Di nuovo ricevo un handle file, ma anziché ottenere il file, questa volta creo uno stream scrivibile chiamando createWritable(). Poi scrivo il blob, ovvero l'immagine della cartolina, nel file. Infine, chiudo lo stream modificabile.

È sempre possibile che si verifichi un errore: potrebbe non essere disponibile spazio sul disco, potrebbe verificarsi un errore di scrittura o lettura o l'utente potrebbe semplicemente annullare la finestra di dialogo del file. Per questo motivo, racchiude sempre le chiamate in un'istruzione 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);
  }
};

Grazie al miglioramento progressivo con l'API File System Access, posso aprire un file come prima. Il file importato viene disegnato direttamente sulla tela. Posso apportare le modifiche e infine salvarle con una vera finestra di dialogo in cui posso scegliere il nome e il percorso di archiviazione del file. Ora il file è pronto per essere conservato per sempre.

App Fugu Greetings con una finestra di dialogo di apertura del file.
La finestra di dialogo per aprire il file.
App Fugu Greetings ora con un&#39;immagine importata.
L'immagine importata.
App Fugu Greetings con l&#39;immagine modificata.
Salvare l'immagine modificata in un nuovo file.

API Web Share e Web Share Target

A parte l'archiviazione per l'eternità, forse voglio davvero condividere il mio biglietto di auguri. Questo è qualcosa che l'API Web Share e l'API Web Share Target mi consentono di fare. I sistemi operativi mobile e, più di recente, quelli desktop hanno acquisito meccanismi di condivisione integrati. Ad esempio, di seguito è riportato il foglio di condivisione di Safari su computer desktop su macOS, attivato da un articolo sul mio blog. Quando fai clic sul pulsante Condividi articolo, puoi condividere un link all'articolo con un amico, ad esempio tramite l'app Messaggi di macOS.

Il riquadro di condivisione di Safari per computer su macOS attivato dal pulsante Condividi di un articolo
API Web Share su Safari per computer su macOS.

Il codice da utilizzare per crearlo è piuttosto semplice. Chiamo navigator.share() e passo title, text e url facoltativi in un oggetto. Ma cosa succede se voglio allegare un'immagine? Il livello 1 dell'API Web Share non supporta ancora questa funzionalità. La buona notizia è che il livello 2 di condivisione web ha aggiunto funzionalità di condivisione dei file.

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

Vediamo come funziona con l'applicazione di cartoline di auguri Fugu. Innanzitutto, devo preparare un oggetto data con un array files composto da un blob, poi un title e un text. In seguito, come best practice, utilizzo il nuovo metodo navigator.canShare() che fa ciò che suggerisce il nome: mi dice se l'oggetto data che sto cercando di condividere può essere tecnicamente condiviso dal browser. Se navigator.canShare() mi dice che i dati possono essere condivisi, posso chiamare navigator.share() come prima. Dato che può verificarsi un errore, sto usando di nuovo un blocco 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);
  }
};

Come prima, utilizzo il miglioramento progressivo. Se nell'oggetto navigator esistono sia 'share' che 'canShare', solo allora procedo e caricashare.mjs tramite import() dinamico. Su browser come Safari mobile che soddisfano solo una delle due condizioni, non carico la funzionalità.

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

In Fugu Greetings, se tocco il pulsante Condividi su un browser supportato come Chrome su Android, si apre il riquadro di condivisione integrato. Posso, ad esempio, scegliere Gmail e il widget per la composizione dell'email viene visualizzato con l'immagine allegata.

Scheda di condivisione a livello di sistema operativo che mostra varie app con cui condividere l&#39;immagine.
È in corso la scelta di un'app con cui condividere il file.
Widget di composizione dell&#39;email di Gmail con l&#39;immagine allegata.
Il file viene allegato a una nuova email nell'editor di Gmail.

L'API Contact Picker

Adesso voglio parlare dei contatti, ovvero della rubrica o dell'app di gestione dei contatti di un dispositivo. Quando scrivi una cartolina, non è sempre facile scrivere correttamente il nome di una persona. Ad esempio, ho un amico, Sergey, che preferisce che il suo nome venga scritto in lettere cirilliche. Sto usando una tastiera QWERTZ tedesca e non ho idea di come digitare il nome. Si tratta di un problema che l'API Contact Selecter può risolvere. Poiché ho un amico archiviato nell'app Contatti del telefono, tramite l'API Contacts Selecter posso accedere ai miei contatti dal web.

Innanzitutto, devo specificare l'elenco delle proprietà a cui voglio accedere. In questo caso, mi interessano solo i nomi, ma per altri casi d'uso potrei essere interessato a numeri di telefono, email, icone di avatar o indirizzi fisici. A questo punto configuro un oggetto options e imposto multiple su true, in modo da poter selezionare più di una voce. Infine, posso chiamare navigator.contacts.select(), che restituisce le proprietà desiderate per i contatti selezionati dall'utente.

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

A questo punto, probabilmente hai imparato il pattern: carica il file solo quando l'API è effettivamente supportata.

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

In Fugu Greeting, quando tocco il pulsante Contatti e seleziono i miei due migliori amici, Сергей Михайлович Брин e 劳伦斯·爱德华·"拉里"·佩奇, puoi vedere come il selezionatore di contatti è limitato a mostrare solo i loro nomi, ma non i loro indirizzi email o altre informazioni come i numeri di telefono. I loro nomi sono poi disegnati sulla mia cartolina di auguri.

Selettore di contatti che mostra i nomi di due contatti nella rubrica.
Seleziona due nomi con il selettore dei contatti dalla rubrica.
I nomi dei due contatti scelti in precedenza disegnati sulla cartolina.
I due nomi vengono poi disegnati sulla cartolina di auguri.

L'API Asynchronous Clipboard

Nel prossimo passaggio c'è il copia e incolla. Una delle nostre operazioni preferite come sviluppatori software è copia e incolla. Come autore di biglietti di auguri, a volte potrei voler fare lo stesso. Potresti voler incollare un'immagine in una cartolina di auguri su cui stai lavorando o copiare la cartolina per continuare a modificarla da un'altra posizione. L'API Async Clipboard supporta sia il testo che le immagini. Ti mostro come ho aggiunto il supporto di copia e incolla all'app di saluti Fugu.

Per copiare qualcosa negli appunti di sistema, devo poterci scrivere. Il metodo navigator.clipboard.write() accetta come parametro un array di elementi negli appunti. Ogni elemento degli appunti è essenzialmente un oggetto con un blob come valore e il tipo del blob come chiave.

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

Per incollare, devo eseguire un ciclo sugli elementi degli appunti che ottengo chiamando navigator.clipboard.read(). Il motivo è che nella clipboard potrebbero essere presenti più elementi in rappresentazioni diverse. Ogni elemento degli appunti ha un campo types che indica i tipi MIME delle risorse disponibili. Chiamo il metodo getType() dell'elemento appunti, passando il tipo MIME che ho ottenuto in precedenza.

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

A questo punto è quasi superfluo dirlo. Lo faccio solo sui browser supportati.

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

Come funziona in pratica? Ho un'immagine aperta nell'app macOS Preview e la copio negli appunti. Quando faccio clic su Incolla, l'app Fugu Greetings mi chiede se voglio consentire all'app di vedere testo e immagini negli appunti.

L&#39;app Fugu Greetings mostra la richiesta di autorizzazione per la clipboard.
Prompt per l'autorizzazione di accesso agli appunti.

Infine, dopo aver accettato l'autorizzazione, l'immagine viene incollata nell'applicazione. Funziona anche il contrario. Fammi copiare una cartolina negli appunti. Quando apro Anteprima, faccio clic su File e poi su Nuovo dagli appunti, la cartolina di auguri viene incollata in una nuova immagine senza titolo.

L&#39;app Anteprima di macOS con un&#39;immagine appena incollata senza titolo.
Un'immagine incollata nell'app Anteprima di macOS.

L'API Badging

Un'altra API utile è l'API Badging. In qualità di PWA installabile, Fugu Greetings ha ovviamente un'icona che gli utenti possono posizionare nella sezione App o nella schermata Home. Un modo semplice e divertente per dimostrare l'API è (abusarne) in Fugu Greetings come contatore dei tratti di penna. Ho aggiunto un gestore di eventi che incrementa il contatore dei tratti dello stilo ogni volta che si verifica l'evento pointerdown e poi imposta il badge dell'icona aggiornato. Ogni volta che la tela viene cancellata, il contatore viene reimpostato e il badge viene rimosso.

let strokes = 0;

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

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

Questa funzionalità è un miglioramento progressivo, quindi la logica di caricamento è la stessa di sempre.

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

In questo esempio ho disegnato i numeri da uno a sette, utilizzando un tratto di penna per ogni numero. Il contatore dei badge sull'icona è ora a sette.

I numeri da uno a sette disegnati sulla cartolina, ciascuno con un solo tratto di penna.
Disegna i numeri da 1 a 7 utilizzando sette tratti di penna.
Icona del badge nell&#39;app Fugu Greetings che mostra il numero 7.
Il contatore dei tratti di penna sotto forma di badge dell'icona dell'app.

L'API Periodic Background Sync

Vuoi iniziare la giornata con qualcosa di nuovo? Una caratteristica molto interessante dell'app Fugu Greetings è che può ispirarti ogni mattina con una nuova immagine di sfondo per iniziare la tua cartolina di auguri. A questo scopo, l'app utilizza l'API Periodic Background Sync.

Il primo passaggio consiste nel registrare un evento di sincronizzazione periodica nella registrazione del servizio worker. Ascolta un tag di sincronizzazione denominato 'image-of-the-day' e ha un intervallo minimo di un giorno, in modo che l'utente possa ricevere una nuova immagine di sfondo ogni 24 ore.

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

Il secondo passaggio consiste nell'ascoltare l'evento periodicsync nel service worker. Se il tag evento è 'image-of-the-day', ovvero quello registrato in precedenza, l'immagine del giorno viene recuperata tramite la funzione getImageOfTheDay(), e il risultato viene propagato a tutti i client, in modo che possano aggiornare i propri canvas e le cache.

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

Anche in questo caso si tratta di un miglioramento progressivo, pertanto il codice viene caricato solo quando l'API è supportata dal browser. Questo vale sia per il codice client sia per il codice del service worker. Nei browser non supportati, nessuno dei due viene caricato. Tieni presente che nel service worker, anziché un import() dinamico (che non è ancora supportato in un contesto di service worker), utilizzo il classico 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');
}

In Fugu Greetings, premendo il pulsante Sfondo viene visualizzata l'immagine della cartolina del giorno, aggiornata ogni giorno tramite l'API Periodic Background Sync.

App Fugu Greetings con una nuova immagine di cartoline del giorno.
Se premi il pulsante Sfondo, viene visualizzata l'immagine del giorno.

API Notification Triggers

A volte, anche se hai un'idea brillante, hai bisogno di un piccolo aiuto per completare una scheda di auguri iniziata. Questa è una funzionalità abilitata dall'API Notification Triggers. Come utente, posso inserire un orario in cui voglio ricevere un promemoria per completare la mia cartolina. A quel punto, riceverò una notifica che mi informa che la mia cartolina è in attesa.

Dopo aver richiesto l'ora di destinazione, l'applicazione pianifica la notifica con un showTrigger. Può essere un TimestampTrigger con la data di destinazione selezionata in precedenza. La notifica del promemoria verrà attivata localmente, non è necessaria la rete o il lato server.

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

Come per tutto ciò che ho mostrato finora, si tratta di un miglioramento progressivo, quindi il codice viene caricato solo in modo condizionale.

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

Quando seleziono la casella di controllo Promemoria in Fugu Greetings, mi viene chiesto quando voglio ricevere un promemoria per completare la mia cartolina.

App Fugu Greetings con un messaggio che chiede all&#39;utente quando vuole che gli venga ricordato di completare la cartolina.
Programmazione di una notifica locale per ricordare di completare una cartolina di auguri.

Quando viene attivata una notifica pianificata in Fugu Greetings, viene visualizzata come qualsiasi altra notifica, ma, come ho scritto in precedenza, non richiedeva una connessione di rete.

Centro notifiche macOS che mostra una notifica attivata dai messaggi di benvenuto Fugu.
La notifica attivata viene visualizzata nel Centro notifiche di macOS.

L'API Wake Lock

Voglio includere anche l'API Wake Lock. A volte basta fissare lo schermo finché l'ispirazione non ti bacia. Il peggio che può succedere è che lo schermo si spenga. L'API Wake Lock può impedire che ciò accada.

Il primo passo è ottenere un wakelock con il navigator.wakelock.request method(). Trasmetto la stringa 'screen' per ottenere un blocco risveglio dello schermo. Aggiungo quindi un gestore di eventi per essere informato quando il blocco di attivazione viene rilasciato. Questo può accadere, ad esempio, quando la visibilità della scheda cambia. In questo caso, quando la scheda diventa di nuovo visibile, posso ottenere di nuovo il wakelock.

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

Sì, si tratta di un miglioramento progressivo, quindi devo caricarlo solo quando il browser supporta l'API.

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

In Fugu Greetings è presente una casella di controllo Insonnia che, se selezionata, mantiene attivo lo schermo.

Se la casella di controllo Insonnia è selezionata, lo schermo rimane attivo.
La casella di controllo Insonnia mantiene l'app attiva.

L'API Idle Detection

A volte, anche se guardi lo schermo per ore, è inutile e non riesci a trovare la minima idea su cosa fare con il tuo biglietto di auguri. L'API Idle Detection consente all'app di rilevare il tempo di inattività dell'utente. Se l'utente rimane inattivo per troppo tempo, l'app torna allo stato iniziale e cancella la tela. Al momento, l'accesso a questa API è limitato dall'autorizzazione di notifica, poiché molti casi d'uso di produzione del rilevamento inattivo sono correlati alle notifiche, ad esempio per inviare una notifica solo a un dispositivo attualmente in uso dall'utente.

Dopo avermi accertato che l'autorizzazione per le notifiche sia stata concessa, creo un'istanza del rilevatore di inattività. Registro un gestore di eventi che rileva le modifiche in stato inattivo, inclusi lo stato dell'utente e della schermata. L'utente può essere attivo o inattivo e lo schermo può essere sbloccato o bloccato. Se l'utente è inattivo, la tela viene cancellata. Ho impostato una soglia di 60 secondi per il rilevatore di inattività.

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

Come sempre, carico questo codice solo se il browser lo supporta.

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

Nell'app Fugu Greetings, la tela viene cancellata quando la casella di controllo Ephemeral è selezionata e l'utente è inattivo per troppo tempo.

App Fugu Greetings con un canvas cancellato dopo che l&#39;utente è rimasto inattivo per troppo tempo.
Quando la casella di controllo Temporanea è selezionata e l'utente è rimasto inattivo per troppo tempo, il canvas viene deselezionato.

Chiusura

Che corsa. Un numero così elevato di API in un'unica app di esempio. Ricorda che non faccio mai pagare all'utente il costo del download per una funzionalità non supportata dal browser. Utilizzando il metodo di potenziamento progressivo, mi assicuro che venga caricato solo il codice pertinente. Poiché con HTTP/2 le richieste sono economiche, questo pattern dovrebbe funzionare bene per molte applicazioni, anche se potresti prendere in considerazione un aggregatore per le app di grandi dimensioni.

Riquadro Rete di DevTools di Chrome che mostra solo le richieste di file con codice supportato dal browser corrente.
La scheda Network di Chrome DevTools mostra solo le richieste per i file con codice supportato dal browser corrente.

L'app potrebbe avere un aspetto leggermente diverso su ogni browser, poiché non tutte le piattaforme supportano tutte le funzionalità, ma la funzionalità di base è sempre presente e viene migliorata progressivamente in base alle funzionalità del browser in questione. Tieni presente che queste funzionalità possono cambiare anche nello stesso browser, a seconda che l'app sia in esecuzione come app installata o in una scheda del browser.

Fugu Greetings in esecuzione su Chrome per Android, che mostra molte funzionalità disponibili.
Saluti Fugu in esecuzione su Chrome per Android.
Saluti Fugu in esecuzione su Safari desktop, con un numero minore di funzionalità disponibili.
Fugu Greetings in esecuzione su Safari per computer.
Fugu Greetings in esecuzione su Chrome per computer, che mostra molte funzionalità disponibili.
Fugu Greetings in esecuzione su Chrome per computer.

Se ti interessa l'app Fugu Greetings, vai a trovarla e crea un fork su GitHub.

Repository Fugu Greetings su GitHub.
App Fugu Greetings su GitHub.

Il team di Chromium sta lavorando duramente per rendere l'erba più verde quando si tratta di API Fugu avanzate. Applicando il miglioramento progressivo allo sviluppo della mia app, mi assicuro che tutti possano usufruire di un'esperienza di base solida e di qualità, ma che le persone che utilizzano browser che supportano più API di piattaforme web possano usufruire di un'esperienza ancora migliore. Non vedo l'ora di scoprire cosa farai con il miglioramento progressivo delle tue app.

Ringraziamenti

Sono grato a Christian Liebel e Hemanth HM che hanno entrambi contribuito a Fugu Greetings. Questo articolo è stato esaminato da Joe Medley e Kayce Basques. Jake Archibald mi ha aiutato a capire la situazione con import() dinamico in un contesto di worker di servizio.