Service Workers ile geliştiricilere ağ bağlantısı sorununu çözme olanağı sunduk. Önbelleğe alma ve isteklerin işlenme şekli üzerinde kontrol sahibi olursunuz. Yani kendi desenlerinizi oluşturabilirsiniz. Olası birkaç kalıba ayrı ayrı göz atalım. Ancak uygulamada, URL'ye ve bağlama bağlı olarak bunları birlikte kullanmanız muhtemeldir.
Bu kalıpların bazılarıyla ilgili çalışan bir demo için Trained-to-thrill'e bakın.
Kaynaklar ne zaman depolanır?
Service worker'lar, istekleri önbelleğe almadan bağımsız olarak işlemenize olanak tanır. Bu nedenle, bunları ayrı ayrı göstereceğim. Öncelikle, önbelleği ne zaman kullanmanız gerektiğini belirleyin.
Yükleme sırasında bağımlılık olarak
Service Worker API, size install etkinliği verir. Bunu, diğer etkinliklerle ilgilenmeden önce hazırlanması gereken şeyleri hazırlamak için kullanabilirsiniz. install sırasında, hizmet çalışanın önceki sürümleri çalışmaya ve sayfa sunmaya devam eder. Bu sırada yaptığınız işlemler mevcut hizmet çalışanını kesintiye uğratmamalıdır.
İçin idealdir: CSS, resimler, yazı tipleri, JS, şablonlar veya sitenizin bu sürümünde statik olarak değerlendireceğiniz diğer tüm öğeler.
Getirilememesi durumunda sitenizin tamamen işlevsiz hale gelmesine neden olacak öğeleri ve eşdeğer platforma özgü bir uygulamanın ilk indirme işlemine dahil edeceği öğeleri getirin.
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open('mysite-static-v3').then(function (cache) {
return cache.addAll([
'/css/whatever-v3.css',
'/css/imgs/sprites-v6.png',
'/css/fonts/whatever-v8.woff',
'/js/all-min-v4.js',
// etc.
]);
}),
);
});
event.waitUntil, yüklemenin süresini ve başarısını tanımlamak için bir söz alır. Promise reddedilirse yükleme başarısız kabul edilir ve bu hizmet çalışanı bırakılır (daha eski bir sürüm çalışıyorsa olduğu gibi bırakılır). caches.open() ve cache.addAll() iade sözleri.
Kaynaklardan herhangi biri getirilemezse cache.addAll() çağrısı reddedilir.
trained-to-thrill sitesinde bunu statik öğeleri önbelleğe almak için kullanıyorum.
Yükleme sırasında, bağımlılık olarak değil
Bu, bağımlılık olarak yüklemeye benzer ancak yüklemenin tamamlanmasını geciktirmez ve önbelleğe alma başarısız olursa yüklemenin başarısız olmasına neden olmaz.
İçin idealdir: Hemen ihtiyaç duyulmayan daha büyük kaynaklar (ör. oyunun sonraki seviyelerinde kullanılacak öğeler).
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open('mygame-core-v1').then(function (cache) {
cache
.addAll
// levels 11-20
();
return cache
.addAll
// core assets and levels 1-10
();
}),
);
});
Bu örnek, 11-20. seviyeler için cache.addAll sözünü event.waitUntil'ye geri vermediğinden başarısız olsa bile oyun çevrimdışı olarak kullanılabilir. Elbette bu seviyelerin olmaması durumunu göz önünde bulundurmanız ve eksikse bunları tekrar önbelleğe almayı denemeniz gerekir.
Hizmet çalışanı, etkinliklerin işlenmesini tamamladığı için 11-20. seviyeler indirilirken sonlandırılabilir. Bu nedenle, seviyeler önbelleğe alınmaz. Web Periodic Background Synchronization API bu gibi durumların yanı sıra filmler gibi daha büyük indirme işlemlerini de yönetebilir.
Etkinleştirildiğinde
İçin idealdir: Temizleme ve taşıma.
Yeni bir hizmet çalışanı yüklendikten ve önceki sürüm kullanılmadıktan sonra yeni hizmet çalışanı etkinleştirilir ve bir activate etkinliği alırsınız. Önceki sürüm kaldırıldığı için IndexedDB'de şema taşımalarını gerçekleştirmenin ve kullanılmayan önbellekleri silmenin tam zamanı.
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames
.filter(function (cacheName) {
// Return true if you want to remove this cache,
// but remember that caches are shared across
// the whole origin
})
.map(function (cacheName) {
return caches.delete(cacheName);
}),
);
}),
);
});
Etkinleştirme sırasında fetch gibi etkinlikler sıraya alınır. Bu nedenle, uzun süren bir etkinleştirme sayfa yüklemelerini engelleyebilir. Etkinleştirme işleminizi olabildiğince basit tutun ve yalnızca önceki sürüm etkin durumdayken yapamadığınız işlemler için kullanın.
trained-to-thrill sitesinde bunu eski önbellekleri kaldırmak için kullanıyorum.
Kullanıcı etkileşimi üzerinde
İçin idealdir: Sitenin tamamı çevrimdışı kullanılamadığında ve kullanıcının çevrimdışı kullanılabilir olmasını istediği içeriği seçmesine izin vermeyi tercih ettiğinizde. Örneğin, YouTube'daki bir video, Wikipedia'daki bir makale veya Flickr'daki belirli bir galeri.
Kullanıcıya "Daha sonra oku" veya "Çevrimdışı kaydet" düğmesi gösterin. Tıklandığında, ağdan ihtiyacınız olanı getirir ve önbelleğe yerleştirir.
document.querySelector('.cache-article').addEventListener('click', function (event) {
event.preventDefault();
var id = this.dataset.articleId;
caches.open('mysite-article-' + id).then(function (cache) {
fetch('/get-article-urls?id=' + id)
.then(function (response) {
// /get-article-urls returns a JSON-encoded array of
// resource URLs that a given article depends on
return response.json();
})
.then(function (urls) {
cache.addAll(urls);
});
});
});
Cache API, sayfalardan ve hizmet çalışanlarından kullanılabilir. Bu da doğrudan sayfadan önbelleğe ekleme yapabileceğiniz anlamına gelir.
Ağ yanıtında
İçin idealdir: Kullanıcının gelen kutusu veya makale içerikleri gibi kaynakları sık sık güncelleme. Ayrıca, avatarlar gibi zorunlu olmayan içerikler için de kullanışlıdır ancak dikkatli olunması gerekir.
Bir istek önbellekteki hiçbir öğeyle eşleşmezse isteği ağdan alın, sayfaya gönderin ve aynı anda önbelleğe ekleyin.
Bir dizi URL (ör. avatarlar) için bunu yaparsanız kaynağınızın depolama alanını şişirmemeye dikkat etmeniz gerekir. Kullanıcının disk alanını geri kazanması gerekiyorsa bu işlem için ilk aday olmak istemezsiniz. Artık ihtiyacınız olmayan öğeleri önbellekten kaldırdığınızdan emin olun.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open('mysite-dynamic').then(function (cache) {
return cache.match(event.request).then(function (response) {
return (
response ||
fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
})
);
});
}),
);
});
Belleğin verimli kullanılabilmesi için bir yanıtın/isteğin gövdesini yalnızca bir kez okuyabilirsiniz. Kod örneğinde, ayrı ayrı okunabilecek ek kopyalar oluşturmak için .clone() kullanılıyor.
trained-to-thrill'de bunu Flickr resimlerini önbelleğe almak için kullanıyorum.
Eskiyken yeniden doğrulama
İçin idealdir: En son sürümün gerekli olmadığı, sık sık güncellenen kaynaklar. Avatarlar bu kategoriye girebilir.
Önbelleğe alınmış bir sürüm varsa onu kullanın ancak bir sonraki sefer için güncellemeyi getirin.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open('mysite-dynamic').then(function (cache) {
return cache.match(event.request).then(function (response) {
var fetchPromise = fetch(event.request).then(function (networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
}),
);
});
Bu, HTTP'nin stale-while-revalidate özelliğine çok benzer.
Push mesajında
Push API, service worker üzerine inşa edilmiş başka bir özelliktir. Bu sayede, işletim sisteminin mesajlaşma hizmetinden gelen bir mesaja yanıt olarak hizmet çalışanı uyandırılabilir. Bu durum, kullanıcının sitenizde açık bir sekmesi olmasa bile gerçekleşir. Yalnızca hizmet çalışanı uyandırılır. Bunu yapmak için bir sayfadan izin istersiniz ve kullanıcıya istem gösterilir.
İçin idealdir: Bildirimlerle ilgili içerikler (ör. sohbet mesajı, son dakika haberi veya e-posta). Ayrıca, yapılacaklar listesi güncellemesi veya takvim değişikliği gibi anında senkronizasyondan yararlanan, nadiren değişen içerikler.
Genel olarak, son sonuç bir bildirimdir. Bu bildirime dokunulduğunda ilgili sayfa açılır ve odaklanır. Ayrıca, önceden önbelleklerin güncellenmesi son derece önemlidir. Kullanıcı, push mesajını aldığında internete bağlıdır ancak bildirimle etkileşim kurduğunda internete bağlı olmayabilir. Bu nedenle, bu içeriğin çevrimdışı olarak kullanılabilmesi çok önemlidir.
Bu kod, bildirim göstermeden önce önbellekleri günceller:
self.addEventListener('push', function (event) {
if (event.data.text() == 'new-email') {
event.waitUntil(
caches
.open('mysite-dynamic')
.then(function (cache) {
return fetch('/inbox.json').then(function (response) {
cache.put('/inbox.json', response.clone());
return response.json();
});
})
.then(function (emails) {
registration.showNotification('New email', {
body: 'From ' + emails[0].from.name,
tag: 'new-email',
});
}),
);
}
});
self.addEventListener('notificationclick', function (event) {
if (event.notification.tag == 'new-email') {
// Assume that all of the resources needed to render
// /inbox/ have previously been cached, e.g. as part
// of the install handler.
new WindowClient('/inbox/');
}
});
Arka plan senkronizasyonu açıkken
Arka planda senkronizasyon da service worker üzerine inşa edilmiş bir özelliktir. Arka plan veri senkronizasyonunu tek seferlik veya (son derece sezgisel) aralıklarla isteyebilirsiniz. Bu durum, kullanıcının sitenizde açık bir sekmesi olmasa bile gerçekleşir. Yalnızca hizmet çalışanı uyandırılır. Bir sayfadan bu işlemi yapmak için izin istersiniz ve kullanıcıya istem gösterilir.
İçin idealdir: Acil olmayan güncellemeler, özellikle de güncelleme başına push mesajı gönderilmesi kullanıcılar için çok sık olacak kadar düzenli olarak gerçekleşen güncellemeler (ör. sosyal medya zaman çizelgeleri veya haber makaleleri).
self.addEventListener('sync', function (event) {
if (event.id == 'update-leaderboard') {
event.waitUntil(
caches.open('mygame-dynamic').then(function (cache) {
return cache.add('/leaderboard.json');
}),
);
}
});
Önbelleğin kalıcılığı
Kökeninize, istediği gibi kullanabileceği belirli bir miktar boş alan verilir. Bu ücretsiz alan, tüm kaynak depolama alanları arasında paylaşılır: (Yerel) Depolama, IndexedDB, Dosya Sistemi Erişimi ve elbette Önbellekler.
Alacağınız tutar belirtilmemiştir. Bu süre, cihaza ve depolama koşullarına göre değişir. Ne kadar kazandığınızı öğrenmek için:
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> Number of bytes used.
// quota.quota -> Maximum number of bytes available.
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`You've used ${percentageUsed}% of the available storage.`);
const remaining = quota.quota - quota.usage;
console.log(`You can write up to ${remaining} more bytes.`);
}
Ancak tüm tarayıcı depolama alanlarında olduğu gibi, cihaz depolama alanı baskısı altındaysa tarayıcı verilerinizi silebilir. Tarayıcı, ne yazık ki ne pahasına olursa olsun saklamak istediğiniz filmler ile pek önemsemediğiniz oyun arasındaki farkı anlayamaz.
Bu sorunu çözmek için StorageManager arayüzünü kullanın:
// From a page:
navigator.storage.persist()
.then(function(persisted) {
if (persisted) {
// Hurrah, your data is here to stay!
} else {
// So sad, your data may get chucked. Sorry.
});
Elbette kullanıcının izin vermesi gerekir. Bunun için Permissions API'yi kullanın.
Kullanıcıyı bu akışın bir parçası yapmak önemlidir. Çünkü artık silme işlemini kontrol etmesini bekleyebiliriz. Cihazlarında depolama alanı baskısı varsa ve gereksiz verileri temizlemek bu sorunu çözmüyorsa kullanıcılar hangi öğeleri saklayıp hangilerini kaldıracaklarına karar verebilir.
Bunun çalışması için işletim sistemlerinin, tarayıcıyı tek bir öğe olarak bildirmek yerine depolama alanı kullanımının dökümlerinde "kalıcı" kaynakları platforma özgü uygulamalarla eşdeğer olarak ele alması gerekir.
Öneri yayınlama
Ne kadar önbelleğe alma işlemi yaptığınız önemli değildir. Service worker, önbelleği yalnızca ne zaman ve nasıl kullanacağını söylediğinizde kullanır. İstekleri işleme konusunda birkaç kalıp aşağıda verilmiştir:
Yalnızca önbellek
İçin idealdir: Sitenizin belirli bir "sürümü" için statik olarak kabul edeceğiniz her şey. Bunları yükleme etkinliğinde önbelleğe almış olmanız gerekir. Bu nedenle, bu parametrelerin orada olacağına güvenebilirsiniz.
self.addEventListener('fetch', function (event) {
// If a match isn't found in the cache, the response
// will look like a connection error
event.respondWith(caches.match(event.request));
});
…ancak bu durumu genellikle özel olarak ele almanız gerekmez. Ağdan geri dönüşle önbellek bu durumu kapsar.
Yalnızca ağ
İçin idealdir: Çevrimdışı karşılığı olmayan öğeler (ör. analiz ping'leri, GET olmayan istekler).
self.addEventListener('fetch', function (event) {
event.respondWith(fetch(event.request));
// or don't call event.respondWith, which
// will result in default browser behavior
});
…ancak bu durumu genellikle özel olarak ele almanız gerekmez. Ağdan geri dönüşle önbellek bu durumu kapsar.
Önbellek, ağa geri dönülüyor
İçin idealdir: Çevrimdışı öncelikli uygulamalar geliştirme. Bu gibi durumlarda, isteklerin çoğunu bu şekilde ele alırsınız. Diğer kalıplar, gelen isteğe bağlı olarak istisnadır.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
}),
);
});
Bu, önbellekteki öğeler için "yalnızca önbellek" davranışını, önbelleğe alınmamış öğeler için ise "yalnızca ağ" davranışını (GET olmayan tüm istekler önbelleğe alınamayacağından bu davranışa dahildir) sağlar.
Önbellek ve ağ yarışı
İçin idealdir: Yavaş disk erişimine sahip cihazlarda performans elde etmeye çalıştığınız küçük öğeler.
Eski sabit sürücüler, virüs tarayıcılar ve daha hızlı internet bağlantılarının bazı kombinasyonlarında, ağdan kaynak almak diske gitmekten daha hızlı olabilir. Ancak, kullanıcının cihazında içerik varken ağa gitmek veri israfına yol açabilir. Bu nedenle, bunu göz önünde bulundurun.
// Promise.race rejects when a promise rejects before fulfilling.
// To make a race function:
function promiseAny(promises) {
return new Promise((resolve, reject) => {
// make sure promises are all promises
promises = promises.map((p) => Promise.resolve(p));
// resolve this promise as soon as one resolves
promises.forEach((p) => p.then(resolve));
// reject if all promises reject
promises.reduce((a, b) => a.catch(() => b)).catch(() => reject(Error('All failed')));
});
}
self.addEventListener('fetch', function (event) {
event.respondWith(promiseAny([caches.match(event.request), fetch(event.request)]));
});
Ağ, önbelleğe geri dönüyor
İçin idealdir: Sitenin "sürümü" dışında, sık sık güncellenen kaynaklar için hızlı bir çözüm. Ör. makaleler, avatarlar, sosyal medya zaman çizelgeleri ve oyun skor tabloları.
Bu durumda, online kullanıcılar en güncel içeriği görürken offline kullanıcılar eski bir önbelleğe alınmış sürümü görür. Ağ isteği başarılı olursa büyük olasılıkla önbellek girişini güncellemek istersiniz.
Ancak bu yöntemin kusurları vardır. Kullanıcının bağlantısı aralıklı veya yavaşsa cihazında zaten bulunan, tamamen kabul edilebilir içeriği almadan önce ağın başarısız olmasını beklemesi gerekir. Bu işlem çok uzun sürebilir ve kullanıcı deneyimini olumsuz etkiler. Daha iyi bir çözüm için sonraki kalıbı (Önce önbellek, sonra ağ) inceleyin.
self.addEventListener('fetch', function (event) {
event.respondWith(
fetch(event.request).catch(function () {
return caches.match(event.request);
}),
);
});
Önce önbellek, sonra ağ
İçin idealdir: Sık sık güncellenen içerikler. Ör. makaleler, sosyal medya zaman çizelgeleri ve oyunlardaki skor tabloları.
Bu durumda sayfanın iki istekte bulunması gerekir: biri önbelleğe, diğeri de ağa. Amaç, önce önbelleğe alınmış verileri göstermek, ardından ağ verileri geldiğinde sayfayı güncellemek.
Bazen yeni veriler geldiğinde (ör. oyun skor tablosu) mevcut verileri değiştirebilirsiniz ancak bu durum, daha büyük içeriklerde sorunlara yol açabilir. Temel olarak, kullanıcının okuduğu veya etkileşimde bulunduğu bir şeyi "kaybetmeyin".
Twitter, yeni içeriği eski içeriğin üzerine ekler ve kullanıcının kesintiye uğramaması için kaydırma konumunu ayarlar. Twitter, içeriklerin çoğunu doğrusal bir sırayla sakladığı için bu mümkündür. Bu kalıbı, trained-to-thrill için kopyaladım. Böylece, güncel içerikler gelir gelmez gösterilirken içerikler de mümkün olduğunca hızlı bir şekilde ekrana yansıtılıyor.
Sayfadaki kod:
var networkDataReceived = false;
startSpinner();
// fetch fresh data
var networkUpdate = fetch('/data.json')
.then(function (response) {
return response.json();
})
.then(function (data) {
networkDataReceived = true;
updatePage(data);
});
// fetch cached data
caches
.match('/data.json')
.then(function (response) {
if (!response) throw Error('No data');
return response.json();
})
.then(function (data) {
// don't overwrite newer network data
if (!networkDataReceived) {
updatePage(data);
}
})
.catch(function () {
// we didn't get cached data, the network is our last hope:
return networkUpdate;
})
.catch(showErrorMessage)
.then(stopSpinner);
Hizmet çalışanındaki kod:
Her zaman ağa gidip önbelleği güncelleyin.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open('mysite-dynamic').then(function (cache) {
return fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
});
}),
);
});
trained-to-thrill'de fetch yerine XHR kullanarak bu sorunu çözdüm ve hizmet çalışanına sonucu nereden alacağını söylemek için Accept üst bilgisini kötüye kullandım (sayfa kodu, hizmet çalışanı kodu).
Genel yedek
Önbellekten veya ağdan bir şey yayınlayamazsanız genel bir geri dönüş sağlayın.
İçin idealdir: Avatarlar, başarısız POST istekleri ve "Çevrimdışıyken kullanılamaz" sayfası gibi ikincil görüntüler.
self.addEventListener('fetch', function (event) {
event.respondWith(
// Try the cache
caches
.match(event.request)
.then(function (response) {
// Fall back to network
return response || fetch(event.request);
})
.catch(function () {
// If both fail, show a generic fallback:
return caches.match('/offline.html');
// However, in reality you'd have many different
// fallbacks, depending on URL and headers.
// Eg, a fallback silhouette image for avatars.
}),
);
});
Yedek olarak kullandığınız öğe büyük olasılıkla bir yükleme bağımlılığıdır.
Sayfanız e-posta gönderiyorsa hizmet çalışanınız, e-postayı IndexedDB giden kutusunda saklamaya geri dönebilir ve gönderme işleminin başarısız olduğunu ancak verilerin başarıyla saklandığını söyleyerek sayfaya yanıt verebilir.
Hizmet çalışanı tarafında şablon oluşturma
İçin idealdir: Sunucu yanıtı önbelleğe alınamayan sayfalar.
Sayfalar sunucuda daha hızlı oluşturulur ancak bu, oturum açma durumu gibi önbellekte anlamlı olmayabilecek durum verilerinin dahil edilmesi anlamına gelebilir. Sayfanız bir hizmet çalışanı tarafından kontrol ediliyorsa şablonla birlikte JSON verileri isteyebilir ve bunun yerine bu verileri oluşturmayı seçebilirsiniz.
importScripts('templating-engine.js');
self.addEventListener('fetch', function (event) {
var requestURL = new URL(event.request.url);
event.respondWith(
Promise.all([
caches.match('/article-template.html').then(function (response) {
return response.text();
}),
caches.match(requestURL.path + '.json').then(function (response) {
return response.json();
}),
]).then(function (responses) {
var template = responses[0];
var data = responses[1];
return new Response(renderTemplate(template, data), {
headers: {
'Content-Type': 'text/html',
},
});
}),
);
});
Birleştirme
Bu yöntemlerden yalnızca birini kullanmak zorunda değilsiniz. Hatta istek URL'sine bağlı olarak bunların birçoğunu kullanabilirsiniz. Örneğin, trained-to-thrill şunları kullanır:
- Statik kullanıcı arayüzü ve davranış için yükleme sırasında önbelleğe alma
- Flickr resimleri ve verileri için Ağ yanıtında önbelleğe alma
- Çoğu istek için önbellekten getirme, ağa geri dönme
- Flickr arama sonuçları için önce önbellekten, sonra ağdan getirme
İsteğe bakıp ne yapacağınıza karar verin:
self.addEventListener('fetch', function (event) {
// Parse the URL:
var requestURL = new URL(event.request.url);
// Handle requests to a particular host specifically
if (requestURL.hostname == 'api.example.com') {
event.respondWith(/* some combination of patterns */);
return;
}
// Routing for local URLs
if (requestURL.origin == location.origin) {
// Handle article URLs
if (/^\/article\//.test(requestURL.pathname)) {
event.respondWith(/* some other combination of patterns */);
return;
}
if (/\.webp$/.test(requestURL.pathname)) {
event.respondWith(/* some other combination of patterns */);
return;
}
if (request.method == 'POST') {
event.respondWith(/* some other combination of patterns */);
return;
}
if (/cheese/.test(requestURL.pathname)) {
event.respondWith(
new Response('Flagrant cheese error', {
status: 512,
}),
);
return;
}
}
// A sensible default pattern
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
}),
);
});
Daha fazla bilgi
- Service worker'lar ve Cache Storage API
- JavaScript Promises—an Introduction: Promise'lerle ilgili kılavuz
Kredi
Güzel simgeler için:
- buzzyrobot tarafından Code
- Scott Lewis'in Takvim'i
- Ben Rizzo'nun Network adlı eseri
- Thomas Le Bas tarafından SD
- iconsmind.com tarafından CPU
- trasnik tarafından Trash
- @daosme tarafından bildirim
- Mister Pixel'in Layout'u
- P.J. Onori'nin Cloud adlı eseri
Ayrıca, "Yayınla"yı tıklamadan önce birçok uluma hatasını yakalayan Jeff Posnick'e de teşekkür ederiz.