Progresif Web Uygulamanızı kademeli olarak iyileştirin

Modern tarayıcılar için geliştirme ve 2003'teki gibi aşamalı olarak iyileştirme

Yayınlanma tarihi: 29 Haziran 2020

2003'ün Mart ayında Nick Finck ve Steve Champeon, web tasarım dünyasını aşamalı geliştirme kavramıyla şaşırtmıştı. Bu kavram, web tasarımında önce temel web sayfası içeriğinin yüklenmesini, ardından içeriğin üzerine daha ayrıntılı ve teknik açıdan titiz sunum katmanları ve özellikler eklenmesini vurgulayan bir stratejidir. 2003'te ise aşamalı geliştirme, o dönemde modern olan CSS özelliklerini, göze batmayan JavaScript'i ve hatta yalnızca ölçeklenebilir vektör grafiklerini kullanmakla ilgiliydi. 2020 ve sonrasında progresif geliştirme, modern tarayıcı özelliklerini kullanmakla ilgilidir.

Kademeli iyileştirme ile geleceğe yönelik kapsayıcı web tasarımı. Finck ve Champeon'ın orijinal sunusundan başlık slaydı.

Modern JavaScript

JavaScript'ten bahsetmişken, en yeni temel ES 2015 JavaScript özelliklerinin tarayıcı desteği durumu mükemmel. Yeni standart; sözler, modüller, sınıflar, şablon değişmezleri, ok işlevleri, let ve const, varsayılan parametreler, oluşturucular, yapı bozma ataması, rest ve spread, Map/Set, WeakMap/WeakSet ve daha birçok özelliği içerir. Tümü desteklenir.

Tüm büyük tarayıcılarda desteği gösteren ES6 özellikleri için CanIUse destek tablosu.
ECMAScript 2015 (ES6) tarayıcı desteği tablosu. (Kaynak)

ES 2017 özelliği olan ve benim de en sevdiğim özelliklerden biri olan asenkron işlevler, tüm büyük tarayıcılarda kullanılabilir. async ve await anahtar kelimeleri, söz dizimi zincirlerini açıkça yapılandırma ihtiyacını ortadan kaldırarak söz dizimine dayalı eşzamansız davranışın daha temiz bir tarzda yazılmasını sağlar.

Tüm büyük tarayıcılarda desteği gösteren, CanIUse'un asenkron işlevler için destek tablosu.
Eşzamansız işlevlerin tarayıcı desteği tablosu. (Kaynak)

Hatta isteğe bağlı zincirleme ve nullish coalescing gibi ES 2020 diline çok yakın zamanda eklenen özellikler bile çok hızlı bir şekilde desteklenmeye başladı. Temel JavaScript özellikleri açısından daha iyi bir seçenek bulmak zordur.

Örneğin:

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
İkonik Windows XP yeşil çimen arka plan resmi.
Temel JavaScript özellikleri söz konusu olduğunda her şey yolundadır. (Microsoft ürününün ekran görüntüsü, izinle kullanılmıştır.)

Örnek uygulama: Fugu Greetings

Bu dokümanda, Fugu Greetings adlı bir PWA ile çalışıyorum (GitHub). Bu uygulamanın adı, web'e Android, iOS ve masaüstü uygulamalarının tüm özelliklerini kazandırmayı amaçlayan Project Fugu 🐡'ya bir saygı duruşudur. Proje hakkında daha fazla bilgiyi açılış sayfasında bulabilirsiniz.

Fugu Greetings, sanal tebrik kartları oluşturup sevdiklerinize göndermenize olanak tanıyan bir çizim uygulamasıdır. PWA'nın temel kavramlarını örnekler. Güvenilir ve tamamen çevrimdışı olarak kullanılabilir. Bu nedenle, ağ bağlantınız olmasa bile kullanmaya devam edebilirsiniz. Ayrıca, cihazın ana ekranına yüklenebilir ve işletim sistemiyle bağımsız bir uygulama olarak sorunsuz şekilde entegre olur.

PWA topluluk logosuna benzeyen bir çizimin yer aldığı Fugu Greetings PWA'sı.
Fugu Greetings örnek uygulaması.

Progresif geliştirme

Bu konuyu ele aldığımıza göre şimdi kademeli iyileştirme hakkında konuşmanın zamanı geldi. MDN Web Docs Sözlüğü, kavramı aşağıdaki şekilde tanımlar:

Aşamalı geliştirme, mümkün olduğunca çok sayıda kullanıcıya temel içerik ve işlevsellik sunan bir tasarım felsefesidir. Bu felsefe, yalnızca gerekli tüm kodu çalıştırabilen en modern tarayıcıların kullanıcılarına mümkün olan en iyi deneyimi sunar.

Özellik algılama genellikle tarayıcıların daha modern işlevleri işleyip işleyemeyeceğini belirlemek için kullanılırken polyfill'ler genellikle JavaScript ile eksik özellikleri eklemek için kullanılır.

[…]

Aşamalı geliştirme, web geliştiricilerin mümkün olan en iyi web sitelerini geliştirmeye odaklanmasına olanak tanıyan ve bu web sitelerinin birden fazla bilinmeyen kullanıcı aracısında çalışmasını sağlayan faydalı bir tekniktir. Graceful degradation (zarif bozulma), ilgili bir kavram olsa da aynı şey değildir ve genellikle progressive enhancement'ın (ilerleyici geliştirme) tersi yönde ilerlediği düşünülür. Aslında her iki yaklaşım da geçerlidir ve genellikle birbirini tamamlayabilir.

MDN katkıda bulunanları

Her tebrik kartını baştan oluşturmak gerçekten zahmetli olabilir. O zaman neden kullanıcıların resim içe aktarmasına ve oradan başlamasına olanak tanıyan bir özellik olmasın? Geleneksel bir yaklaşımla, bunu gerçekleştirmek için <input type=file> öğesini kullanırdınız. Öncelikle öğeyi oluşturur, type özelliğini 'file' olarak ayarlayıp accept özelliğine MIME türleri ekler, ardından programatik olarak "tıklayıp" değişiklikleri dinlersiniz. Seçtiğiniz resim doğrudan tuvale aktarılır.

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

İçe aktarma özelliği varsa kullanıcıların tebrik kartlarını yerel olarak kaydedebilmesi için muhtemelen bir dışa aktarma özelliği de olmalıdır. Dosyaları kaydetmenin geleneksel yolu, download özelliğiyle ve href olarak bir blob URL'siyle bir bağlantı oluşturmaktır. Ayrıca, indirmeyi tetiklemek için programatik olarak "tıklarsınız " ve bellek sızıntılarını önlemek için blob nesnesi URL'sini iptal etmeyi unutmazsınız.

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

Ama bir dakika bekleyin. Zihinsel olarak, tebrik kartını "indirmez", "kaydedersiniz". Tarayıcı, dosyayı nereye koyacağınızı seçmenize olanak tanıyan bir "kaydet" iletişim kutusu göstermek yerine, tebrik kartını kullanıcı etkileşimi olmadan doğrudan indirip İndirilenler klasörünüze yerleştirmiştir. Bu iyi bir durum değil.

Daha iyi bir yol olsaydı? Yerel bir dosyayı açıp düzenledikten sonra değişiklikleri yeni bir dosyaya veya başlangıçta açtığınız orijinal dosyaya kaydetseydiniz nasıl olurdu? Evet, var. File System Access API, dosya ve dizinleri açıp oluşturmanıza, ayrıca bunları değiştirip kaydetmenize olanak tanır .

Peki bir API'nin özelliklerini nasıl tespit edebilirim? File System Access API, window.chooseFileSystemEntries() adlı yeni bir yöntemi kullanıma sunar. Bu nedenle, bu yöntemin kullanılabilir olup olmamasına bağlı olarak farklı içe ve dışa aktarma modüllerini koşullu olarak yüklemem gerekiyor.

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

Ancak File System Access API'nin ayrıntılarına girmeden önce, buradaki aşamalı geliştirme modelini kısaca vurgulamak istiyorum. File System Access API'yi desteklemeyen tarayıcılarda eski komut dosyalarını yüklüyorum.

Eski dosyaların yüklendiğini gösteren Safari Web Inspector.
Yüklenen eski dosyaları gösteren Firefox Geliştirici Araçları.

Ancak API'yi destekleyen bir tarayıcı olan Chrome'da yalnızca yeni komut dosyaları yüklenir. Bu, tüm modern tarayıcıların desteklediğiimport() dinamik sayesinde zarif bir şekilde mümkün olur. Daha önce de söylediğim gibi, çimler bu günlerde oldukça yeşil.

Chrome Geliştirici Araçları&#39;nda modern dosyaların yüklendiği gösteriliyor.
Chrome Geliştirici Araçları ağ sekmesi.

File System Access API

Bu konuyu ele aldığımıza göre artık File System Access API'ye dayalı gerçek uygulamaya bakmanın zamanı geldi. Bir resmi içe aktarmak için window.chooseFileSystemEntries() işlevini çağırıyorum ve resim dosyaları istediğimi belirttiğim bir accepts özelliği iletiyorum. Hem dosya uzantıları hem de MIME türleri desteklenir. Bu işlem, getFile() çağrılarak gerçek dosyanın alınabileceği bir dosya tutacağıyla sonuçlanır.

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

Resim dışa aktarma işlemi neredeyse aynıdır ancak bu kez 'save-file' tür parametresini chooseFileSystemEntries() yöntemine iletmem gerekir. Bu işlem sonucunda dosya kaydetme iletişim kutusu açılıyor. Dosya açıkken 'open-file' varsayılan olduğu için bu gerekli değildi. accepts parametresini önceki gibi ayarladım ancak bu kez yalnızca PNG resimlerle sınırlı. Yine bir dosya tutma yeri alıyorum ancak bu kez dosyayı almak yerine createWritable() çağrısı yaparak yazılabilir bir akış oluşturuyorum. Ardından, dosya için tebrik kartı resmim olan blob'u yazıyorum. Son olarak, yazılabilir akışı kapatıyorum.

Her zaman her şey başarısız olabilir: Diskte yer kalmamış olabilir, yazma veya okuma hatası olabilir ya da kullanıcı dosya iletişim kutusunu iptal edebilir. Bu nedenle, aramaları her zaman bir try...catch ifadesiyle sarmalarım.

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

File System Access API ile aşamalı iyileştirme kullanarak, dosyaları eskisi gibi açabiliyorum. İçe aktarılan dosya doğrudan tuval üzerine çizilir. Düzenlemelerimi yapıp dosyayı adını ve depolama konumunu seçebileceğim gerçek bir kaydetme iletişim kutusuyla kaydedebiliyorum. Dosya artık sonsuza kadar saklanmaya hazır.

Dosya açma iletişim kutusu içeren Fugu Greetings uygulaması.
Dosya açma iletişim kutusu.
Fugu Greetings uygulaması artık içe aktarılmış bir resimle birlikte.
İçe aktarılan resim.
Değiştirilmiş resmin bulunduğu Fugu Greetings uygulaması.
Değiştirilen görüntüyü yeni bir dosyaya kaydetme.

Web Share ve Web Share Target API'leri

attempt-right

Sonsuza kadar saklamanın yanı sıra tebrik kartımı paylaşmak da isteyebilirim. Bu, Web Share API ve Web Share Target API'nin yapmama olanak tanıdığı bir şeydir. Mobil ve daha yakın zamanda masaüstü işletim sistemleri, yerleşik paylaşım mekanizmalarına sahip oldu.

Örneğin, macOS'teki masaüstü Safari'nin paylaşım sayfası, kullanıcı blogumda Makaleyi Paylaş'ı tıkladığında tetiklenir. macOS Mesajlar uygulamasını kullanarak makalenin bağlantısını bir arkadaşınızla paylaşabilirsiniz.

Bunu yapmak için navigator.share() işlevini çağırıyorum ve isteğe bağlı title, text ve url değerlerini bir nesne içinde iletiyorum. Peki resim eklemek istersem ne olur? Web Share API'nin 1. düzeyi henüz bunu desteklemiyor. Web Share Level 2, dosya paylaşımı özelliklerini ekledi.

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

Bu özelliği Fugu Greeting Card uygulamasıyla nasıl kullanacağınızı göstereyim. Öncelikle, bir blob'dan oluşan bir files dizisi içeren bir data nesnesi, ardından bir title ve bir text hazırlamam gerekiyor. Ardından, en iyi uygulama olarak, adının da belirttiği gibi çalışan yeni navigator.canShare() yöntemini kullanıyorum: Bu yöntem, paylaşmaya çalıştığım data nesnesinin tarayıcı tarafından teknik olarak paylaşılıp paylaşılamayacağını bana bildiriyor. navigator.canShare(), verilerin paylaşılabileceğini söylerse navigator.share()'ı eskisi gibi aramaya hazırım. Her şey başarısız olabileceği için tekrar try...catch bloğunu kullanıyorum.

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

Daha önce olduğu gibi, progresif geliştirme kullanıyorum. Yalnızca 'share' ve 'canShare' özellikleri navigator nesnesinde mevcutsa devam edip share.mjs özelliğini dinamik import() kullanarak yüklerim. İki koşuldan yalnızca birini karşılayan mobil Safari gibi tarayıcılarda işlevi yüklemiyorum.

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

Fugu Greetings'de, Android'de Chrome gibi desteklenen bir tarayıcıda Paylaş düğmesine dokunduğumda yerleşik paylaşım sayfası açılıyor. Örneğin, Gmail'i seçebilirim ve e-posta oluşturma widget'ı, eklenmiş resimle birlikte açılır.

Resmin paylaşılabileceği çeşitli uygulamaların gösterildiği işletim sistemi düzeyinde paylaşım sayfası.
Dosyanın paylaşılacağı uygulamayı seçme.
Resmin eklendiği Gmail&#39;in e-posta oluşturma widget&#39;ı.
Dosya, Gmail'in e-posta oluşturma bölümünde yeni bir e-postaya eklenir.

Contact Picker API

Ardından, cihazın adres defteri veya kişi yöneticisi uygulaması anlamına gelen kişiler hakkında konuşmak istiyorum. Bir tebrik kartı yazarken birinin adını doğru yazmak her zaman kolay olmayabilir. Örneğin, arkadaşım Sergey adının Kiril alfabesiyle yazılmasını tercih ediyor. Almanca QWERTZ klavye kullanıyorum ve adını nasıl yazacağımı bilmiyorum. Bu, Kişi Seçici API'nin çözebileceği bir sorundur. Arkadaşım telefonumun Kişiler uygulamasında kayıtlı olduğundan Kişiler Seçici API'yi kullanarak web'den kişilere erişebilirim.

Öncelikle, erişmek istediğim mülklerin listesini belirtmem gerekiyor. Bu durumda yalnızca adları istiyorum ancak diğer kullanım alanlarında telefon numaraları, e-postalar, avatar simgeleri veya fiziksel adreslerle ilgilenebilirim. Ardından, options nesnesini yapılandırıp multiple değerini true olarak ayarlıyorum. Böylece birden fazla giriş seçebiliyorum. Son olarak, kullanıcı tarafından seçilen kişiler için ideal özellikleri döndüren navigator.contacts.select() işlevini çağırabilirim.

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

Bu noktada, muhtemelen şu kalıbı öğrenmişsinizdir: Dosyayı yalnızca API gerçekten desteklendiğinde yüklerim.

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

Fugu Greeting'de Kişiler düğmesine dokunup iki en iyi arkadaşımı, Сергей Михайлович Брин ve 劳伦斯·爱德华·"拉里"·佩奇'i seçtiğimde, kişi seçicinin yalnızca bu kişilerin adlarını gösterip e-posta adreslerini veya telefon numaraları gibi diğer bilgileri göstermediğini görebilirsiniz. Daha sonra bu isimler tebrik kartıma çizilir.

Adres defterindeki iki kişinin adını gösteren kişi seçici.
Adres defterinden kişi seçiciyle iki ad seçme.
Tebrik kartında, daha önce seçilen iki kişinin adı yer alır.
İki ad, tebrik kartına çizilir.

Eşzamansız Pano API'si

Sıradaki konu kopyalama ve yapıştırma. Yazılım geliştiriciler olarak en sevdiğimiz işlemlerden biri kopyalama ve yapıştırmadır. Tebrik kartı yazarı olarak bazen ben de aynısını yapmak isteyebilirim. Üzerinde çalıştığım bir tebrik kartına resim yapıştırmak veya tebrik kartımı kopyalamak isteyebilirim. Böylece kartı başka bir yerden düzenlemeye devam edebilirim. Async Clipboard API, hem metni hem de görüntüleri destekler. Fugu Greetings uygulamasına kopyalama ve yapıştırma desteğini nasıl eklediğimi adım adım açıklayacağım.

Bir şeyi sistemin panosuna kopyalamak için panoya yazmam gerekir. navigator.clipboard.write() yöntemi, pano öğeleri dizisini parametre olarak alır. Her pano öğesi, değeri blob olan ve anahtarı blob'un türü olan bir nesnedir.

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

Yapıştırmak için navigator.clipboard.read() çağırarak aldığım pano öğelerini döngüye almam gerekiyor. Bunun nedeni, panoda farklı temsillerde birden fazla pano öğesi bulunabilmesidir. Her pano öğesinde, mevcut kaynakların MIME türlerini belirten bir types alanı var. Daha önce aldığım MIME türünü ileterek pano öğesinin getType() yöntemini çağırıyorum.

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

Bunu söylemeye neredeyse gerek bile yok. Bu işlemi yalnızca desteklenen tarayıcılarda yapıyorum.

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

Peki, bu özellik uygulamada nasıl çalışır? macOS Önizleme uygulamasında açık olan bir resmi panoya kopyalıyorum. Yapıştır'ı tıkladığımda Fugu Greetings uygulaması, uygulamaya panodaki metin ve resimleri görme izni vermek isteyip istemediğimi soruyor.

Pano izni istemini gösteren Fugu Greetings uygulaması.
Pano izni istemi.

Son olarak, izin kabul edildikten sonra resim uygulamaya yapıştırılır. Bu sürecin tersi de geçerlidir. Tebrik kartını panoya kopyalamama izin ver. Ardından Önizleme'yi açıp Dosya'yı ve Pano İçeriğinden Yeni'yi tıkladığımda tebrik kartı, yeni ve adsız bir resme yapıştırılıyor.

macOS&#39;teki Önizleme uygulamasında, başlığı olmayan ve yeni yapıştırılmış bir resim.
macOS Önizleme uygulamasına yapıştırılan bir resim.

Badging API

Yararlı bir diğer API ise Badging API'dir. Kurulabilir bir PWA olan Fugu Greetings'in, kullanıcıların uygulama dock'una veya ana ekrana yerleştirebileceği bir uygulama simgesi vardır. API'yi eğlenceli bir şekilde göstermek için Fugu Greetings'de kalem vuruşu sayacı olarak kullanabilirsiniz. pointerdown etkinliği her gerçekleştiğinde kalem vuruşları sayacını artıran ve ardından güncellenen simge rozetini ayarlayan bir etkinlik işleyici ekledim. Tuval her temizlendiğinde sayaç sıfırlanır ve rozet kaldırılır.

let strokes = 0;

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

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

Bu özellik, aşamalı bir iyileştirme olduğundan yükleme mantığı her zamanki gibidir.

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

Bu örnekte, her sayı için tek bir kalem darbesi kullanarak birden yediye kadar olan sayıları çizdim. Simgenin üzerindeki rozet sayacı artık yedi.

Tebrik kartına çizilen birden yediye kadar olan sayılar, her biri tek bir kalem darbesiyle çizilmiş.
Yedi kalem darbesiyle 1'den 7'ye kadar sayıları çizme.
Fugu Greetings uygulamasında 7 sayısını gösteren rozet simgesi.
Uygulama simgesi rozeti şeklinde kalem vuruşları sayacı.

Periodic Background Sync API

Her güne yeni bir şeyle başlamak ister misiniz? Fugu Greetings uygulamasının güzel bir özelliği, her sabah tebrik kartınızı oluşturmaya başlamanız için size yeni bir arka plan resmiyle ilham verebilmesidir. Uygulama, bunu yapmak için Periodic Background Sync API'yi kullanır.

İlk adım, hizmet çalışanı kaydında periyodik bir senkronizasyon etkinliğini kaydetmektir. 'image-of-the-day' adlı bir senkronizasyon etiketini dinler ve minimum aralığı bir gündür. Bu nedenle, kullanıcı her 24 saatte bir yeni arka plan resmi alabilir.

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

İkinci adım, hizmet çalışanında periodicsync etkinliğini dinlemektir. Etkinlik etiketi 'image-of-the-day' ise (yani daha önce kaydedilmiş olan) günün resmi, getImageOfTheDay() işleviyle alınır ve sonuç tüm istemcilere yayılır. Böylece istemciler, kanvaslarını ve önbelleklerini güncelleyebilir.

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

Bu, gerçekten de aşamalı bir iyileştirme olduğundan kod yalnızca API tarayıcı tarafından desteklendiğinde yüklenir. Bu durum hem istemci kodu hem de hizmet çalışanı kodu için geçerlidir. Desteklenmeyen tarayıcılarda ikisi de yüklenmez. Hizmet çalışanında, dinamik import() yerine (hizmet çalışanı bağlamında henüz desteklenmiyor) klasik importScripts() kullandığıma dikkat edin.

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

Fugu Greetings'de Duvar kağıdı düğmesine basıldığında, her gün Periodic Background Sync API ile güncellenen günlük tebrik kartı resmi gösterilir.

Duvar kağıdı düğmesine basıldığında günün resmi gösterilir.

Notification Triggers API

Bazen çok fazla ilham kaynağı olsa bile, başlanan bir tebrik kartını bitirmek için küçük bir dokunuşa ihtiyaç duyabilirsiniz. Bu özellik, Notification Triggers API tarafından etkinleştirilir. Kullanıcı olarak, tebrik kartımı bitirmem için hatırlatma almak istediğim zamanı girebilirim. Bu süre geldiğinde, tebrik kartımın beklediğine dair bir bildirim alacağım.

Uygulama, hedef zamanı istemesinin ardından showTrigger ile bildirimi planlar. Bu, daha önce seçilen hedef tarihle birlikte bir TimestampTrigger olabilir. Hatırlatıcı bildirimi yerel olarak tetiklenir. Ağ veya sunucu tarafı gerekmez.

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

Şimdiye kadar gösterdiğim her şeyde olduğu gibi bu da aşamalı bir iyileştirmedir. Bu nedenle kod yalnızca koşullu olarak yüklenir.

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

Fugu Greetings'de Hatırlatıcı onay kutusunu işaretlediğimde, tebrik kartımı bitirmek için ne zaman hatırlatma almak istediğim soruluyor.

Kullanıcıya tebrik kartını tamamlaması için ne zaman hatırlatma almak istediğini soran istem içeren Fugu Greetings uygulaması.
Tebrik kartını tamamlamayı hatırlatmak için yerel bildirim planlama.

Fugu Greetings'de planlanmış bir bildirim tetiklendiğinde diğer bildirimler gibi gösterilir ancak daha önce de yazdığım gibi, ağ bağlantısı gerektirmez.

Tetiklenen bildirim, macOS Bildirim Merkezi'nde görünür.

Wake Lock API

Wake Lock API'yi de eklemek istiyorum. Bazen ilham perisi gelene kadar ekrana uzun süre bakmanız yeterlidir. Bu durumda en kötü ihtimalle ekran kapanır. Wake Lock API, bunun olmasını engelleyebilir.

İlk adım, navigator.wakelock.request method() ile uyandırma kilidi elde etmektir. Ekran uyanık kalma kilidi almak için 'screen' dizesini iletiyorum. Ardından, uyandırma kilidi serbest bırakıldığında bilgilendirilmek için bir etkinlik işleyici ekliyorum. Örneğin, sekme görünürlüğü değiştiğinde bu durum oluşabilir. Bu durumda, sekme tekrar görünür hale geldiğinde uyandırma kilidini yeniden alabilirim.

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

Evet, bu aşamalı bir geliştirme olduğundan yalnızca tarayıcı API'yi desteklediğinde yüklemem gerekiyor.

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

Fugu Greetings'de, işaretlendiğinde ekranın uyanık kalmasını sağlayan bir Insomnia (Uykusuzluk) onay kutusu bulunur.

Uykusuzluk onay kutusu işaretlenirse ekran uyanık kalır.
Uykusuzluk onay kutusu, uygulamanın uyanık kalmasını sağlar.

Idle Detection API

Bazen ekrana saatlerce baksanız bile hiçbir şey yapamazsınız ve tebrik kartınızla ilgili en ufak bir fikir bile aklınıza gelmez. Idle Detection API, uygulamanın kullanıcının boşta kalma süresini algılamasına olanak tanır. Kullanıcı çok uzun süre boşta kalırsa uygulama ilk durumuna sıfırlanır ve tuval temizlenir. Bu API, boşta kalma algılamanın birçok üretim kullanım alanı bildirimlerle ilgili olduğundan (ör. yalnızca kullanıcının etkin olarak kullandığı cihaza bildirim göndermek için) bildirim izni ile sınırlıdır.

Bildirim izninin verildiğinden emin olduktan sonra boşta kalma algılayıcıyı başlatıyorum. Kullanıcı ve ekran durumunu içeren boşta kalma değişikliklerini dinleyen bir etkinlik işleyici kaydediyorum. Kullanıcı etkin veya boşta olabilir ve ekranın kilidi açık ya da kapalı olabilir. Kullanıcı işlem yapmadığında tuval temizlenir. Boşta kalma algılayıcıya 60 saniyelik bir eşik veriyorum.

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

Her zaman olduğu gibi, bu kodu yalnızca tarayıcı desteklediğinde yüklüyorum.

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

Fugu Greetings uygulamasında, Ephemeral onay kutusu işaretlendiğinde ve kullanıcı çok uzun süre boşta kaldığında tuval temizlenir.

Kullanıcı uzun süre boşta kaldıktan sonra tuvali temizlenmiş Fugu Greetings uygulaması.
Geçici onay kutusu işaretlendiğinde ve kullanıcı çok uzun süre boyunca etkin olmadığında tuval temizlenir.

Kapanış

Vay canına, ne yolculuktu! Tek bir örnek uygulamada bu kadar çok API var. Ayrıca, tarayıcılarının desteklemediği bir özellik için kullanıcının indirme maliyetini hiçbir zaman ödemediğini unutmayın. Progresif geliştirme kullanarak yalnızca alakalı kodun yüklendiğinden emin oluyorum. HTTP/2 ile istekler ucuz olduğundan bu kalıp, birçok uygulama için iyi çalışır. Ancak çok büyük uygulamalarda bir paketleyici kullanmayı düşünebilirsiniz.

Chrome Geliştirici Araçları'ndaki Ağ sekmesinde yalnızca tarayıcının desteklediği kod içeren dosyalara yönelik istekler gösteriliyor.

Tüm platformlar tüm özellikleri desteklemediğinden uygulama her tarayıcıda biraz farklı görünebilir ancak temel işlev her zaman mevcuttur ve tarayıcının özelliklerine göre kademeli olarak geliştirilir. Bu özellikler, uygulamanın yüklü bir uygulama olarak mı yoksa tarayıcı sekmesinde mi çalıştırıldığına bağlı olarak aynı tarayıcıda bile değişebilir.

Kullanılabilir birçok özelliği gösteren Android Chrome'da çalışan Fugu Greetings.
Masaüstü Safari'de çalışan ve daha az kullanılabilir özellik gösteren Fugu Greetings.
Masaüstü Chrome'da çalışan ve birçok özelliği gösteren Fugu Greetings.

GitHub'daki Fugu'yu çatallayabilirsiniz.

Chromium ekibi, gelişmiş Fugu API'leri konusunda daha iyi bir deneyim sunmak için yoğun şekilde çalışıyor. Uygulamamı oluştururken progresif geliştirme uygulayarak herkesin iyi ve sağlam bir temel deneyim elde etmesini, ancak daha fazla web platformu API'sini destekleyen tarayıcıları kullananların daha da iyi bir deneyim elde etmesini sağlıyorum. Uygulamalarınızda aşamalı iyileştirme ile neler yapacağınızı görmek için sabırsızlanıyorum.

Teşekkür

Fugu Greetings'e katkıda bulunan Christian Liebel ve Hemanth HM'ye teşekkür ederim. Bu belge Joe Medley ve Kayce Basques tarafından incelenmiştir. Jake Archibald, hizmet çalışanı bağlamında dinamik import() ile ilgili durumu öğrenmeme yardımcı oldu.