Migliorare progressivamente la tua app web progressiva

Creare per browser moderni e migliorare progressivamente come nel 2003

Data di pubblicazione: 29 giugno 2020

Nel 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 enfatizza il caricamento iniziale dei contenuti principali della pagina web, per poi aggiungere progressivamente livelli di presentazione e funzionalità più sfumati e tecnicamente rigorosi sopra i contenuti. Nel 2003, il miglioramento progressivo riguardava l'utilizzo di funzionalità CSS moderne, JavaScript non intrusivo e persino Scalable Vector Graphics. Il miglioramento progressivo nel 2020 e negli anni successivi consiste nell'utilizzare le funzionalità dei browser moderni.

Progettazione web inclusiva per il futuro con miglioramenti progressivi. Slide di titolo della presentazione originale di Finck e Champeon.

JavaScript moderno

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

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

Le funzioni asincrone, una funzionalità 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 su promesse in uno stile più pulito, evitando la necessità 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 di linguaggio ES 2020 più recenti, come l'optional chaining e l'operatore di coalescenza null, hanno raggiunto il supporto molto rapidamente. Per quanto riguarda le funzionalità principali di JavaScript, non si può chiedere di meglio.

Ad esempio:

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 con l'erba verde.
Le funzionalità principali di JavaScript sono ben consolidate. (Screenshot del prodotto Microsoft, utilizzato con autorizzazione.)

L'app di esempio: Fugu Greetings

Per questo documento, lavoro con una PWA chiamata Fugu Greetings (GitHub). Il nome di questa app è un omaggio al progetto Fugu 🐡, un'iniziativa per dare al web tutte le funzionalità delle applicazioni Android, iOS e 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. Rappresenta un esempio dei concetti chiave delle PWA. È affidabile e completamente abilitato offline, quindi anche se non hai una rete, puoi comunque utilizzarlo. È anche installabile nella schermata Home di un dispositivo e si integra perfettamente con il sistema operativo come applicazione autonoma.

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

Potenziamento progressivo

Ora che abbiamo chiarito questo aspetto, è 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 la migliore esperienza possibile solo agli utenti dei browser più moderni che possono 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, facendo in modo che funzionino su più user agent sconosciuti. Il degrado controllato è correlato, ma non è la stessa cosa ed è spesso visto come un'operazione che va nella direzione opposta al miglioramento progressivo. In realtà, entrambi gli approcci sono validi e spesso possono essere complementari.

Collaboratori di MDN

Creare ogni biglietto di auguri da zero può essere davvero complicato. Perché non creare 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 il relativo type su 'file' e aggiungere i tipi MIME alla proprietà accept, quindi "fare clic" in modo programmatico e ascoltare le modifiche. Quando selezioni un'immagine, questa viene importata direttamente sul canvas.

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 importazione, probabilmente dovrebbe esserci anche una funzionalità di esportazione in modo che gli utenti possano salvare i propri biglietti di auguri in locale. Il modo tradizionale per salvare i file è creare un link di ancoraggio con un attributo download e con un URL blob come href. Inoltre, dovresti "fare clic" 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" un biglietto di auguri, ma lo hai "salvato". Anziché mostrare una finestra di dialogo "Salva" che ti consente di scegliere dove inserire il file, il browser ha scaricato direttamente il biglietto di auguri senza interazione dell'utente e lo ha inserito direttamente nella cartella Download. Non è il massimo.

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

Quindi, 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 diversi moduli di importazione ed esportazione a seconda che questo metodo sia disponibile.

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

Ma prima di entrare nei dettagli dell'API File System Access, vorrei evidenziare rapidamente il pattern di miglioramento progressivo. Sui browser che non supportano l'API File System Access, carico gli script legacy.

Safari Web Inspector che mostra il caricamento dei file legacy.
Strumenti per sviluppatori di Firefox che mostrano il caricamento dei file legacy.

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

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

API File System Access

Ora che ho affrontato questo argomento, è 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 file immagine. Sono supportate sia le estensioni dei file sia i tipi MIME. In questo modo ottengo un handle del 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(). Da qui viene visualizzata una finestra di dialogo di salvataggio del file. Con il file aperto, questa operazione non era necessaria, poiché 'open-file' è l'impostazione predefinita. Ho impostato il parametro accepts in modo simile a prima, ma questa volta limitato alle sole immagini PNG. Anche in questo caso ricevo un handle del file, ma anziché ottenere il file, questa volta creo un flusso scrivibile chiamando createWritable(). Poi scrivo il blob, ovvero l'immagine del biglietto di auguri, nel file. Infine, chiudo lo stream scrivibile.

Tutto può sempre fallire: lo spazio sul disco potrebbe essere esaurito, potrebbe verificarsi un errore di scrittura o lettura oppure l'utente potrebbe semplicemente annullare la finestra di dialogo del file. Per questo motivo, inserisco 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);
  }
};

Utilizzando il 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 salvarle con una finestra di dialogo di salvataggio reale, in cui posso 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 per l&#39;apertura dei file.
La finestra di dialogo di apertura del file.
L&#39;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

attempt-right

Oltre a conservarla per l'eternità, magari voglio anche condividere la mia cartolina di auguri. Questo è qualcosa che mi consentono di fare l'API Web Share e l'API Web Share Target. I sistemi operativi mobile e, più di recente, desktop hanno acquisito meccanismi di condivisione integrati.

Ad esempio, il foglio di condivisione di Safari per computer su macOS viene attivato quando un utente fa clic su Condividi articolo sul mio blog. Potresti condividere un link all'articolo con un amico utilizzando l'app Messaggi di macOS.

Per farlo, chiamo navigator.share() e gli trasmetto title, text e url facoltativi in un oggetto. Ma se voglio allegare un'immagine? Il livello 1 dell'API Web Share non lo supporta ancora. La buona notizia è che il livello 2 della 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);
}

Ti mostro come farlo funzionare con l'applicazione per biglietti di auguri Fugu. Innanzitutto, devo preparare un oggetto data con un array files composto da un blob, quindi un title e un text. Successivamente, 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ò tecnicamente essere condiviso dal browser. Se navigator.canShare() mi dice che i dati possono essere condivisi, sono pronto a chiamare navigator.share() come prima. Poiché tutto può fallire, 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, utilizzo il miglioramento progressivo. Se esistono sia 'share' che 'canShare' nell'oggetto navigator, solo allora vado avanti e carico share.mjs utilizzando import() dinamico. Sui 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 foglio di condivisione integrato. Ad esempio, posso scegliere Gmail e il widget di composizione delle email viene visualizzato con l'immagine allegata.

Foglio di condivisione a livello di sistema operativo che mostra varie app con cui condividere l&#39;immagine.
Scelta di un'app con cui condividere il file.
Il widget di composizione delle email di Gmail con l&#39;immagine allegata.
Il file viene allegato a una nuova email nel riquadro di composizione di Gmail.

API Contact Picker

Poi voglio parlare dei contatti, ovvero della rubrica di un dispositivo o dell'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 di nome Sergey che preferisce che il suo nome venga scritto in caratteri cirillici. Sto usando una tastiera QWERTZ tedesca e non ho idea di come digitare il suo nome. Questo è un problema che può essere risolto dall'API Contact Picker. Poiché ho memorizzato il mio amico nell'app Contatti dello smartphone, utilizzando l'API Contacts Picker, 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 avatar o indirizzi fisici. Poi, 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à ideali 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);
  }
};

Ormai avrai capito 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, Сергей Михайлович Брин e 劳伦斯·爱德华·"拉里"·佩奇, puoi vedere come il selettore di contatti è limitato alla visualizzazione dei soli nomi, ma non degli indirizzi email o di altre informazioni come i numeri di telefono. I loro nomi vengono poi disegnati sul mio biglietto di auguri.

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

API Asynchronous Clipboard

A seguire, la copia e l'incolla. Una delle nostre operazioni preferite come sviluppatori di software è il copia e incolla. In qualità di autore di biglietti di auguri, a volte potrei voler fare lo stesso. Potrei voler incollare un'immagine in un biglietto di auguri a cui sto lavorando o copiare il biglietto di auguri per continuare a modificarlo da un'altra posizione. L'API Async Clipboard supporta sia testo che immagini. Ti mostrerò come ho aggiunto il supporto per il copia e incolla all'app Fugu Greetings.

Per copiare qualcosa negli appunti di sistema, devo scriverci. Il metodo navigator.clipboard.write() accetta 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 scorrere gli elementi degli appunti che ottengo chiamando navigator.clipboard.read(). Il motivo è che potrebbero essere presenti più elementi 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);
  }
};

Ormai è quasi superfluo dirlo. Posso farlo 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 Anteprima di macOS 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 che mostra la richiesta di autorizzazione degli appunti.
Il prompt di autorizzazione di accesso agli appunti.

Infine, dopo aver accettato l'autorizzazione, l'immagine viene incollata nell'applicazione. Funziona anche al contrario. Fammi copiare un biglietto di auguri negli appunti. Quando apro Anteprima e faccio clic su File e poi su Nuovo da Appunti, il biglietto di auguri viene incollato 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.

API Badging

Un'altra API utile è l'API Badging. In quanto PWA installabile, Fugu Greetings ha ovviamente un'icona dell'app che gli utenti possono posizionare nel dock delle app o nella schermata Home. Un modo divertente per dimostrare l'API è utilizzarla in Fugu Greetings, come contatore di 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 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 solita.

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

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

I numeri da 1 a 7 disegnati sul biglietto di auguri, ognuno 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.

API Periodic Background Sync

Vuoi iniziare ogni giorno con qualcosa di nuovo? Una funzionalità interessante 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 passo consiste nel registrare un evento di sincronizzazione periodica nella registrazione del service worker. È in ascolto di un tag di sincronizzazione chiamato '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 con la funzione getImageOfTheDay() e il risultato viene propagato a tutti i client, in modo che possano aggiornare le loro tele 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 di un miglioramento progressivo, quindi il codice viene caricato solo quando l'API è supportata dal browser. Ciò vale sia per il codice client sia per il codice del service worker. Sui browser non supportati, nessuno dei due viene caricato. Nota come nel service worker, anziché un import() dinamico (che non è ancora supportato in un contesto 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, se premi il pulsante Sfondo, viene visualizzata l'immagine del biglietto di auguri del giorno, che viene aggiornata ogni giorno con l'API Periodic Background Sync.

Se premi il pulsante Sfondo, viene visualizzata l'immagine del giorno.

API Notification Triggers

A volte, anche con molta ispirazione, hai bisogno di un piccolo aiuto per finire un biglietto di auguri iniziato. Si tratta di una funzionalità abilitata dall'API Notification Triggers. Come utente, posso inserire un orario in cui voglio ricevere un promemoria per completare il mio biglietto di auguri. Quando arriverà il momento, riceverò una notifica che mi avvisa che il biglietto 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, senza bisogno di 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, mi viene chiesto quando voglio ricevere un promemoria per completare il biglietto di auguri.

L&#39;app Fugu Greetings con un messaggio che chiede all&#39;utente quando vuole ricevere un promemoria per completare il biglietto di auguri.
Pianificazione di una notifica locale per ricevere un promemoria per completare un biglietto di auguri.

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

La notifica attivata viene visualizzata nel Centro Notifiche di macOS.

API Wake Lock

Voglio includere anche l'API Wake Lock. A volte devi solo fissare lo schermo abbastanza a lungo finché l'ispirazione non ti bacia. Il peggio che può succedere è che lo schermo si spenga. L'API Wake Lock può impedirlo.

Il primo passaggio consiste nell'ottenere un wake lock con navigator.wakelock.request method(). Passo la stringa 'screen' per ottenere un wakelock dello schermo. Aggiungo poi un listener di eventi per ricevere una notifica quando il wake lock viene rilasciato. Ciò può verificarsi, ad esempio, quando cambia la visibilità della scheda. Se ciò accade, quando la scheda torna visibile, posso riacquisire il wake lock.

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, c'è una casella di controllo Insonnia che, se selezionata, mantiene lo schermo attivo.

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

API Idle Detection

A volte, anche se fissi lo schermo per ore, 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 il canvas. Questa API è protetta dall'autorizzazione per le notifiche, poiché molti casi d'uso di produzione del rilevamento dell'inattività sono correlati alle notifiche, ad esempio per inviare una notifica solo a un dispositivo che l'utente sta utilizzando attivamente.

Dopo aver verificato che l'autorizzazione per le notifiche sia concessa, istanzio il detector di inattività. Registro un listener di eventi che ascolta le modifiche dello stato di inattività, che include l'utente e lo stato dello schermo. L'utente può essere attivo o inattivo e lo schermo può essere sbloccato o bloccato. Se l'utente è inattivo, il canvas viene cancellato. Assegno al rilevatore di inattività 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, il canvas viene cancellato quando la casella di controllo Effimero è selezionata e l'utente è inattivo per troppo tempo.

L&#39;app Fugu Greetings con un canvas vuoto dopo che l&#39;utente è rimasto inattivo per troppo tempo.
Se la casella di controllo Temporaneo è selezionata e l'utente è inattivo da troppo tempo, il canvas viene cancellato.

Chiusura

Uff, che corsa. Tante API in una sola app di esempio. E ricorda, non faccio mai pagare all'utente il costo del download per una funzionalità non supportata dal browser. Utilizzando il 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 bundler per le app molto grandi.

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

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, migliorata progressivamente in base alle funzionalità del browser specifico. 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 Android Chrome, che mostra molte funzionalità disponibili.
Fugu Greetings in esecuzione su Safari desktop, che mostra meno funzionalità disponibili.
Fugu Greetings in esecuzione su Chrome desktop, che mostra molte funzionalità disponibili.

Puoi creare un fork di Fugu su GitHub.

Il team di Chromium sta lavorando duramente per migliorare la situazione per quanto riguarda le API Fugu avanzate. Applicando il miglioramento progressivo durante la creazione della mia app, mi assicuro che tutti ottengano un'esperienza di base buona e solida, ma che le persone che utilizzano browser che supportano più API della piattaforma web ottengano un'esperienza ancora migliore. Non vedo l'ora di scoprire come utilizzerai il miglioramento progressivo nelle tue app.

Ringraziamenti

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