Meningkatkan Progressive Web App Anda secara bertahap

Mem-build untuk browser modern dan meningkatkan secara bertahap seperti tahun 2003

Pada Maret 2003, Nick Finck dan Steve Champeon mengejutkan dunia desain web dengan konsep progressive enhancement, strategi untuk desain web yang menekankan pemuatan konten halaman web inti terlebih dahulu, lalu secara bertahap menambahkan lapisan presentasi dan fitur yang lebih nuansatu dan secara teknis ketat di atas konten. Sementara pada tahun 2003, progressive enhancement adalah tentang penggunaan—pada saat itu—fitur CSS modern, JavaScript yang tidak mengganggu, dan bahkan hanya Scalable Vector Graphics. Progressive enhancement pada tahun 2020 dan seterusnya adalah tentang penggunaan kemampuan browser modern.

Desain web inklusif untuk masa depan dengan progressive enhancement. Slide judul dari presentasi asli Finck dan Champeon.
Slide: Desain Web Inklusif untuk Masa Depan dengan Progressive Enhancement. (Sumber)

JavaScript Modern

Berbicara tentang JavaScript, situasi dukungan browser untuk fitur JavaScript core ES 2015 terbaru sangat bagus. Standar baru ini mencakup promise, modul, class, literal template, fungsi panah, let dan const, parameter default, generator, penetapan destrukturisasi, sisa dan penyebaran, Map/Set, WeakMap/WeakSet, dan banyak lagi. Semuanya didukung.

Tabel dukungan CanIUse untuk fitur ES6 yang menampilkan dukungan di semua browser utama.
Tabel dukungan browser ECMAScript 2015 (ES6). (Sumber)

Fungsi asinkron, fitur ES 2017 dan salah satu favorit saya secara pribadi, dapat digunakan di semua browser utama. Kata kunci async dan await memungkinkan perilaku asinkron berbasis promise ditulis dalam gaya yang lebih bersih, sehingga tidak perlu mengonfigurasi rantai promise secara eksplisit.

Tabel dukungan CanIUse untuk fungsi asinkron yang menampilkan dukungan di semua browser utama.
Tabel dukungan browser fungsi Asinkron. (Sumber)

Bahkan penambahan bahasa ES 2020 terbaru seperti chaining opsional dan nullish coalescing telah mendapatkan dukungan dengan sangat cepat. Anda dapat melihat contoh kode di bawah. Dalam hal fitur JavaScript inti, tidak ada yang lebih baik dari yang ada saat ini.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
Gambar latar rumput hijau Windows XP yang ikonik.
Semuanya tampak bagus jika menyangkut fitur JavaScript inti. (Screenshot produk Microsoft, digunakan dengan izin.)

Aplikasi contoh: Fugu Greetings

Untuk artikel ini, saya menggunakan PWA sederhana, yang disebut Fugu Greetings (GitHub). Nama aplikasi ini adalah penghargaan untuk Project Fugu 🐡, sebuah upaya untuk memberi web semua kemampuan aplikasi Android/iOS/desktop. Anda dapat membaca selengkapnya tentang project ini di halaman landing-nya.

Fugu Greetings adalah aplikasi gambar yang memungkinkan Anda membuat kartu ucapan virtual, dan mengirimnya kepada orang yang Anda cintai. Contoh ini menunjukkan konsep inti PWA. Aplikasi ini andal dan sepenuhnya diaktifkan secara offline, sehingga meskipun tidak memiliki jaringan, Anda tetap dapat menggunakannya. Aplikasi ini juga Dapat diinstal ke layar utama perangkat dan terintegrasi dengan lancar dengan sistem operasi sebagai aplikasi mandiri.

PWA Fugu Greetings dengan gambar yang menyerupai logo komunitas PWA.
Aplikasi contoh Fugu Greetings.

Peningkatan progresif

Setelah itu, saatnya membahas progressive enhancement. Glosarium MDN Web Docs menentukan konsep sebagai berikut:

Progressive enhancement adalah filosofi desain yang memberikan dasar konten dan fungsi penting kepada sebanyak mungkin pengguna, sekaligus memberikan pengalaman terbaik hanya kepada pengguna browser paling modern yang dapat menjalankan semua kode yang diperlukan.

Deteksi fitur umumnya digunakan untuk menentukan apakah browser dapat menangani fungsi yang lebih modern, sedangkan polyfill sering digunakan untuk menambahkan fitur yang tidak ada dengan JavaScript.

[…]

Peningkatan progresif adalah teknik yang berguna yang memungkinkan developer web berfokus pada pengembangan situs terbaik sekaligus membuat situs tersebut berfungsi di beberapa agen pengguna yang tidak dikenal. Degradasi halus terkait, tetapi tidak sama dan sering kali dianggap berlawanan dengan progressive enhancement. Pada kenyataannya, kedua pendekatan tersebut valid dan sering kali dapat saling melengkapi.

Kontributor MDN

Memulai setiap kartu ucapan dari awal bisa sangat merepotkan. Jadi, mengapa tidak memiliki fitur yang memungkinkan pengguna mengimpor gambar, dan memulai dari sana? Dengan pendekatan tradisional, Anda akan menggunakan elemen <input type=file> untuk mewujudkannya. Pertama, Anda akan membuat elemen, menetapkan type ke 'file', dan menambahkan jenis MIME ke properti accept, lalu "mengklik"nya secara terprogram dan memproses perubahan. Saat Anda memilih gambar, gambar tersebut akan langsung diimpor ke kanvas.

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

Jika ada fitur impor, mungkin harus ada fitur ekspor sehingga pengguna dapat menyimpan kartu ucapan secara lokal. Cara tradisional untuk menyimpan file adalah dengan membuat link anchor dengan atribut download dan dengan URL blob sebagai href-nya. Anda juga akan "mengklik"nya secara terprogram untuk memicu download, dan, untuk mencegah kebocoran memori, jangan lupa untuk mencabut URL objek 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();
};

Tapi tunggu sebentar. Secara mental, Anda belum "mendownload" kartu ucapan, Anda telah "menyimpan"nya. Daripada menampilkan dialog "simpan" yang memungkinkan Anda memilih tempat untuk menyimpan file, browser langsung mendownload kartu ucapan tanpa interaksi pengguna dan langsung menyimpannya ke folder Download. Ini tidak bagus.

Bagaimana jika ada cara yang lebih baik? Bagaimana jika Anda dapat membuka file lokal, mengeditnya, lalu menyimpan perubahan, baik ke file baru, atau kembali ke file asli yang awalnya Anda buka? Ternyata ada. File System Access API memungkinkan Anda membuka dan membuat file serta direktori, serta mengubah dan menyimpannya .

Jadi, bagaimana cara mendeteksi fitur API? File System Access API mengekspos metode baru window.chooseFileSystemEntries(). Akibatnya, saya perlu memuat modul impor dan ekspor yang berbeda secara kondisional, bergantung pada apakah metode ini tersedia. Kami telah menunjukkan cara melakukannya di bawah.

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

Namun, sebelum membahas detail File System Access API, izinkan saya menyoroti pola progressive enhancement di sini. Di browser yang saat ini tidak mendukung File System Access API, saya memuat skrip lama. Anda dapat melihat tab jaringan Firefox dan Safari di bawah.

Safari Web Inspector menampilkan file lama yang dimuat.
Tab jaringan Safari Web Inspector.
Alat Developer Firefox menampilkan file lama yang dimuat.
Tab jaringan Firefox Developer Tools.

Namun, di Chrome, browser yang mendukung API, hanya skrip baru yang dimuat. Hal ini dapat dilakukan dengan elegan berkat import() dinamis, yang didukung oleh semua browser modern. Seperti yang saya katakan sebelumnya, rumput saat ini cukup hijau.

Chrome DevTools menampilkan file modern yang dimuat.
Tab jaringan Chrome DevTools.

File System Access API

Jadi, setelah membahas hal ini, sekarang saatnya melihat implementasi sebenarnya berdasarkan File System Access API. Untuk mengimpor gambar, saya memanggil window.chooseFileSystemEntries() dan meneruskan properti accepts tempat saya menyatakan bahwa saya menginginkan file gambar. Ekstensi file dan jenis MIME didukung. Tindakan ini menghasilkan handle file, yang dapat saya gunakan untuk mendapatkan file sebenarnya dengan memanggil 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);
  }
};

Mengekspor gambar hampir sama, tetapi kali ini Saya harus meneruskan parameter jenis 'save-file' ke metode chooseFileSystemEntries(). Dari sini, saya mendapatkan dialog simpan file. Dengan file terbuka, hal ini tidak diperlukan karena 'open-file' adalah default. Saya menetapkan parameter accepts seperti sebelumnya, tetapi kali ini hanya terbatas pada gambar PNG. Sekali lagi, saya mendapatkan kembali handle file, tetapi bukan mendapatkan file, kali ini saya membuat aliran yang dapat ditulis dengan memanggil createWritable(). Selanjutnya, saya menulis blob, yang merupakan gambar kartu ucapan saya, ke file. Terakhir, saya menutup aliran data yang dapat ditulis.

Semuanya dapat gagal: Disk mungkin kehabisan ruang, mungkin ada error tulis atau baca, atau mungkin pengguna membatalkan dialog file. Itulah sebabnya saya selalu menggabungkan panggilan dalam pernyataan 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);
  }
};

Dengan menggunakan progressive enhancement dengan File System Access API, Saya dapat membuka file seperti sebelumnya. File yang diimpor akan langsung digambar ke kanvas. Saya dapat melakukan pengeditan dan akhirnya menyimpannya dengan kotak dialog simpan yang sebenarnya tempat saya dapat memilih nama dan lokasi penyimpanan file. Sekarang file siap disimpan selamanya.

Aplikasi Fugu Greetings dengan dialog buka file.
Dialog buka file.
Aplikasi Fugu Greetings kini dengan gambar yang diimpor.
Image yang diimpor.
Aplikasi Fugu Greetings dengan gambar yang diubah.
Menyimpan gambar yang diubah ke file baru.

Web Share dan Web Share Target API

Selain menyimpannya selamanya, mungkin saya ingin membagikan kartu ucapan saya. Hal ini dapat dilakukan dengan Web Share API dan Web Share Target API. Sistem operasi seluler, dan baru-baru ini desktop, telah mendapatkan mekanisme berbagi bawaan. Misalnya, di bawah ini adalah sheet berbagi Safari desktop di macOS yang dipicu dari artikel di blog saya. Saat mengklik tombol Bagikan Artikel, Anda dapat membagikan link ke artikel kepada teman, misalnya, melalui aplikasi Message macOS.

Sheet berbagi Safari desktop di macOS dipicu dari tombol Bagikan artikel
Web Share API di Safari desktop di macOS.

Kode untuk melakukannya cukup sederhana. Saya memanggil navigator.share() dan meneruskan title, text, dan url opsional dalam objek. Namun, bagaimana jika saya ingin melampirkan gambar? Level 1 Web Share API belum mendukungnya. Kabar baiknya, Berbagi Web Tingkat 2 telah menambahkan kemampuan berbagi 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);
}

Mari kita pelajari cara membuatnya berfungsi dengan aplikasi kartu Ucapan Fugu. Pertama, saya perlu menyiapkan objek data dengan array files yang terdiri dari satu blob, lalu title dan text. Selanjutnya, sebagai praktik terbaik, saya menggunakan metode navigator.canShare() baru yang melakukan apa yang disarankan namanya: Metode ini memberi tahu saya apakah objek data yang saya coba bagikan secara teknis dapat dibagikan oleh browser. Jika navigator.canShare() memberi tahu saya bahwa data dapat dibagikan, saya siap memanggil navigator.share() seperti sebelumnya. Karena semuanya dapat gagal, saya kembali menggunakan blok 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);
  }
};

Seperti sebelumnya, saya menggunakan progressive enhancement. Jika 'share' dan 'canShare' ada di objek navigator, barulah saya melanjutkan dan memuat share.mjs melalui import() dinamis. Di browser seperti Safari seluler yang hanya memenuhi salah satu dari dua kondisi, saya tidak memuat fungsi tersebut.

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

Di Fugu Greetings, jika saya mengetuk tombol Bagikan di browser pendukung seperti Chrome di Android, lembar berbagi bawaan akan terbuka. Misalnya, saya dapat memilih Gmail, dan widget penulis email akan muncul dengan gambar yang dilampirkan.

Sheet berbagi tingkat OS yang menampilkan berbagai aplikasi untuk berbagi gambar.
Memilih aplikasi untuk berbagi file.
Widget tulis email Gmail dengan gambar terlampir.
File akan dilampirkan ke email baru di editor Gmail.

Contact Picker API

Selanjutnya, saya ingin membahas kontak, yang berarti buku alamat atau aplikasi pengelola kontak perangkat. Saat Anda menulis kartu ucapan, mungkin tidak selalu mudah untuk menulis nama seseorang dengan benar. Misalnya, saya memiliki teman bernama Sergey yang lebih suka namanya dieja dalam huruf Sirilik. Saya menggunakan keyboard QWERTZ Jerman dan tidak tahu cara mengetik namanya. Ini adalah masalah yang dapat dipecahkan oleh Contact Picker API. Karena saya menyimpan teman saya di aplikasi kontak ponsel, melalui Contacts Picker API, saya dapat mengakses kontak saya dari web.

Pertama, saya perlu menentukan daftar properti yang ingin diakses. Dalam hal ini, saya hanya menginginkan nama, tetapi untuk kasus penggunaan lainnya, saya mungkin tertarik dengan nomor telepon, email, ikon avatar, atau alamat fisik. Selanjutnya, saya mengonfigurasi objek options dan menetapkan multiple ke true, sehingga saya dapat memilih lebih dari satu entri. Terakhir, saya dapat memanggil navigator.contacts.select(), yang menampilkan properti yang diinginkan untuk kontak yang dipilih pengguna.

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

Dan sekarang Anda mungkin telah mempelajari polanya: Saya hanya memuat file saat API benar-benar didukung.

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

Di Fugu Greeting, saat saya mengetuk tombol Kontak dan memilih dua sahabat saya, Сергей Михайлович Брин dan 劳伦斯·爱德华·"拉里"·佩奇, Anda dapat melihat cara pemilih kontak dibatasi untuk hanya menampilkan nama mereka, tetapi tidak menampilkan alamat email mereka, atau informasi lain seperti nomor telepon mereka. Nama mereka kemudian digambar di kartu ucapan saya.

Pemilih kontak yang menampilkan nama dua kontak di buku alamat.
Memilih dua nama dengan pemilih kontak dari buku alamat.
Nama dua kontak yang dipilih sebelumnya yang digambar di kartu ucapan.
Kedua nama tersebut kemudian digambar ke kartu ucapan.

Asynchronous Clipboard API

Selanjutnya adalah menyalin dan menempel. Salah satu operasi favorit kami sebagai developer software adalah salin dan tempel. Sebagai penulis kartu ucapan, terkadang saya ingin melakukan hal yang sama. Saya mungkin ingin menempelkan gambar ke kartu ucapan yang sedang saya kerjakan, atau menyalin kartu ucapan agar dapat terus mengeditnya dari tempat lain. Async Clipboard API, mendukung teks dan gambar. Izinkan saya menjelaskan cara menambahkan dukungan salin dan tempel ke aplikasi Fugu Greetings.

Untuk menyalin sesuatu ke papan klip sistem, saya harus menulis ke papan klip tersebut. Metode navigator.clipboard.write() menggunakan array item papan klip sebagai parameter. Setiap item papan klip pada dasarnya adalah objek dengan blob sebagai nilai, dan jenis blob sebagai kunci.

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

Untuk menempelkan, saya perlu melakukan loop pada item papan klip yang saya dapatkan dengan memanggil navigator.clipboard.read(). Alasannya adalah beberapa item papan klip mungkin ada di papan klip dalam representasi yang berbeda. Setiap item papan klip memiliki kolom types yang memberi tahu saya jenis MIME resource yang tersedia. Saya memanggil metode getType() item papan klip, dengan meneruskan jenis MIME yang saya peroleh sebelumnya.

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

Dan sekarang sudah hampir tidak perlu dikatakan lagi. Saya hanya melakukannya di browser yang mendukung.

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

Jadi, bagaimana cara kerjanya dalam praktik? Saya membuka gambar di aplikasi Pratinjau macOS dan menyalinnya ke papan klip. Saat saya mengklik Tempel, aplikasi Fugu Greetings akan menanyakan apakah saya ingin mengizinkan aplikasi melihat teks dan gambar di papan klip.

Aplikasi Fugu Greetings yang menampilkan permintaan izin papan klip.
Perintah izin papan klip.

Terakhir, setelah menerima izin, gambar akan ditempelkan ke dalam aplikasi. Cara sebaliknya juga berfungsi. Izinkan saya menyalin kartu ucapan ke papan klip. Saat saya membuka Pratinjau dan mengklik File, lalu New from Clipboard, kartu ucapan akan ditempelkan ke gambar baru tanpa judul.

Aplikasi Pratinjau macOS dengan gambar tanpa judul yang baru saja ditempel.
Gambar yang disisipkan ke aplikasi Pratinjau macOS.

Badging API

API lain yang berguna adalah Badging API. Sebagai PWA yang dapat diinstal, Fugu Greetings tentu saja memiliki ikon aplikasi yang dapat ditempatkan pengguna di dok aplikasi atau layar utama. Cara yang menyenangkan dan mudah untuk mendemonstrasikan API adalah dengan (salah) menggunakannya di Fugu Greetings sebagai penghitung goresan pena. Saya telah menambahkan pemroses peristiwa yang menambahkan penghitung goresan pena setiap kali peristiwa pointerdown terjadi, lalu menetapkan badge ikon yang diperbarui. Setiap kali kanvas dihapus, penghitung akan direset, dan badge akan dihapus.

let strokes = 0;

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

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

Fitur ini adalah peningkatan progresif, sehingga logika pemuatan seperti biasa.

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

Dalam contoh ini, saya telah menggambar angka dari satu hingga tujuh, menggunakan satu goresan pena per angka. Penghitung badge di ikon kini berjumlah tujuh.

Angka dari satu hingga tujuh digambar ke kartu ucapan, masing-masing hanya dengan satu goresan pena.
Menggambar angka dari 1 hingga 7, menggunakan tujuh goresan pena.
Ikon badge di aplikasi Fugu Greetings yang menampilkan angka 7.
Pembilang goresan pena dalam bentuk badge ikon aplikasi.

Periodic Background Sync API

Ingin memulai setiap hari dengan sesuatu yang baru? Fitur menarik dari aplikasi Fugu Greetings adalah aplikasi ini dapat menginspirasi Anda setiap pagi dengan gambar latar baru untuk memulai kartu ucapan. Aplikasi menggunakan Periodic Background Sync API untuk mencapai hal ini.

Langkah pertama adalah register peristiwa sinkronisasi berkala dalam pendaftaran pekerja layanan. Fungsi ini memproses tag sinkronisasi yang disebut 'image-of-the-day' dan memiliki interval minimum satu hari, sehingga pengguna bisa mendapatkan gambar latar baru setiap 24 jam.

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

Langkah kedua adalah memproses peristiwa periodicsync di pekerja layanan. Jika tag peristiwa adalah 'image-of-the-day', yaitu tag yang terdaftar sebelumnya, gambar hari diambil melalui fungsi getImageOfTheDay(), dan hasilnya disebarkan ke semua klien, sehingga mereka dapat memperbarui kanvas dan 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,
          });
        });
      })()
    );
  }
});

Sekali lagi, ini benar-benar merupakan peningkatan progresif, sehingga kode hanya dimuat saat API didukung oleh browser. Hal ini berlaku untuk kode klien dan kode pekerja layanan. Di browser yang tidak mendukung, keduanya tidak dimuat. Perhatikan bagaimana di pekerja layanan, bukan import() dinamis (yang belum didukung dalam konteks pekerja layanan), saya menggunakan importScripts() klasik.

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

Di Fugu Greetings, menekan tombol Wallpaper akan menampilkan gambar kartu ucapan hari ini yang diperbarui setiap hari melalui Periodic Background Sync API.

Aplikasi Fugu Greetings dengan gambar kartu ucapan baru hari ini.
Menekan tombol Wallpaper akan menampilkan gambar hari ini.

Notification Triggers API

Terkadang, meskipun memiliki banyak inspirasi, Anda memerlukan dorongan untuk menyelesaikan kartu ucapan yang telah dimulai. Ini adalah fitur yang diaktifkan oleh Notification Triggers API. Sebagai pengguna, saya dapat memasukkan waktu yang saya inginkan untuk mendapatkan notifikasi agar menyelesaikan kartu ucapan. Saat tiba waktunya, saya akan mendapatkan notifikasi bahwa kartu ucapan saya sudah siap.

Setelah meminta waktu target, aplikasi menjadwalkan notifikasi dengan showTrigger. Ini dapat berupa TimestampTrigger dengan tanggal target yang dipilih sebelumnya. Notifikasi pengingat akan dipicu secara lokal, tidak diperlukan sisi jaringan atau 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),
  });
}

Seperti semua hal yang telah saya tunjukkan sejauh ini, ini adalah peningkatan progresif, sehingga kode hanya dimuat secara kondisional.

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

Saat saya mencentang kotak Pengingat di Fugu Greetings, perintah akan menanyakan kapan saya ingin diingatkan untuk menyelesaikan kartu ucapan.

Aplikasi Fugu Greetings dengan perintah yang meminta pengguna kapan mereka ingin diingatkan untuk menyelesaikan kartu ucapan mereka.
Menjadwalkan notifikasi lokal untuk mendapatkan pengingat guna menyelesaikan kartu ucapan.

Saat notifikasi terjadwal dipicu di Fugu Greetings, notifikasi akan ditampilkan seperti notifikasi lainnya, tetapi seperti yang saya tulis sebelumnya, notifikasi tidak memerlukan koneksi jaringan.

Pusat Notifikasi macOS menampilkan notifikasi yang dipicu dari Fugu Greetings.
Notifikasi yang dipicu akan muncul di Pusat Notifikasi macOS.

Wake Lock API

Saya juga ingin menyertakan Wake Lock API. Terkadang Anda hanya perlu menatap layar cukup lama hingga inspirasi datang. Hal terburuk yang dapat terjadi adalah layar akan mati. Wake Lock API dapat mencegah hal ini terjadi.

Langkah pertama adalah mendapatkan kunci layar aktif dengan navigator.wakelock.request method(). Saya meneruskan string 'screen' untuk mendapatkan kunci layar saat aktif. Kemudian, saya menambahkan pemroses peristiwa untuk diberi tahu saat kunci layar aktif dilepaskan. Hal ini dapat terjadi, misalnya, saat visibilitas tab berubah. Jika hal ini terjadi, saya dapat, saat tab terlihat lagi, mendapatkan kembali kunci layar aktif.

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

Ya, ini adalah peningkatan progresif, jadi saya hanya perlu memuat API saat browser mendukung API.

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

Di Fugu Greetings, ada kotak centang Insomnia yang, jika dicentang, akan membuat layar tetap aktif.

Centang kotak insomnia, jika dicentang, akan membuat layar tetap aktif.
Centang Insomnia untuk membuat aplikasi tetap aktif.

Idle Detection API

Terkadang, meskipun Anda menatap layar selama berjam-jam, hal itu tidak berguna dan Anda tidak dapat menemukan ide sedikit pun tentang apa yang harus dilakukan dengan kartu ucapan Anda. Idle Detection API memungkinkan aplikasi mendeteksi waktu tidak ada aktivitas pengguna. Jika pengguna tidak ada aktivitas terlalu lama, aplikasi akan direset ke status awal dan menghapus kanvas. API ini saat ini dibatasi oleh izin notifikasi, karena banyak kasus penggunaan produksi deteksi tidak ada aktivitas yang terkait dengan notifikasi, misalnya, hanya mengirim notifikasi ke perangkat yang saat ini digunakan secara aktif oleh pengguna.

Setelah memastikan bahwa izin notifikasi diberikan, saya kemudian membuat instance detector tidak ada aktivitas. Saya mendaftarkan pemroses peristiwa yang memproses perubahan tidak ada aktivitas, yang mencakup pengguna dan status layar. Pengguna dapat aktif atau tidak ada aktivitas, dan layar dapat dibuka kuncinya atau dikunci. Jika pengguna tidak ada aktivitas, kanvas akan dihapus. Saya memberikan batas waktu 60 detik ke detektor tidak ada aktivitas.

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

Dan seperti biasa, saya hanya memuat kode ini jika browser mendukungnya.

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

Di aplikasi Fugu Greetings, kanvas akan dihapus saat kotak centang Ephemeral dicentang dan pengguna tidak ada aktivitas selama terlalu lama.

Aplikasi Fugu Greetings dengan kanvas yang dihapus setelah pengguna tidak ada aktivitas terlalu lama.
Jika kotak centang Ephemeral dicentang dan pengguna tidak ada aktivitas selama terlalu lama, kanvas akan dihapus.

Penutup

Fiuh, perjalanan yang luar biasa. Ada begitu banyak API hanya dalam satu aplikasi contoh. Dan, ingat, saya tidak pernah meminta pengguna membayar biaya download untuk fitur yang tidak didukung browser mereka. Dengan menggunakan progressive enhancement, saya memastikan hanya kode yang relevan yang dimuat. Selain itu, karena dengan HTTP/2, permintaan menjadi murah, pola ini akan berfungsi dengan baik untuk banyak aplikasi, meskipun Anda mungkin ingin mempertimbangkan bundler untuk aplikasi yang sangat besar.

Panel Jaringan Chrome DevTools yang hanya menampilkan permintaan untuk file dengan kode yang didukung browser saat ini.
Tab Jaringan Chrome DevTools yang hanya menampilkan permintaan untuk file dengan kode yang didukung browser saat ini.

Aplikasi mungkin terlihat sedikit berbeda di setiap browser karena tidak semua platform mendukung semua fitur, tetapi fungsi intinya selalu ada—yang ditingkatkan secara progresif sesuai dengan kemampuan browser tertentu. Perhatikan bahwa kemampuan ini dapat berubah bahkan di satu browser yang sama, bergantung pada apakah aplikasi berjalan sebagai aplikasi yang diinstal atau di tab browser.

Fugu Greetings yang berjalan di Chrome Android, menampilkan banyak fitur yang tersedia.
Fugu Greetings yang berjalan di Chrome Android.
Fugu Greetings berjalan di Safari desktop, yang menampilkan lebih sedikit fitur yang tersedia.
Fugu Greetings berjalan di Safari desktop.
Fugu Greetings yang berjalan di Chrome desktop, menampilkan banyak fitur yang tersedia.
Fugu Greetings berjalan di Chrome desktop.

Jika Anda tertarik dengan aplikasi Fugu Greetings, cari dan fork di GitHub.

Repo Fugu Greetings di GitHub.
Aplikasi Fugu Greetings di GitHub.

Tim Chromium bekerja keras untuk meningkatkan kualitas Fugu API lanjutan. Dengan menerapkan progressive enhancement dalam pengembangan aplikasi, saya memastikan bahwa semua orang mendapatkan pengalaman dasar yang baik dan solid, tetapi orang yang menggunakan browser yang mendukung lebih banyak API platform Web mendapatkan pengalaman yang lebih baik. Kami menantikan apa yang Anda lakukan dengan progressive enhancement di aplikasi Anda.

Ucapan terima kasih

Saya berterima kasih kepada Christian Liebel dan Hemanth HM yang telah berkontribusi pada Fugu Greetings. Artikel ini ditinjau oleh Joe Medley dan Kayce Basques. Jake Archibald membantu saya mengetahui situasi dengan import() dinamis dalam konteks pekerja layanan.