Progresif Web Uygulamanızı kademeli olarak iyileştirin

Modern tarayıcılar için geliştirme ve 2003 yılındaki gibi giderek gelişme

Nick Finck ve Steve Champeon, Mart 2003'te progresif iyileştirme konseptiyle web tasarımı dünyasını şaşırttı. Bu strateji, öncelikle temel web sayfası içeriğinin yüklenmesini vurgulayan ve ardından içeriğin üzerine daha ayrıntılı ve teknik olarak titiz sunum katmanları ve özellikler ekleyen bir web tasarımı stratejisiydi. 2003'te progresif geliştirme, o zamanlar modern 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 için modern tarayıcı özelliklerinin kullanılması gerekiyor.

Kademeli geliştirmeyle geleceğe yönelik kapsayıcı web tasarımı. Finck ve Champeon'ın orijinal sunusundan başlık slaytı.
Slayt: Progresif İlerleme ile Gelecek İçin Kapsayıcı Web Tasarımı. (Kaynak)

Modern JavaScript

JavaScript söz konusu olduğunda, en son temel ES 2015 JavaScript özellikleri için tarayıcı destek durumu harika. Yeni standart vadeler, modüller, sınıflar, şablon değişmez değerleri, ok işlevleri, let ve const, varsayılan parametreler, oluşturucular, yıkıcı atama, dinlenme ve yayılma, Map/Set, WeakMap/WeakSet ve çok daha fazlasını içerir. Tümü desteklenir.

Başlıca tüm tarayıcılardaki desteği gösteren, ES6 özellikleriyle ilgili CanIUse destek tablosu.
ECMAScript 2015 (ES6) tarayıcı destek tablosu. (Kaynak)

Eş zamansız işlevler, hem ES 2017'nin hem de benim favorilerimden biri, yaygın tüm tarayıcılarda kullanılabilir. async ve await anahtar kelimeleri, eşzamansız ve taahhüte dayalı davranışların daha net bir stilde yazılmasına olanak tanıyarak taahhüt zincirlerini açıkça yapılandırma ihtiyacını ortadan kaldırır.

Başlıca tüm tarayıcılar genelinde desteği gösteren, eş zamansız işlevler için CanIUse destek tablosu.
Eş zamansız işlevler tarayıcı desteği tablosu. (Kaynak)

Ayrıca, isteğe bağlı zincirleme ve boş birleştirme gibi kısa süre önce ES2020 diline eklenen çok kısa bir süre içinde, desteğe de çok hızlı bir şekilde ulaştık. Aşağıda kod örneğini görebilirsiniz. Temel JavaScript özelliklerine gelince, çimlerimiz bugünkü kadar yeşil olamaz.

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, yeşildir. (izin ile kullanılan Microsoft ürününün ekran görüntüsü.)

Örnek uygulama: Fugu Selamları

Bu makalede Fugu Greetings (GitHub) adlı basit bir PWA ile çalışıyorum. Bu uygulamanın adı, web'e Android/iOS/masaüstü uygulamalarının tüm gücünü kazandırmaya çalışan Project Fugu 🐡 için şapka örneğidir. Projenin açılış sayfasından daha fazla bilgi edinebilirsiniz.

Fugu Greetings, sanal tebrik kartları oluşturmanıza ve bunları sevdiklerinize göndermenize olanak tanıyan bir çizim uygulamasıdır. PWA'nın temel kavramlarına örnektir. Güvenilirdir ve tamamen çevrimdışıdır. Bu sayede ağınız olmasa bile kullanmaya devam edebilirsiniz. Aynı zamanda cihazların ana ekranına Yüklenebilir ve bağımsız bir uygulama olarak işletim sistemiyle sorunsuz bir şekilde entegre olur.

Fugu, PWA topluluk logosuna benzeyen bir çizimle PWA'yı selamlıyor.
Fugu Karşılamaları örnek uygulaması.

Progresif geliştirme

Artık size progresif geliştirme hakkında bilgi vermek gerekiyor. MDN Web Dokümanları Sözlüğü, kavramı şu şekilde tanımlar:

Progresif geliştirme, mümkün olan en iyi deneyimi yalnızca gerekli tüm kodları çalıştırabilen en modern tarayıcıların kullanıcılarına sunarken aynı zamanda gerekli içerik ve işlevlerin temelini oluşturan bir tasarım felsefesidir.

Özellik algılama, genellikle tarayıcıların daha modern işlevleri işleyip işleyemeyeceğini belirlemek için kullanılır. Çoklu dolgular ise genellikle JavaScript ile eksik özellikleri eklemek için kullanılır.

[…]

Progresif geliştirme, web geliştiricilerin mümkün olan en iyi web sitelerini geliştirmeye odaklanırken bu web sitelerinin birden fazla bilinmeyen kullanıcı aracısında çalışmasını sağlayan yararlı bir tekniktir. Dereceli azalma birbiriyle ilişkilidir ancak aynı şey değildir ve genellikle progresif geliştirmenin tam tersi yönde ilerlemek olarak görülür. Aslında her iki yaklaşım da geçerlidir ve genellikle birbirini tamamlar.

MDN'ye katkıda bulunanlar

Her tebrik kartına sıfırdan başlamak çok zahmetli bir iş olabilir. Öyleyse kullanıcıların bir resmi içe aktarmasına olanak veren bir özelliğe sahip olmadan çalışmaya ne dersiniz? Geleneksel bir yaklaşımda bunu sağlamak için bir <input type=file> öğesi kullanırdınız. İlk olarak öğeyi oluşturur, type değerini 'file' olarak ayarlar ve MIME türlerini accept özelliğine ekleyin. Daha sonra, programatik olarak öğeyi "tıklayıp" değişiklikleri dinleyin. Bir resim seçtiğinizde bu 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();
  });
};

Bir import özelliği olduğunda, kullanıcıların tebrik kartlarını yerel olarak kaydedebilmesi için muhtemelen bir import özelliği olmalıdır. Dosyaları kaydetmenin geleneksel yolu, download özelliğine ve href olarak bir blob URL'sine sahip bir bağlayıcı bağlantı oluşturmaktır. İndirmeyi tetiklemek için programlı bir şekilde "tıklarsınız " ve bellek sızıntılarını önlemek için blob nesne URL'sini iptal etmeyi unutmayacağınızı umarı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. Mesela bir tebrik kartını "indirmiyor", "kaydetmişsin". Dosyayı nereye yerleştireceğinizi seçmenize olanak tanıyan bir "kaydet" iletişim kutusu göstermek yerine, tarayıcı, karşılama kartını kullanıcı etkileşimi olmadan doğrudan indirmiştir ve doğrudan İndirilenler klasörünüze yerleştirmiştir. Bu pek iyi değil.

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

Peki bir API'yi özellik algılama özelliğini nasıl algılarım? File System Access API, window.chooseFileSystemEntries() adlı yeni bir yöntemi kullanıma sunuyor. Sonuç olarak, bu yöntemin kullanılabilir olup olmadığına bağlı olarak koşullu olarak farklı içe ve dışa aktarma modülleri yüklemem gerekiyor. Bunun nasıl yapılacağını aşağıda görebilirsiniz.

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 ayrıntılarına geçmeden önce, buradaki progresif geliştirme modelini kısaca vurgulamak istiyorum. Şu anda File System Access API'yi desteklemeyen tarayıcılarda eski komut dosyalarını yüklüyorum. Firefox ve Safari'nin ağ sekmelerini aşağıda görebilirsiniz.

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

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

Modern dosyaların yüklenmesini gösteren Chrome Geliştirici Araçları.
Chrome Geliştirici Araçları ağ sekmesi.

File System Access API

Bu konuyu ele aldığım için şimdi Dosya Sistemi Erişim API'sına dayalı gerçek uygulamaya bakalım. Bir resmi içe aktarmak için window.chooseFileSystemEntries() yöntemini çağırıyorum ve resim dosyaları istediğimi belirttiğim bir accepts özelliğini iletiyorum. MIME türlerinin yanı sıra dosya uzantıları da desteklenir. Bu işlem sonucunda, getFile() yöntemini çağırarak asıl dosyayı alabileceğim bir dosya tanıtıcısı elde edilir.

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 chooseFileSystemEntries() yöntemine bir 'save-file' tür parametresi iletmem gerekiyor. Buradan dosya kaydetme iletişim kutusunu açıyorum. Dosya açıkken 'open-file' varsayılan olduğundan bu işlem gerekli değildi. accepts parametresini öncekine benzer şekilde ayarladım ancak bu süre yalnızca PNG resimleriyle sınırlıydı. Yine bir dosya tanıtıcısı elde ediyorum ancak bu sefer dosyayı almak yerine, createWritable() yöntemini çağırarak yazılabilir bir akış oluşturuyorum. Sonra, dosyaya tebrik kartı resmim olan blob'u yazıyorum. Son olarak da yazılabilir akışı kapatıyorum.

Her zaman hata oluşabilir: Diskte yer kalmamış olabilir, yazma veya okuma hatası olabilir ya da kullanıcı dosya iletişim kutusunu iptal etmiş olabilir. Bu nedenle, çağrıları her zaman try...catch ifadesinde sarmalıyorum.

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 progresif geliştirme kullanarak bir dosyayı önceki gibi açabiliyorum. İçe aktarılan dosya kanvasın üzerine çizilir. Düzenlemelerimi yapıp, dosyanın adını ve depolama konumunu seçebileceğim gerçek bir kaydetme iletişim kutusuyla son olarak kaydedebiliyorum. Artık dosya sonsuza kadar korunmaya hazırdır.

Dosya açma iletişim kutusuyla Fugu Selamlama uygulaması.
Dosya açma iletişim kutusu.
Fugu Selamlama uygulaması artık içe aktarılmış bir resim içeriyor.
İçe aktarılan görüntü.
Değiştirilmiş resimle Fugu Selamlama uygulaması.
Değiştirilen resim yeni bir dosyaya kaydediliyor.

Web Paylaşımı ve Web Paylaşımı Hedef API'leri

Belki sonsuza kadar saklamanın dışında, belki de tebrik kartımı paylaşmak isterim. Web Share API ve Web Share Target API bunu yapmama olanak tanıyor. Mobil ve yakın zamanda sunulan masaüstü işletim sistemleri, yerleşik paylaşım mekanizmalarına sahip. Örneğin aşağıda, blogumdaki bir makaleden tetiklenen masaüstü Safari paylaşım sayfasını macOS'te görebilirsiniz. Makaleyi Paylaş düğmesini tıkladığınızda makalenin bağlantısını bir arkadaşınızla (örneğin, macOS Mesajlar uygulaması aracılığıyla) paylaşabilirsiniz.

Masaüstü Safari&#39;nin macOS&#39;teki paylaşım sayfası, bir makalenin Paylaş düğmesinden tetikleniyor
macOS'teki masaüstü Safari'de Web Share API.

Bunun için gereken kod oldukça basittir. navigator.share() adını belirtiyorum ve bunu bir nesnede isteğe bağlı title, text ve url olarak iletiyorum. Ama resim eklemek istersem ne olur? Web Share API'nin 1. Seviyesinde bu özellik henüz desteklenmiyor. Ancak iyi bir haberimiz var. Web Paylaşımı Düzeyi 2'nin dosya paylaşımı özellikleri eklendi.

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

Bunun için Fugu Tebrik kartı uygulaması ile nasıl çalışacağınızı göstereceğim. Öncelikle, bir blob ve ardından title ile text içeren files dizisine sahip bir data nesnesi hazırlamam gerekiyor. Ardından, en iyi uygulama olarak adının anlaşıldığı işlemi yapan yeni navigator.canShare() yöntemini kullanıyorum: Paylaşmaya çalıştığım data nesnesinin tarayıcı tarafından teknik olarak paylaşılıp paylaşılamayacağını söylüyor. navigator.canShare() bana verilerin paylaşılabileceğini söylerse önceden olduğu gibi navigator.share() araması yapmaya hazırım. Her şey başarısız olabileceği için tekrar try...catch bloku 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 aşamalı geliştirme kullanıyorum. navigator nesnesinde hem 'share' hem de 'canShare' varsa yalnızca bu durumda devam edip dinamik import() aracılığıyla share.mjs yüklüyorum. Mobil Safari gibi iki koşuldan yalnızca birini karşılayan tarayıcılarda işlevi yüklemiyorum.

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

Fugu Selamları'nda, Android'deki Chrome gibi destekleyen bir tarayıcıda Paylaş düğmesine dokunursam yerleşik paylaşım sayfası açılır. Örneğin, Gmail'i seçtiğinizde e-posta oluşturma widget'ı resim eklenmiş olarak açılır.

Resmin paylaşılacağı çeşitli uygulamaları gösteren işletim sistemi düzeyinde paylaşım sayfası.
Dosyanın paylaşılacağı uygulamayı seçme.
Gmail&#39;in resmin ekli olduğu e-posta oluşturma widget&#39;ı.
Dosya, Gmail'in oluşturucusunda yeni bir e-postaya eklenir.

Kişi Seçici API'sı

Şimdi de kişiler hakkında, yani cihazın adres defteri veya kişi yöneticisi uygulaması hakkında konuşmak istiyorum. Bir tebrik kartı yazarken birinin adını doğru şekilde yazmak her zaman kolay olmayabilir. Örneğin, adının Kiril harfleriyle yazılmasını tercih eden bir arkadaşım Sergey. Almanca bir QWERTZ klavye kullanıyorum ve adlarını nasıl yazacağımı bilmiyorum. Bu, Contact Picker API'nin çözebileceği bir sorundur. Bir arkadaşım, telefonumun Kişiler uygulamasında kayıtlı olduğundan, Kişiler Seçici API'sı aracılığıyla web'deki kişilerime ulaşabiliyorum.

Öncelikle, erişmek istediğim özelliklerin listesini belirtmem gerekiyor. Bu durumda sadece adları istiyorum, ancak diğer kullanım örneklerinde telefon numaraları, e-postalar, avatar simgeleri veya fiziksel adresler ilgimi çekebilir. Daha sonra, bir options nesnesi yapılandırıyorum ve birden fazla giriş seçebilmek için multiple öğesini true olarak ayarlıyorum. Son olarak, kullanıcı tarafından seçilen kişiler için istenen özellikleri döndüren navigator.contacts.select() yöntemini ç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);
  }
};

Şu ana kadar muhtemelen bu kalıbı öğrenmişsinizdir: Dosyayı yalnızca API gerçekten desteklendiğinde yüklüyorum.

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

Fugu Greeting'de Kişiler düğmesine dokunduğumda ve en iyi iki arkadaşım olan öngörülü olmayan arkadaşlarımı seçtiğimde, ер ücretей миайлович 이рин ve 劳伦斯·爱德·"拉里"·佩奇 gibi diğer kişilerin e-posta adreslerini görebilir Adları daha sonra tebrik kartıma yazılıyor.

Adres defterindeki iki kişinin adını gösteren kişi seçici.
Adres defterinden kişi seçiciyi kullanarak iki ad belirleme.
Daha önce seçilen iki kişinin adı tebrik kartının üzerinde çizilir.
Ardından, iki ad tebrik kartına çizilir.

Asenkron Pano API'si

Sırada kopyalama ve yapıştırma var. Yazılım geliştiricileri olarak en sevdiğimiz işlemlerden biri kopyalama ve yapıştırma işlemi. Bir tebrik kartı yazarı olarak ben de bazen aynısını yapmak isteyebilirim. Üzerinde çalıştığım bir tebrik kartına resim yapıştırmak veya tebrik kartımı kopyalayıp başka bir yerden düzenlemeye devam etmek isteyebilirim. Async Clipboard API hem metin hem de resimleri destekler. Fugu Karşılama uygulamasına kopyalayıp yapıştırma desteğini nasıl eklediğimi size adım adım açıklayacağım.

Sistemin panosuna bir şey kopyalamak için ona yazmam gerekiyor. navigator.clipboard.write() yöntemi, pano öğeleri dizisini parametre olarak alır. Her pano öğesi temel olarak değer olarak blob, anahtar olarak ise 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ğrısıyla elde ettiğim pano öğelerini döngüye almam gerekiyor. Bunun nedeni, birden fazla pano öğesinin panoda farklı biçimlerde yer alabilmesidir. Her pano öğesinde, kullanılabilir kaynakların MIME türlerini bildiren bir types alanı bulunur. Daha önce edindiğim 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);
  }
};

Üstelik bunu söylememe gerek yok. Bunu yalnızca destekleyen tarayıcılarda yapıyorum.

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

Peki bu uygulamadaki işleyiş şekli nasıldır? macOS Preview uygulamasında bir resmim var ve bunu panoya kopyalıyorum. Yapıştır'ı tıkladığımda Fugu Greetings uygulaması, uygulamanın panodaki metin ve resimleri görmesine izin vermek isteyip istemediğimi soruyor.

Pano izin istemini gösteren Fugu Selamlama uygulaması.
Pano izin istemi.

Son olarak, izni kabul ettikten sonra resim uygulamaya yapıştırılır. Bunun tersi de geçerlidir. Panoya bir tebrik kartı kopyalıyorum. Önizleme'yi açıp Dosya'yı ve ardından Panodan yeni'yi tıkladığımda tebrik kartı yeni bir adsız resme yapıştırılıyor.

Adsız, yeni yapıştırılmış bir resmin olduğu macOS Preview uygulaması.
macOS Preview uygulamasına yapıştırılan bir resim.

Rozet API'si

Sunulan diğer bir kullanışlı API de Rozet API'sidir. Yüklenebilir bir PWA olan Fugu Greetings'in elbette kullanıcıların uygulama yuvasına veya ana ekrana yerleştirebileceği bir uygulama simgesi var. API'yi göstermenin eğlenceli ve kolay bir yolu, onu Fugu Selamları'nda kalem darbesi sayacı olarak kullanmaktır. pointerdown etkinliği her gerçekleştiğinde kalem vuruşu sayacını artıran ve daha sonra, 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 progresif bir geliştirme olduğundan yükleme mantığı her zamanki gibidir.

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

Bu örnekte, her numara için bir kalem çizgisi kullanarak, birden yediye kadar olan sayıları çizdim. Simgenin üzerindeki rozet sayacı şimdi yedide.

Birden yediye kadar olan sayılar tebrik kartına çizilir. Her biri tek bir kalemle yazılır.
1'den 7'ye kadar olan sayıları yedi kalem çizgisiyle çizme.
Fugu Karşılama uygulamasında 7 rakamını gösteren rozet simgesi.
Kalem vuruşu sayacı, uygulama simgesi rozeti biçimindedir.

Periyodik Arka Plan Senkronizasyonu API'sı

Her güne yeni bir şeyle yeni bir başlangıç yapmak ister misiniz? Fugu Selam uygulamasının kullanışlı bir özelliği de her sabah, tebrik kartınıza başlamak için yeni bir arka plan resmiyle size ilham vermesidir. Uygulama, bunun için Periodic Arka Plan Senkronizasyonu API'sini kullanır.

İlk adım, hizmet çalışanı kaydına periyodik bir senkronizasyon etkinliği register. 'image-of-the-day' adlı bir senkronizasyon etiketini dinler ve en az bir günlük bir aralığa sahiptir. Böylece kullanıcı 24 saatte bir yeni bir 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, Service Worker'da periodicsync etkinliğini dinlemektir. Etkinlik etiketi 'image-of-the-day' ise yani daha önce kaydedilen etiketse getImageOfTheDay() işlevi aracılığıyla günün resmi alınır ve sonuç tüm istemcilere yayılır. Böylece müşteriler tuvallerini 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 da gerçekten progresif bir geliştirmedir. Bu yüzden, kod yalnızca API, tarayıcı tarafından desteklendiğinde yüklenir. Bu, hem istemci kodu hem de hizmet çalışanı kodu için geçerlidir. Desteklenmeyen tarayıcılarda, bu tarayıcıların ikisi de yüklenmez. Dinamik import() (henüz hizmet çalışanı bağlamında desteklenmez) yerine Service Worker'da klasik importScripts() özelliğini nasıl kullandığımı unutmayın.

// 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 Karşılamaları'nda Duvar kağıdı düğmesine basıldığında, Periodic Arka Plan Senkronizasyonu API'si aracılığıyla her gün güncellenen günün tebrik kartı resmi görüntülenir.

Günün yeni tebrik kartı resmine sahip Fugu Tebrik uygulaması.
Duvar kağıdı düğmesine bastığınızda günün resmi görüntülenir.

Bildirim Tetikleyicileri API'sı

Bazen çok fazla ilham verse bile, başladığınız bir tebrik kartını bitirmek için biraz harekete geçmeniz gerekir. Bu, Bildirim Tetikleyicileri API'si tarafından etkinleştirilen bir özelliktir. Kullanıcı olarak, tebrik kartımı tamamlamam için otomatik olarak bilgilendirilmek istediğim bir zamanı girebiliyorum. O zaman geldiğinde, tebrik kartımın beklediğine dair bir bildirim alırım.

Uygulama, hedef zamanı bildirdikten sonra showTrigger ile bildirimi planlar. Bu, daha önce seçilmiş hedef tarihe sahip bir TimestampTrigger olabilir. Hatırlatıcı bildirimi yerel olarak tetiklenir ve 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 diğer her şey gibi bu da progresif bir geliştirme olduğundan kod yalnızca koşullu olarak yüklenir.

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

Fugu Selamları'nda Hatırlatıcı onay kutusunu işaretlediğimde tebrik kartımı bitirmemin ne zaman hatırlatılmasını istediğimi soran bir istem gösteriliyor.

Kullanıcıya tebrik kartını ne zaman tamamlaması gerektiğini soran bir istem içeren Fugu Tebrik uygulaması.
Bir tebrik kartını bitirmeniz için hatırlatma yapılacak yerel bir bildirim planlama.

Fugu Selamları'nda planlanmış bir bildirim tetiklendiğinde, aynı diğer bildirimler gibi gösterilir ama daha önce de belirttiğim gibi ağ bağlantısı gerektirmiyordu.

Fugu Karşılaması&#39;ndan gelen tetiklenen bir bildirimi gösteren macOS Bildirim Merkezi.
Tetiklenen bildirim macOS Bildirim Merkezi'nde görünür.

Wake Lock API'si

Wake Lock API'yi de dahil etmek istiyorum. Bazen ilham sizi öpünceye kadar ekrana yeterince uzun süre bakmanız gerekir. Bu durumda olabilecek en kötü şey ekranın kapatılmasıdır. Wake Lock API bunu önleyebilir.

İlk adım, navigator.wakelock.request method() ile bir uyanık kalma kilidi edinmektir. Ekran uyanık kalma kilidi almak için 'screen' dizesini iletiyorum. Daha sonra, uyanık kalma kilidi serbest bırakıldığında bilgilendirilecek bir etkinlik işleyici ekliyorum. Bu durum, örneğin sekme görünürlüğü değiştiğinde gerçekleşebilir. Bu durumda, sekme tekrar görünür hale geldiğinde uyanık kalma kilidini yeniden edinebilirim.

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 kademeli 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 Selamları'nda Uykusuzluk onay kutusu bulunur. Bu kutu işaretlendiğinde ekran uyanık tutar.

Uykusuzluk onay kutusu işaretliyse ekranı uyanık tutar.
Uykusuzluk onay kutusu uygulamayı uyanık tutar.

Boşta Kalma Algılama API'si

Bazen ekrana saatlerce baksanız bile bu işe yaramaz. Tebrikler mi? Tebrik kartınıza ne yapacağınızı bilemeyeceksiniz. Idle Detection API, uygulamanın kullanıcıların boşta kalma süresini algılamasına olanak tanır. Kullanıcı çok uzun bir süre boşta kalırsa uygulama ilk duruma sıfırlanır ve zemini temizler. Boşta kalma algılamasının üretimde kullanıldığı çoğu kullanım alanı bildirimlerle ilgilidir (ör. yalnızca kullanıcının aktif olarak kullandığı bir cihaza bildirim göndermek) için bu API şu anda bildirim izninin arkasındadır.

Bildirim izninin verildiğinden emin olduktan sonra boşta kalma algılayıcısını örnekliyorum. Kullanıcı ve ekran durumu dahil boşta yapılan değişiklikleri işleyen bir etkinlik işleyici kaydediyorum. Kullanıcı etkin veya boşta olabilir ve ekran kilidi açılabilir ya da kilitlenebilir. Kullanıcı boştaysa tuval temizlenir. Boşta olan 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 zamanki gibi, bu kodu yalnızca tarayıcı desteklediğinde yüklüyorum.

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

Fugu Karşılamaları uygulamasında Geçici onay kutusu işaretlendiğinde ve kullanıcı çok uzun süre boşta kaldığında tuval temizlenir.

Fugu Selamlama uygulaması, kullanıcının çok uzun süre işlem yapmaması nedeniyle temiz bir kanvasla gösteriliyor.
Geçici onay kutusu işaretlendiğinde ve kullanıcı çok uzun süredir boşta kaldığında tuval temizlenir.

Kapanma

Vay canına, ne müthiş bir yolculuk. Tek bir örnek uygulamada çok fazla API var. Unutmayın, kullanıcıdan tarayıcının desteklemediği bir özellik için indirme ücreti ödemek zorunda bırakmam. Kademeli geliştirme kullanarak yalnızca alakalı kodun yüklenmesini sağlıyorum. HTTP/2'de istekler ucuz olduğundan bu kalıp birçok uygulamada iyi sonuç verecektir. Bununla birlikte, gerçekten büyük uygulamalar için bir paketleyici kullanmayı düşünebilirsiniz.

Yalnızca geçerli tarayıcının desteklediği koda sahip dosya isteklerini gösteren Chrome Geliştirici Araçları Ağ paneli.
Chrome Geliştirici Araçları Ağ sekmesinde yalnızca geçerli tarayıcının desteklediği koda sahip dosya istekleri gösteriliyor.

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

Android Chrome&#39;da çalışan Fugu Selamları&#39;nda kullanılabilir birçok özellik gösteriliyor.
Android Chrome'da çalışan Fugu Karşılamaları.
Masaüstü Safari&#39;de çalışan Fugu Selamları&#39;nda daha az kullanılabilir özellik gösteriliyor.
Masaüstü Safari'de Fugu Karşılamaları.
Masaüstü Chrome&#39;da çalışan Fugu Selamları, kullanılabilir birçok özelliği gösteriyor.
Masaüstü Chrome'da Fugu Karşılamaları çalışıyor.

Fugu Karşılamaları uygulaması ilginizi çekiyorsa GitHub'da uygulamayı çatallayın.

GitHub&#39;da Fugu Greetings deposu.
GitHub'daki Fugu Karşılamaları uygulaması.

Chromium ekibi, gelişmiş Fugu API'leri söz konusu olduğunda çimleri daha yeşil hale getirmek için yoğun bir şekilde çalışıyor. Uygulamamın geliştirilmesinde aşamalı iyileştirme uygulayarak herkesin iyi ve sağlam bir temel deneyim yaşamasını sağlarım, ancak daha fazla Web platformu API'sini destekleyen tarayıcıları kullanan kişilerin daha da iyi bir deneyim yaşamasını sağlarım. Uygulamalarınızdaki aşamalı geliştirmeyle 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'e minnettarım. Bu makale 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.