Progresif Web Uygulamanızı kademeli olarak iyileştirin

Modern tarayıcılar için geliştirme yapıp 2003'teymiş gibi aşamalı olarak geliştirme

Mart 2003'te Nick Finck ve Steve Champeon, web tasarım dünyasını kademeli geliştirme kavramıyla şaşırttı. Web tasarımı için bir strateji olan kademeli geliştirme, önce web sayfasının temel içeriğinin yüklenmesini vurgular ve ardından içeriğin üzerine giderek daha ayrıntılı ve teknik açıdan titiz sunum ve özellik katmanları ekler. 2003'te aşamalı geliştirme, o zamanlar modern CSS özelliklerini, göze çarpmayan JavaScript'i ve hatta ölçeklenebilir vektör grafiklerini kullanmakla ilgiliydi. 2020 ve sonrasındaki aşamalarda, aşamalı geliştirme modern tarayıcı özelliklerini kullanmakla ilgilidir.

Kademeli iyileştirmeyle gelecek için kapsayıcı web tasarımı. Finck ve Champeon'un orijinal sunumundaki başlık slaytı.
Slayt: Kademeli İyileştirme ile Gelecek İçin Kapsayıcı Web Tasarımı. (Kaynak)

Modern JavaScript

JavaScript'den bahsetmişken, en son temel ES 2015 JavaScript özellikleri için tarayıcı desteğinin mükemmel olduğunu belirtmek isteriz. Yeni standart; umutlar, modüller, sınıflar, şablon literalleri, ok işlevleri, let ve const, varsayılan parametreler, jeneratörler, yapı bozma atama, geri kalan ve dağıtma, Map/Set, WeakMap/WeakSet ve daha fazlasını içerir. Tüm tarayıcılar 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ı destek tablosu. (Kaynak)

ES 2017 özelliği olan ve en sevdiğim işlevlerden biri olan arayüz dışı işlevler, tüm popüler tarayıcılarda kullanılabilir. async ve await anahtar kelimeleri, söz zincirlerinin açıkça yapılandırılması gerekmeden, söze dayalı ve eşzamansız davranışın daha net bir şekilde yazılmasını sağlar.

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

İsteğe bağlı zincirleme ve boş birleştirme gibi ES 2020'de eklenen yeni dil özellikleri bile çok hızlı bir şekilde destek kapsamına alındı. Aşağıda bir kod örneğini görebilirsiniz. Temel JavaScript özellikleri söz konusu olduğunda, durum bugün olduğundan çok daha iyi.

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

Örnek uygulama: Fugu Greetings

Bu makalede, Fugu Greetings (GitHub) adlı basit bir PWA kullanıyorum. Bu uygulamanın adı, web'e Android/iOS/masaüstü uygulamalarının tüm özelliklerini kazandırma çabası olan Project Fugu'ya 🐡 bir göndermedir. 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. Bu örnek, PWA'nın temel kavramlarını göstermektedir. Güvenilir ve tamamen çevrimdışı olarak kullanılabildiğinden, ağ bağlantınız olmasa bile bu özelliği kullanabilirsiniz. Ayrıca cihazın ana ekranına yüklenebilir ve bağımsız bir uygulama olarak işletim sistemiyle sorunsuz şekilde entegre olur.

PWA topluluğu logosuna benzeyen bir çizim içeren Fugu Greetings PWA.
Fugu Greetings örnek uygulaması.

Progresif geliştirme

Bu bilgilerden sonra kademeli iyileştirme hakkında konuşmanın zamanı geldi. MDN Web Dokümanları Sözlüğü bu kavramı şu şekilde tanımlamaktadır:

Kademeli iyileştirme, mümkün olduğunca çok sayıda kullanıcıya temel içerik ve işlevsellik sunarken 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 sunan 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 doldurma 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ştirirken bu web sitelerini birden fazla bilinmeyen kullanıcı aracısında çalıştırmasına olanak tanıyan faydalı bir tekniktir. Sorunsuz düşüş, kademeli iyileştirmeyle ilgili ancak aynı değildir ve genellikle kademeli iyileştirmenin tam tersi yönde bir hareket olarak görülür. Gerçekte her iki yaklaşım da geçerlidir ve genellikle birbirini tamamlayabilir.

MDN katkıda bulunanları

Her tebrik kartını sıfırdan oluşturmak gerçekten zahmetli olabilir. Bu nedenle, kullanıcıların bir resim içe aktarmasına ve oradan başlamasına olanak tanıyan bir özellik neden olmasın? Geleneksel yaklaşımda bunu yapmak için bir <input type=file> öğesi kullanırdınız. Öncelikle öğeyi oluşturur, type değerini 'file' olarak ayarlar ve accept mülküne MIME türleri eklersiniz. Ardından öğeyi programatik olarak "tıklar" ve değişiklikleri beklersiniz. 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ğine sahip bir ana sayfa bağlantısı ve href olarak bir blob URL'si oluşturmaktır. Ayrıca, indirme işlemini tetiklemek için bu URL'yi programatik olarak "tıklamanız" gerekir. Bellek sızıntısını önlemek için blob nesnesi URL'sini iptal etmeyi de unutmayın.

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, bir tebrik kartını "indirmediniz", "kaydettiniz". Tarayıcı, dosyayı nereye koyacağınızı seçmenize olanak tanıyan bir "kaydet" iletişim kutusu göstermek yerine, kullanıcı etkileşimi olmadan doğrudan tebrik kartını indirip İndirilenler klasörünüze yerleştirdi. Bu pek iyi değil.

Daha iyi bir yol olsaydı ne olurdu? Yerel bir dosyayı açıp düzenledikten sonra değişiklikleri yeni bir dosyaya veya ilk açtığınız orijinal dosyaya kaydedebilseydiniz ne olurdu? File System Access API, dosya ve dizinleri açıp oluşturmanın yanı sıra bunları değiştirmenize ve kaydetmenize olanak tanır.

Peki bir API'de nasıl özellik algılayabilirim? File System Access API, yeni bir yöntem window.chooseFileSystemEntries() sağlar. Bu nedenle, bu yöntemin kullanılabilir olup olmadığına bağlı olarak farklı içe aktarma ve dışa aktarma modüllerini koşullu olarak yüklemem gerekiyor. Bunu nasıl yapacağınızı 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, aşamalı iyileştirme modelini kısaca vurgulamak isterim. Ş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üklendiğini gösteren Safari Web İnceleyici.
Safari Web İnspector ağ sekmesi.
Eski dosyaların yüklendiğini gösteren Firefox Geliştirici Araçları.
Firefox Developer Tools ağ sekmesi.

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ği dinamik import() sayesinde zarif bir şekilde mümkün kılınmaktadır. Daha önce de söylediğim gibi, bu günlerde işler oldukça yolunda.

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

File System Access API

Bu konuyu ele aldıktan sonra, File System Access API'ye dayalı gerçek uygulamaya bakmanın zamanı geldi. Resim içe aktarmak için window.chooseFileSystemEntries() işlevini çağırıyorum ve resim dosyaları istediğimi belirten bir accepts mülkü iletiyorum. Hem dosya uzantıları hem de MIME türleri desteklenir. Bu işlem, getFile() işlevini çağırarak asıl dosyayı alabileceğim bir dosya tutamacı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 sefer chooseFileSystemEntries() yöntemine 'save-file' türündeki bir parametre göndermem gerekiyor. Bu işlemden sonra bir dosya kaydetme iletişim kutusu görüyorum. Dosya açıkken 'open-file' varsayılan olduğu için bu gerekli değildi. accepts parametresini öncekine benzer şekilde ayarladım ancak bu sefer yalnızca PNG resimleriyle sınırladım. Yine bir dosya tutamaç döndürülür ancak bu sefer dosyayı almak yerine createWritable() işlevini çağırarak yazılabilir bir akış oluştururum. Ardından, tebrik kartı resmim olan blob'u dosyaya yazarım. Son olarak, yazılabilir akışı kapatıyorum.

Her şey her zaman başarısız olabilir: 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 bir try...catch ifadesi içine alırı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ı geliştirmeyi kullanarak dosyaları eskisi gibi açabilirim. İçe aktarılan dosya doğrudan tuvale çizilir. Düzenlemeler yapıp dosyanın adını ve depolama konumunu seçebileceğim gerçek bir kaydetme iletişim kutusuyla dosyayı kaydedebilirim. Dosya artık sonsuza kadar saklanacak şekilde hazırdı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 resim içeriyor.
İçe aktarılan resim.
Değiştirilmiş resmi içeren Fugu Greetings uygulaması.
Değiştirilen resmi yeni bir dosyaya kaydetme.

Web Share ve Web Share Target API'leri

Kartımı sonsuza kadar saklamak dışında, paylaşmak da istiyorum. Bu, Web Paylaş API ve Web Paylaş Hedef API'nin bana izin verdiği bir şey. Mobil ve daha yakın zamanda masaüstü işletim sistemleri yerleşik paylaşım mekanizmalarına kavuştu. Örneğin, aşağıdaki resimde masaüstü Safari'nin macOS'teki paylaşım sayfası gösterilmektedir. Bu sayfa, blog'umdaki bir makaleden tetiklenmiştir. Makaleyi Paylaş düğmesini tıkladığınızda makalenin bağlantısını bir arkadaşınızla (ör. macOS Mesajlar uygulaması üzerinden) paylaşabilirsiniz.

Bir makalenin Paylaş düğmesinden tetiklenen macOS&#39;teki masaüstü Safari&#39;nin paylaşım sayfası
MacOS'teki masaüstü Safari'sinde Web Paylaşımı API'si.

Bunu gerçekleştirmek için kullanacağınız kod oldukça basittir. navigator.share() işlevini çağırıyorum ve bir nesnede isteğe bağlı title, text ve url parametrelerini ilerliyorum. Peki bir resim eklemek istersem ne olur? Web Share API'nin 1. düzeyi henüz bunu desteklemiyor. Neyse ki Web Paylaşımı 2. Seviye, dosya paylaşımı özellikleri 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);
}

Bunu Fugu tebrik kartı uygulamasıyla nasıl yapacağınızı göstereyim. Öncelikle, bir blobdan 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ından da anlaşılacağı gibi, yeni navigator.canShare() yöntemini kullanırım: Paylaşmaya çalıştığım data nesnesinin teknik olarak tarayıcı tarafından paylaşılıp paylaşılamayacağı hakkında bilgi verir. navigator.canShare(), verilerin paylaşılabileceğini söylerse navigator.share()'u aramaya hazırım. Her şey başarısız olabileceği için yine bir try...catch bloğu 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ştirmeyi kullanıyorum. navigator nesnesinde hem 'share' hem de 'canShare' varsa ancak o zaman devam edip dinamik import() aracılığıyla share.mjs'ı yüklerim. Mobil Safari gibi, iki koşuldan yalnızca birini karşılayan tarayıcılarda işlevi yüklemem.

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

Fugu Greetings'da, Android'de Chrome gibi desteklenen bir tarayıcıda Paylaş düğmesine dokunursam yerleşik paylaşım sayfası açılır. Örneğin, Gmail'i seçtiğimde, e-posta oluşturucu widget'ı, resmi ekleyerek açılır.

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

Contact Picker API

Ardından, cihazın adres defteri veya kişi yöneticisi uygulaması olan kişilerden bahsetmek istiyorum. Bir tebrik kartı yazarken kişinin adını doğru yazmak her zaman kolay olmayabilir. Örneğin, adının Kiril alfabesiyle yazılmasını tercih eden bir arkadaşım var. Almanca QWERTZ klavye kullanıyorum ve adlarını nasıl yazacağımı bilmiyorum. Bu, Contact Picker API'nin çözebileceği bir sorundur. Arkadaşımı telefonumun Kişiler uygulamasında sakladığım için Kişi Seçici API'si aracılığıyla web'den kişilerime erişebiliyorum.

Öncelikle, erişmek istediğim mülklerin listesini belirtmem gerekiyor. Bu durumda yalnızca adları istiyorum ancak diğer kullanım alanları için telefon numaraları, e-postalar, avatar simgeleriyle fiziksel adresler de ilgimi çekebilir. Ardından, birden fazla giriş seçebilmek için bir options nesnesi yapılandırıp multiple değerini true olarak ayarlıyorum. Son olarak, kullanıcı tarafından seçilen kişiler için istenen ö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);
  }
};

Şimdiye kadar muhtemelen şu kalıbı öğrendiniz: Yalnızca API gerçekten desteklendiğinde dosyayı yüklerim.

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

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

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

Eşzamansız Clipboard API

Bir sonraki konu kopyalama ve yapıştırma. Yazılım geliştiricileri olarak en sevdiğimiz işlemlerden biri kopyalama ve yapıştırmadır. Kart yazarı olarak bazen aynısını yapmak isterim. Üzerinde çalıştığım bir tebrik kartına resim yapıştırmak veya tebrik kartımı başka bir yerden düzenlemeye devam edebilmek için kopyalamak istiyorum. Async Clipboard API hem metni hem de görselleri destekler. Fugu Greetings uygulamasına kopyalama ve yapıştırma desteğini nasıl eklediğimi adım adım anlatacağım.

Bir şeyi sistemin panosuna kopyalamak için panoya yazmam gerekiyor. navigator.clipboard.write() yöntemi, parametre olarak bir pano öğesi dizisi alır. Her panoya kopyalanan öğe, temelde değeri bir blob, anahtarı ise blob 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ını kullanarak elde ettiğim pano öğelerini döngüden geçirmem gerekiyor. Bunun nedeni, farklı temsillerde birden fazla pano öğesinin panoda bulunabilmesidir. Her panoya alınan öğenin, mevcut kaynakların MIME türlerini belirten bir types alanı vardır. Daha önce aldığım MIME türünü ileterek panosundaki öğenin 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);
  }
};

Bu konuda söylenecek çok şey var. Bu işlemi yalnızca desteklenen tarayıcılarda yaparım.

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

Peki bu nasıl çalışır? macOS Önizleme uygulamasında açık bir resmim var ve bu resmi panoya kopyaladım. Yapıştır'ı tıkladığımda Fugu Greetings uygulaması, uygulamanın panodaki metin ve görselleri görmesine izin vermek isteyip istemediğimi sorar.

Fugu Greetings uygulamasında gösterilen panos izni istemini gösteren resim.
Pano izni istemini gösterir.

Son olarak, izin kabul edildikten sonra resim uygulamaya yapıştırılır. Bunun tersi de geçerlidir. Bir tebrik kartını panoya kopyalayacağım. Ardından Önizleme'yi açıp Dosya'yı ve ardından Kopya Kutusundan 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 bulunduğu macOS Önizleme uygulaması.
MacOS Önizleme uygulamasına yapıştırılmış bir resim.

Badging API

Badging API de faydalı API'lerden biridir. Yüklenebilir bir PWA olarak Fugu Greetings'in, kullanıcıların uygulama paneline veya ana ekrana yerleştirebileceği bir uygulama simgesi vardır. API'yi göstermenin eğlenceli ve kolay bir yolu, Fugu Greetings'da kalem vuruşları sayacı olarak kullanmaktır. pointerdown etkinliği gerçekleştiğinde kalem vuruşları sayacını artıran ve ardından güncellenmiş 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 geliştirme olduğundan yükleme mantığı her zamanki gibidir.

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

Bu örnekte, her sayı için bir kalem darbesi kullanarak bir ile yedi arasındaki sayıları çizdim. Simgedeki rozet sayacı artık yedi.

Tebrik kartına tek bir kalem darbesiyle çizilmiş bir ile yedi arasındaki sayılar.
Kalemle yedi vuruş yaparak 1'den 7'ye kadar olan sayıları çizme.
Fugu Greetings uygulamasında 7 rakamını gösteren rozet simgesi.
Uygulama simgesi rozeti şeklindeki kalem darbesi sayacı.

Periyodik Arka Plan Senkronizasyonu API'si

Her güne yeni bir şeyle başlamak ister misiniz? Fugu Greetings uygulamasının güzel özelliklerinden biri, her sabah yeni bir arka plan resmiyle sizi selam kartınıza başlamaya teşvik etmesidir. Uygulama bunu yapmak için Periodic Background Sync API'yi kullanır.

İlk adım, hizmet çalışanı kaydına düzenli senkronizasyon etkinliği registertir. 'image-of-the-day' adlı bir senkronizasyon etiketini dinler ve en az bir günlük 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, hizmet çalışanında periodicsync etkinliğine kulak vermek olacaktır. Etkinlik etiketi 'image-of-the-day' ise (yani daha önce kaydedilen etiket) günün resmi getImageOfTheDay() işlevi aracılığıyla alınır ve sonuç tüm istemcilere dağıtılır. Böylece istemciler tuvallerini ve önbelleğe alınmış verilerini 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 yine aşamalı bir geliştirmedir. Bu nedenle 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 bu iki dosya da yüklenmez. Hizmet çalışanında dinamik import() (henüz hizmet çalışanı bağlamında desteklenmiyor) yerine klasik importScripts() kullanacağı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 Greetings'da Duvar kağıdı düğmesine bastığınızda, Düzenli Arka Plan Senkronizasyonu API'si aracılığıyla her gün güncellenen günün tebrik kartı resmi gösterilir.

Günün yeni tebrik kartı resmini içeren Fugu Greetings uygulaması.
Duvar kağıdı düğmesine bastığınızda günün resmi gösterilir.

Notification Triggers API

Bazen çok fazla ilham kaynağına sahip olsanız bile, başladığınız tebrik kartını bitirmek için bir dürtüye ihtiyacınız olur. Bu özellik, Notification Triggers API tarafından etkinleştirilir. Kullanıcı olarak, tebrik kartımı bitirmem için hatırlatılmak istediğim zamanı girebilirim. Bu zaman geldiğinde, tebrik kartımın beklediğine dair bir bildirim alacağım.

Hedef zamanın istenmesinden sonra uygulama, bildirimi showTrigger ile planlar. Bu, önceden seçilen hedef tarihi içeren bir TimestampTrigger olabilir. Hatırlatıcı bildirimi yerel olarak tetiklenir. Ağ veya sunucu tarafına gerek yoktur.

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 geliş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'da Hatırlatıcı onay kutusunu işaretlediğimde, tebrik kartımı tamamlamam gerektiğini ne zaman hatırlatılmasını istediğimi soran bir istem gösteriliyor.

Kullanıcıya tebrik kartını ne zaman bitireceğini hatırlatmasını istediği zamanı soran bir istem içeren Fugu Greetings uygulaması.
Tebrik kartı tamamlama hatırlatması için yerel bildirim planlama.

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

Fugu Greetings&#39;dan gelen bir bildirimin tetiklendiğini gösteren macOS Bildirim Merkezi.
Tetiklenen bildirim, macOS Bildirim Merkezi'nde görünür.

Wake Lock API

Wake Lock API'yi de dahil etmek istiyorum. Bazen ilham size gelene kadar ekrana uzun süre bakmanız yeterlidir. Bu durumda en kötü ihtimal ekranın kapanmasıdır. Wake Lock API, bunun olmasını önleyebilir.

İlk adım, navigator.wakelock.request method() ile uyanma kilidi elde etmektir. Ekran uyanık kalma kilidi almak için 'screen' dizesini iletirim. Ardından, uyanma kilidi açıldığında bilgilendirilmek için bir etkinlik işleyici eklerim. Örneğin, sekme görünürlüğü değiştiğinde bu durumla karşılaşabilirsiniz. Bu durumda, sekme tekrar görünür hale geldiğinde uyanma 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 aşamalı bir geliştirmedir. Bu nedenle, yalnızca tarayıcı API'yi desteklediğinde yüklemem gerekir.

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

Fugu Greetings'da, işaretlendiğinde ekranı açık tutan bir Uykusuzluk onay kutusu bulunur.

Uykusuzluk onay kutusu işaretliyse ekran açık kalır.
Uykusuzluk onay kutusu, uygulamayı uyanık tutuyor.

Idle Detection API

Bazen saatlerce ekrana baksanız bile hiçbir şey yapamayabilirsiniz ve tebrik kartınızla ne yapacağınızı bilemeyebilirsiniz. Idle Detection API, uygulamanın kullanıcının boşta kalma süresini algılamasına olanak tanır. Kullanıcı uzun süre boyunca işlem yapmazsa uygulama ilk duruma sıfırlanır ve tuval temizlenir. Boş ekran algılamanın birçok üretim kullanım alanı bildirimlerle ilgili olduğundan (ör. yalnızca kullanıcının o anda etkin olarak kullandığı bir cihaza bildirim göndermek için) bu API şu anda bildirim izni kapsamındadır.

Bildirim izninin verildiğinden emin olduktan sonra boşta kalma algılayıcısını örneklendiririm. Kullanıcıyı ve ekran durumunu içeren boşta kalma değişikliklerini dinleyen bir etkinlik işleyici kaydediyorum. Kullanıcı etkin veya etkin olmayan durumda olabilir ve ekran kilitli veya kilidi açık olabilir. Kullanıcı işlem yapmazsa tuval temizlenir. Boşta kalma algılayıcısına 60 saniyelik bir eşik değeri 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üklerim.

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

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

Kullanıcı uzun süre hareketsiz kaldıktan sonra temiz bir tuval içeren Fugu Greetings uygulaması.
Geçici onay kutusu işaretliyse ve kullanıcı çok uzun süredir etkin değilse tuval temizlenir.

Kapanış

Vay canına. Tek bir örnek uygulamada bu kadar çok API var. Ayrıca, kullanıcının tarayıcısının desteklemediği bir özelliğin indirme maliyetini hiçbir zaman kullanıcıdan ödemesini istemem. Progresif geliştirmeyi kullanarak yalnızca alakalı kodun yüklenmesini sağlıyorum. HTTP/2'de istekler ucuz olduğundan bu kalıp birçok uygulama için işe yarayabilir. Ancak gerçekten büyük uygulamalar için bir paketleyici kullanmayı düşünebilirsiniz.

Yalnızca geçerli tarayıcının desteklediği kod içeren dosya isteklerini gösteren Chrome DevTools Ağ paneli.
Yalnızca mevcut tarayıcının desteklediği kod içeren dosya isteklerini gösteren Chrome Geliştirici Araçları Ağ sekmesi.

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 ilgili tarayıcının özelliklerine göre aşamalı olarak geliştirilir. Bu özelliklerin, uygulamanın yüklü bir uygulama olarak mı yoksa tarayıcı sekmesinde mi çalıştığına bağlı olarak aynı tarayıcıda bile değişebileceğini unutmayın.

Android Chrome&#39;da çalışan Fugu Greetings&#39;in birçok özelliğini gösteren ekran.
Android Chrome'da çalışan Fugu Greetings.
Masaüstü Safari&#39;de çalışan Fugu Greetings. Daha az özellik gösterilmektedir.
Fugu Greetings, masaüstü Safari'de çalışıyor.
Masaüstü Chrome&#39;da çalışan Fugu Greetings&#39;in birçok özelliğini gösteren ekran.
Fugu Greetings, masaüstü Chrome'da çalışıyor.

Fugu Greetings uygulamasıyla ilgileniyorsanız GitHub'da bu uygulamayı alıp çatallayın.

GitHub&#39;daki Fugu Greetings deposu.
GitHub'daki Fugu Greetings uygulaması.

Chromium ekibi, gelişmiş Fugu API'leri konusunda daha iyi bir deneyim sunmak için yoğun şekilde çalışıyor. Uygulamamı geliştirirken aşamalı geliştirmeyi uygulayarak herkesin iyi ve sağlam bir temel deneyim elde etmesini sağlarken daha fazla web platformu API'sini destekleyen tarayıcılar kullanan kullanıcıların daha da iyi bir deneyim elde etmesini sağlıyorum. Uygulamalarınızda aşamalı geliştirmeyle neler yapacağınızı görmeyi sabırsızlıkla bekliyorum.

Teşekkür ederiz

Fugu Greetings'e katkıda bulunan Christian Liebel ve Hemanth HM'ye teşekkür ederim. 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.