Migliorare progressivamente la tua app web progressiva

Sviluppo per browser moderni e miglioramento progressivo come nel 2003

Nel marzo del 2003, Nick Finck e Steve Champeon stupivano il mondo del web design con il concetto di miglioramento progressivo, una strategia di web design che enfatizza il caricamento iniziale dei contenuti principali delle pagine web per poi aggiungere progressivamente livelli di presentazione e funzionalità più precisi e tecnici ai contenuti. Nel 2003, il miglioramento progressivo prevedeva l'utilizzo, all'epoca, di funzionalità CSS moderne, JavaScript non invasivo e persino Scalable Vector Graphics. Il miglioramento progressivo nel 2020 e negli anni successivi riguarda l'utilizzo di funzionalità moderne dei browser.

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

JavaScript moderno

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

La tabella di supporto di CanIUse per le funzionalità ES6 che mostra il supporto su tutti i principali browser.
Tabella di supporto dei browser 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 un comportamento asincrono basato sulle promesse in uno stile più chiaro, evitando di dover configurare esplicitamente catene di promesse.

La tabella di supporto di CanIUse per le funzioni asincrone che mostrano il supporto in tutti i principali browser.
Tabella di supporto del browser con le funzioni asincrone. (Fonte)

Inoltre, anche le recentissime aggiunte di lingue ES 2020, come il concatenamento facoltativo e la coalescenza nullish, hanno raggiunto il supporto molto rapidamente. Di seguito è riportato un esempio di codice. Per quanto riguarda le funzionalità JavaScript principali, l'erba non potrebbe essere molto più verde di quanto non sia oggi.

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 di Windows XP, verde erba.
Per quanto riguarda le funzionalità JavaScript principali, l'erba è verde. (Screenshot del prodotto Microsoft, utilizzato con l'autorizzazione).

App di esempio: Fugu Greetings

Per questo articolo, lavoro con una semplice PWA, chiamata Fugu Greetings (GitHub). Il nome di questa app è solo l'inizio di Project Fugu 🐡, un'iniziativa che ha l'obiettivo di dotare il web di tutte le potenzialità delle applicazioni Android/iOS/desktop. Puoi scoprire di più sul progetto nella relativa pagina di destinazione.

Fugu Greetings è un'app di disegno che ti consente di creare biglietti di auguri virtuali e inviarli ai tuoi cari. È un esempio dei concetti principali delle PWA. È affidabile e completamente offline abilitato, quindi puoi utilizzarla anche se non disponi di una rete. È inoltre installabile sulla schermata Home di un dispositivo e si integra perfettamente con il sistema operativo come applicazione autonoma.

PWA Fugu Greetings con un disegno che ricorda il logo della community PWA.
L'app di esempio Fugu Greetings.

Miglioramento progressivo

Una volta rimosso, è il momento di parlare di miglioramento progressivo. Il glossario di documenti web MDN definisce il concetto nel seguente modo:

Il miglioramento progressivo è una filosofia di progettazione che fornisce una base di contenuti e funzionalità essenziali al maggior numero di utenti possibile, 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 sono in grado di 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ù user agent sconosciuti. Il degrado significativo è correlato, ma non è la stessa cosa ed è spesso visto come andare nella direzione opposta al miglioramento progressivo. In realtà, entrambi gli approcci sono validi e spesso possono integrarsi a vicenda.

Collaboratori MDN

Iniziare ogni biglietto di auguri da zero può essere molto difficile. Quindi perché non avere una funzionalità che consenta agli utenti di importare un'immagine e iniziare da lì? Con un approccio tradizionale, avresti dovuto usare un elemento <input type=file> per renderlo possibile. Innanzitutto, crea l'elemento, imposta il valore type su 'file' e aggiungi i tipi MIME alla proprietà accept, quindi fai clic sull'elemento in modo programmatico per rimanere in attesa delle 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();
  });
};

Quando è presente una funzionalità di import, probabilmente dovrebbe esistere una funzionalità di import per consentire agli utenti di salvare le cartoline di auguri localmente. Il modo tradizionale per salvare i file è creare un link di ancoraggio con un attributo download e con un URL blob come href. Devi anche "fai clic" in modo programmatico per attivare il download e, per evitare fughe 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 minuto. Mentalmente, non hai "scaricato" una cartolina di auguri, l'hai salvata. Anziché mostrare una finestra di dialogo "Salva" che ti consente di scegliere dove posizionare il file, il browser ha scaricato direttamente la scheda di auguri senza interazione da parte dell'utente e l'ha inserita direttamente nella cartella Download. Non è il massimo.

E se esistesse un modo migliore? E se potessi semplicemente aprire un file locale, modificarlo e poi salvare le modifiche, in un nuovo file o tornare al file originale che avevi aperto inizialmente? Ho notato che c'è. L'API File System Access ti consente di aprire e creare file e directory, nonché di modificarli e salvarli .

Come faccio a rilevare le funzionalità di un'API? L'API File System Access espone un nuovo metodo window.chooseFileSystemEntries(). Di conseguenza, devo caricare in modo condizionale moduli di importazione ed esportazione diversi a seconda della disponibilità di questo metodo. La procedura ti è spiegata di seguito.

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 approfondire i dettagli dell'API File System Access, vediamo rapidamente il modello di miglioramento progressivo. Sui browser che attualmente non supportano l'API File System Access, carico gli script precedenti. Puoi visualizzare le schede della rete di Firefox e Safari di seguito.

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

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

Chrome DevTools che mostra il caricamento dei file moderni.
Scheda di rete Chrome DevTools.

API File System Access

Una volta risolto il problema, è il momento di esaminare l'effettiva implementazione basata sull'API File System Access. Per importare un'immagine, chiamo window.chooseFileSystemEntries() e passo una proprietà accepts dove dico di voler file immagine. Sono supportati sia le estensioni dei file sia i tipi MIME. Ciò si traduce in un handle di file, da cui posso ottenere 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 uguale, ma questa volta devo passare un parametro di tipo 'save-file' al metodo chooseFileSystemEntries(). Da qui vedrò una finestra di dialogo per il salvataggio dei file. Con il file aperto, non era necessario perché 'open-file' è l'impostazione predefinita. Ho impostato il parametro accepts in modo simile a prima, ma questa volta è limitato solo alle immagini PNG. Ricevo di nuovo un handle di file, ma invece di recuperare il file, questa volta creo uno stream scrivibile chiamando createWritable(). Poi scrivo nel file il blob, che è l'immagine della mia cartolina di auguri. Infine, chiudo lo stream scrivibile.

Si può sempre verificare un errore totale: lo spazio sul disco potrebbe essere esaurito, potrebbe essersi verificato un errore di scrittura o lettura o semplicemente l'utente annulla la finestra di dialogo del file. Per questo motivo, racchiudo 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 sul canvas. Posso apportare le modifiche e salvarle in una finestra di dialogo "Salva" in cui scegliere il nome e la posizione di archiviazione del file. Ora il file è pronto per essere conservato per l'eternità.

App Fugu Greetings con una finestra di dialogo di apertura dei file.
La finestra di dialogo di apertura dei file.
App Fugu Greetings ora con un&#39;immagine importata.
L'immagine importata.
App Fugu Greetings con l&#39;immagine modificata.
Salvataggio dell'immagine modificata in un nuovo file.

API Web Share e Web Share Target

Oltre a conservare la memoria per l'eternità, forse in realtà voglio condividere il mio biglietto di auguri. L'API Web Share e l'API Web Share Target mi permettono di farlo. I sistemi operativi per dispositivi mobili e, più di recente, per desktop hanno acquisito meccanismi di condivisione integrati. Ad esempio, di seguito è riportato il foglio di condivisione di Safari per computer 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.

Foglio 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 per renderlo possibile è piuttosto semplice. Chiamo navigator.share() e gli passo title, text e url facoltativo in un oggetto. E se volessi 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 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);
}

Ti mostro come far funzionare questa operazione con l'applicazione per il biglietto di auguri Fugu. Innanzitutto, devo preparare un oggetto data con un array files composto da un BLOB, poi title e text. Come best practice, utilizzo il nuovo metodo navigator.canShare() che fa quello che suggerisce il suo nome: Mi dice se l'oggetto data che sto cercando di condividere può essere tecnicamente condiviso dal browser. Se navigator.canShare() mi comunica che i dati possono essere condivisi, posso chiamare navigator.share() come prima. Dato che è possibile che non si verifichi un problema, utilizzo 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, uso il miglioramento progressivo. Se 'share' e 'canShare' sono presenti nell'oggetto navigator, solo allora carico share.mjs tramite import() dinamico. Sui browser come Safari per dispositivi mobili 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 che supporta il browser come Chrome su Android, si apre il foglio di condivisione integrato. Ad esempio, posso scegliere Gmail: compare il widget Email Composer con l'immagine allegata.

Foglio di condivisione a livello di sistema operativo che mostra varie app con cui condividere l&#39;immagine.
Scegliere 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 nel compositore di Gmail.

L'API Contact Picker

Ora vorrei parlare dei contatti, cioè la rubrica di un dispositivo o l'app di gestione dei contatti. Quando scrivi un biglietto di auguri, potrebbe non essere sempre facile scrivere correttamente il nome di qualcuno. Ad esempio, ho un amico Sergey che preferisce che il suo nome si scriva in caratteri cirillici. Uso una tastiera QWERTZ tedesca e non ho idea di come digitare il nome. Questo è un problema che l'API Contact Picker può risolvere. Dato che i miei amici sono archiviati nell'app Contatti del telefono, tramite l'API Contacts Picker, posso accedere ai contatti dal Web.

Innanzitutto, devo specificare l'elenco di proprietà a cui voglio accedere. In questo caso, voglio solo i nomi, ma per altri casi d'uso potrei essere interessati a numeri di telefono, email, icone avatar o indirizzi fisici. Successivamente, configuro un oggetto options e imposto multiple su true, in modo da poter selezionare più voci. 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 avrai probabilmente imparato il pattern: Carico 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, marca di un amico o un'azienda, marca di Brera e 劳伦斯·爱德华·"拉里"·佩奇, puoi visualizzare solo i nomi dei contatti, ad esempio i nomi degli altri contatti, I loro nomi sono poi riportati sulla mia cartolina di auguri.

Selettore di contatti che mostra i nomi di due contatti nella rubrica.
Selezione di due nomi con il selettore contatti dalla rubrica.
I nomi dei due contatti selezionati in precedenza disegnati sulla cartolina di auguri.
I due nomi vengono quindi riportati sul biglietto di auguri.

L'API Asynchronous Clipboard

Nel prossimo video c'è il copia e incolla. Il copia e incolla è una delle nostre operazioni preferite degli sviluppatori di software. Come autrice di biglietti di auguri, a volte potrei voler fare lo stesso. Potrei incollare un'immagine in un biglietto di auguri a cui sto lavorando o copiare la mia cartolina di auguri per poter continuare a modificarla da qualche altra parte. L'API Async Clipboard supporta sia testo che immagini. Vediamo come ho aggiunto il supporto per copia e incolla nell'app Fugu Greetings.

Per copiare qualcosa negli appunti del sistema, devo scriverci sopra. Il metodo navigator.clipboard.write() utilizza un array di elementi degli appunti come parametro. 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 visualizzare gli elementi degli appunti che ottengo chiamando navigator.clipboard.read(). Il motivo è che più elementi degli Appunti potrebbero essere presenti negli Appunti 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 degli 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);
  }
};

E ormai è quasi inutile dirlo. Lo faccio solo sui browser supportati.

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

Come funziona nella pratica? Ho un'immagine aperta nell'app Anteprima di macOS e la copia 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.

App Fugu Greetings che mostra la richiesta di autorizzazione per gli appunti.
La richiesta di autorizzazione per gli appunti.

Infine, dopo aver accettato l'autorizzazione, l'immagine viene incollata nell'applicazione. funziona anche l'altro senso del giro. Copia un biglietto di auguri negli appunti. Quando apro l'anteprima e 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 senza titolo appena incollata.
Un'immagine incollata nell'app Anteprima di macOS.

L'API Badging

Un'altra utile API è l'API Badging. Come PWA installabile, Fugu Greetings ha ovviamente un'icona dell'app che gli utenti possono inserire sul dock delle app o sulla schermata Home. Un modo semplice e divertente per dimostrare l'API consiste nell'usarla (ab) in Fugu Greetings come contatore dei tratti di penna. Ho aggiunto un listener di eventi che incrementa il contatore dei tratti di penna ogni volta che si verifica l'evento pointerdown, quindi 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.

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

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

I numeri da uno a sette disegnati sul biglietto di auguri, ciascuno con un tratto a penna.
Disegnare i numeri da 1 a 7 con 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.

API Periodic Background Sync

Vuoi iniziare ogni giorno con qualcosa di nuovo? Una caratteristica speciale dell'app Fugu Greetings è che può ispirarti ogni mattina con una nuova immagine di sfondo per iniziare il tuo biglietto di auguri. L'app utilizza l'API Periodic Background Sync per raggiungere questo obiettivo.

Il primo passaggio consiste nel register un evento di sincronizzazione periodico nella registrazione del service worker. Rimane in ascolto di un tag di sincronizzazione chiamato 'image-of-the-day' e ha un intervallo minimo di un giorno, per cui l'utente può ottenere 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 canvas e 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 davvero di un miglioramento progressivo, per cui 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 che non sono supportati, non viene caricato nessuno di questi. Nota che utilizzo il service worker, invece di un elemento import() dinamico (non ancora supportato nel contesto di un service worker ancora), l'uso della versione classica 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, se premi il pulsante Sfondo viene visualizzata l'immagine della cartolina di auguri del giorno che viene aggiornata ogni giorno tramite l'API Periodic Background Sync.

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

API Notification Triggers

A volte, anche con tanta ispirazione, hai bisogno di una spinta per finire un biglietto di auguri iniziato. Questa è una funzionalità abilitata dall'API Notification Triggers. In qualità di utente, posso inserire un'ora in cui voglio ricevere un consiglio per finire la mia cartolina di auguri. Quando sarà il momento, riceverò una notifica che mi informa che la mia cartolina di auguri è in attesa.

Dopo aver richiesto l'ora di destinazione, l'applicazione pianifica la notifica con un showTrigger. Può essere un TimestampTrigger con la data target selezionata in precedenza. La notifica del promemoria verrà attivata localmente, non è necessaria alcuna rete o 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 il resto 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, un messaggio mi chiede quando voglio ricevere un promemoria per finire la mia cartolina di auguri.

App Fugu Greetings con un messaggio che chiede all&#39;utente quando vuole ricevere un promemoria per completare la cartolina di auguri.
Programmare una notifica locale per ricevere un promemoria che ti ricordi di completare una cartolina di auguri.

Quando una notifica programmata viene attivata in Fugu Greetings, viene visualizzata come qualsiasi altra notifica ma, come ho scritto prima, non richiede una connessione di rete.

Il Centro notifiche macOS mostra una notifica attivata da Fugu Greetings.
La notifica attivata viene visualizzata nel Centro notifiche di macOS.

API Wake Lock

Vorrei includere anche l'API wakelock. A volte basta guardare lo schermo per un tempo abbastanza lungo finché l'ispirazione non ti bacia. La cosa peggiore che può capitare è che lo schermo si spegna. L'API Wake Lock può impedire che accada.

Il primo passaggio consiste nell'ottenere un wakelock con navigator.wakelock.request method(). Passo la stringa 'screen' per ottenere un wakelock schermo. Aggiungo quindi un listener di eventi che mi comunica quando viene rilasciato il wakelock. Questo può accadere, ad esempio, quando la visibilità della scheda cambia. In tal 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 Insomnia che, se selezionata, tiene attivo lo schermo.

La casella di controllo per l&#39;insonnia, se selezionata, mantiene lo schermo attivo.
La casella di controllo Insonnia mantiene attiva l'app.

API Idle Detection

A volte, anche se guardi lo schermo per ore, è inutile e non ti viene in mente cosa fare con un 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 viene ripristinata allo stato iniziale e cancella il canvas. Questa API attualmente è protetta da autorizzazione alle notifiche, poiché molti casi d'uso in produzione per il rilevamento di inattività sono correlati alle notifiche, ad esempio per inviare una notifica solo a un dispositivo che l'utente sta utilizzando attivamente.

Dopo aver ottenuto l'autorizzazione alle notifiche, creo un'istanza del rilevatore di inattività. Registri un listener di eventi che rimane in ascolto delle modifiche inattive, che includono l'utente e lo stato 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. Imposto il rilevatore di inattività con una soglia di 60 secondi.

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 quando il browser lo supporta.

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

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

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

Chiusura

Fiuuu, che giro. Tante API in un'unica app di esempio. Inoltre, ricorda che non faccio mai pagare all'utente il costo di download per una funzionalità non supportata dal suo browser. Utilizzando il miglioramento progressivo, mi assicuro che venga caricato solo il codice pertinente. E poiché con HTTP/2 le richieste sono economiche, questo pattern dovrebbe funzionare bene per molte applicazioni, anche se potresti considerare un bundler per le app molto grandi.

Riquadro Network di Chrome DevTools che mostra solo le richieste di file con codice supportato dal browser corrente.
Scheda Rete di Chrome DevTools che mostra solo le richieste di file con codice supportato dal browser corrente.

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

Fugu Greetings in esecuzione su Chrome per Android che mostra molte funzionalità disponibili.
Fugu Greetings in esecuzione su Chrome per Android.
Fugu Greetings in esecuzione su Safari per computer, che mostra meno funzionalità disponibili.
Fugu Greetings in esecuzione su Safari per computer.
Fugu Greetings in esecuzione su Chrome desktop che mostra molte funzionalità disponibili.
Fugu Greetings in esecuzione su Chrome per computer.

Se ti interessa l'app Fugu Greetings, cercala e forkla su GitHub.

Repo di Fugu Greetings su GitHub.
App Fugu Greetings su GitHub.

Il team di Chromium si sta adoperando per rendere l'erba più ecologica in relazione alle API Fugu avanzate. Applicando un miglioramento progressivo allo sviluppo della mia app, mi assicuro che tutti abbiano un'esperienza di base buona e solida, ma che le persone che utilizzano browser che supportano più API della piattaforma web abbiano un'esperienza ancora migliore. Non vedo l'ora di scoprire cosa farai con il miglioramento progressivo nelle tue app.

Ringraziamenti

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