Dengan Service Worker, kami berhenti mencoba menyelesaikan masalah secara offline, dan memberi developer bagian-bagian yang bergerak untuk menyelesaikannya sendiri. Tindakan ini memberi Anda kontrol atas penyimpanan dalam cache dan cara permintaan ditangani. Artinya, Anda dapat membuat pola Anda sendiri. Mari kita lihat beberapa kemungkinan pola secara terpisah, tetapi dalam praktiknya, Anda mungkin akan menggunakan banyak pola secara bersamaan, bergantung pada URL dan konteks.
Untuk demo kerja beberapa pola ini, lihat Dilatih untuk memberikan sensasi, dan video ini yang menunjukkan dampak performa.
Mesin cache—kapan harus menyimpan resource
Pekerja Layanan memungkinkan Anda menangani permintaan secara terpisah dari penyimpanan dalam cache, jadi saya akan mendemonstrasikannya secara terpisah. Pertama-tama, penyimpanan dalam cache, kapan harus dilakukan?
Saat penginstalan—sebagai dependensi

Pekerja Layanan memberi Anda peristiwa install
. Anda dapat menggunakannya untuk menyiapkan berbagai hal, hal yang harus
siap sebelum Anda menangani peristiwa lainnya. Saat hal ini terjadi, versi sebelumnya dari Pekerja Layanan Anda
masih berjalan dan menayangkan halaman, sehingga hal yang Anda lakukan di sini tidak boleh mengganggunya.
Ideal untuk: CSS, gambar, font, JS, template… pada dasarnya apa pun yang Anda anggap statis untuk "versi" situs Anda tersebut.
Hal-hal ini akan membuat situs Anda sepenuhnya tidak berfungsi jika gagal diambil, hal-hal yang akan menjadi bagian dari download awal oleh aplikasi khusus platform yang setara.
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
menggunakan promise untuk menentukan durasi dan keberhasilan penginstalan. Jika promise menolak, penginstalan dianggap gagal dan Pekerja Layanan ini akan ditinggalkan (jika versi lama berjalan, versi tersebut tidak akan diubah). caches.open()
dan cache.addAll()
menampilkan promise.
Jika salah satu resource gagal diambil, panggilan cache.addAll()
akan ditolak.
Di trained-to-thrill, saya menggunakannya untuk menyimpan aset statis dalam cache.
Saat penginstalan—bukan sebagai dependensi

Hal ini mirip dengan di atas, tetapi tidak akan menunda penyelesaian penginstalan dan tidak akan menyebabkan penginstalan gagal jika cache gagal.
Ideal untuk: resource yang lebih besar yang tidak langsung diperlukan, seperti aset untuk level selanjutnya dalam game.
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
();
}),
);
});
Contoh di atas tidak meneruskan promise cache.addAll
untuk level 11–20 kembali ke
event.waitUntil
, sehingga meskipun gagal, game akan tetap tersedia secara offline. Tentu saja, Anda
harus mempertimbangkan kemungkinan tidak adanya level tersebut dan mencoba menyimpannya dalam cache lagi jika
tidak ada.
Pekerja Layanan dapat dihentikan saat level 11–20 didownload karena telah selesai menangani peristiwa, yang berarti level tersebut tidak akan di-cache. Di masa mendatang, Web Periodic Background Synchronization API akan menangani kasus seperti ini, dan download yang lebih besar seperti film. API tersebut saat ini hanya didukung di fork Chromium.
Saat diaktifkan

Ideal untuk: pembersihan dan migrasi.
Setelah Pekerja Layanan baru diinstal dan versi sebelumnya tidak digunakan, pekerja layanan baru
akan diaktifkan, dan Anda akan mendapatkan peristiwa activate
. Karena versi lama tidak digunakan lagi, ini adalah waktu yang tepat untuk menangani
migrasi skema di IndexedDB
dan juga menghapus cache yang tidak digunakan.
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);
}),
);
}),
);
});
Selama aktivasi, peristiwa lain seperti fetch
dimasukkan ke dalam antrean, sehingga aktivasi yang lama dapat
berpotensi memblokir pemuatan halaman. Buat aktivasi Anda sehemat mungkin, dan hanya gunakan untuk hal-hal yang
tidak dapat Anda lakukan saat versi lama aktif.
Di trained-to-thrill, saya menggunakannya untuk menghapus cache lama.
Terhadap interaksi pengguna

Ideal untuk: saat seluruh situs tidak dapat diakses secara offline, dan Anda memilih untuk mengizinkan pengguna memilih konten yang ingin mereka akses secara offline. Misalnya, video di YouTube, artikel di Wikipedia, galeri tertentu di Flickr.
Berikan tombol "Baca nanti" atau "Simpan untuk offline" kepada pengguna. Saat diklik, ambil data yang Anda perlukan dari jaringan dan tampilkan di cache.
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);
});
});
});
caches API tersedia dari halaman serta pekerja layanan, yang berarti Anda dapat menambahkan ke cache langsung dari halaman.
Pada respons jaringan

Ideal untuk: memperbarui resource secara rutin seperti kotak masuk pengguna, atau konten artikel. Juga berguna untuk konten yang tidak penting seperti avatar, tetapi perlu kehati-hatian.
Jika permintaan tidak cocok dengan apa pun dalam cache, dapatkan dari jaringan, kirim ke halaman, dan tambahkan ke cache secara bersamaan.
Jika Anda melakukannya untuk berbagai URL, seperti avatar, Anda harus berhati-hati agar tidak membebani penyimpanan origin. Jika pengguna perlu mengklaim kembali ruang disk, Anda tidak ingin menjadi kandidat utama. Pastikan Anda menghapus item di cache yang tidak lagi diperlukan.
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;
})
);
});
}),
);
});
Untuk memungkinkan penggunaan memori yang efisien, Anda hanya dapat membaca isi respons/permintaan satu kali. Kode
di atas menggunakan .clone()
untuk membuat salinan
tambahan yang dapat dibaca secara terpisah.
Di trained-to-thrill, saya menggunakannya untuk menyimpan gambar Flickr dalam cache.
Tidak berlaku saat validasi ulang

Ideal untuk: sering memperbarui resource yang tidak memerlukan versi terbaru. Avatar dapat termasuk dalam kategori ini.
Jika ada versi yang di-cache yang tersedia, gunakan versi tersebut, tetapi ambil update untuk lain waktu.
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;
});
}),
);
});
Hal ini sangat mirip dengan stale-while-revalidate HTTP.
Pada pesan push

Push API adalah fitur lain yang dibuat di atas Service Worker. Hal ini memungkinkan Pekerja Layanan diaktifkan sebagai respons terhadap pesan dari layanan pesan OS. Hal ini terjadi meskipun pengguna tidak membuka tab ke situs Anda. Hanya Pekerja Layanan yang diaktifkan. Anda meminta izin untuk melakukannya dari halaman dan pengguna akan diminta.
Ideal untuk: konten yang terkait dengan notifikasi, seperti pesan chat, berita terbaru, atau email. Selain itu, konten yang jarang berubah dan mendapatkan manfaat dari sinkronisasi langsung, seperti pembaruan daftar tugas atau perubahan kalender.
Hasil akhir yang umum adalah notifikasi yang, saat diketuk, akan membuka/memfokuskan halaman yang relevan, tetapi memperbarui cache sebelum hal ini terjadi sangat penting. Pengguna jelas sedang online saat menerima pesan push, tetapi mungkin tidak saat akhirnya berinteraksi dengan notifikasi, sehingga penting untuk menyediakan konten ini secara offline.
Kode ini memperbarui cache sebelum menampilkan notifikasi:
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/');
}
});
Pada sinkronisasi latar belakang

Sinkronisasi latar belakang adalah fitur lain yang dibuat di atas Pekerja Layanan. Dengan ini, Anda dapat meminta sinkronisasi data latar belakang sebagai satu kali, atau pada interval (sangat heuristik). Hal ini terjadi meskipun pengguna tidak membuka tab ke situs Anda. Hanya Pekerja Layanan yang diaktifkan. Anda meminta izin untuk melakukannya dari halaman dan pengguna akan diminta.
Ideal untuk: update yang tidak mendesak, terutama yang terjadi secara rutin sehingga pesan push per update akan terlalu sering bagi pengguna, seperti linimasa media sosial atau artikel berita.
self.addEventListener('sync', function (event) {
if (event.id == 'update-leaderboard') {
event.waitUntil(
caches.open('mygame-dynamic').then(function (cache) {
return cache.add('/leaderboard.json');
}),
);
}
});
Persistensi cache
Origin Anda diberi ruang kosong dalam jumlah tertentu untuk melakukan apa yang diinginkan. Ruang kosong tersebut dibagikan di antara semua penyimpanan origin: Penyimpanan(lokal), IndexedDB, Akses Sistem File, dan tentu saja Cache.
Jumlah yang Anda dapatkan tidak sesuai spesifikasi. Hal ini akan berbeda-beda bergantung pada kondisi perangkat dan penyimpanan. Anda dapat mengetahui jumlah yang Anda miliki melalui:
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.`);
}
Namun, seperti semua penyimpanan browser, browser dapat menghapus data Anda jika perangkat mengalami tekanan penyimpanan. Sayangnya, browser tidak dapat membedakan antara film yang ingin Anda simpan dengan segala cara, dan game yang tidak terlalu Anda minati.
Untuk mengatasi hal ini, gunakan antarmuka StorageManager:
// 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.
});
Tentu saja, pengguna harus memberikan izin. Untuk melakukannya, gunakan Permissions API.
Membuat pengguna menjadi bagian dari alur ini penting, karena kita sekarang dapat mengharapkan mereka mengontrol penghapusan. Jika perangkat mereka mengalami tekanan penyimpanan, dan menghapus data yang tidak penting tidak menyelesaikannya, pengguna dapat menilai item mana yang akan disimpan dan dihapus.
Agar berfungsi, sistem operasi harus memperlakukan origin "tahan lama" sebagai setara dengan aplikasi khusus platform dalam perincian penggunaan penyimpanan, bukan melaporkan browser sebagai satu item.
Saran Penayangan—merespons permintaan
Tidak masalah seberapa banyak penyimpanan dalam cache yang Anda lakukan, pekerja layanan tidak akan menggunakan cache kecuali jika Anda memberi tahu kapan dan caranya. Berikut adalah beberapa pola untuk menangani permintaan:
Hanya cache

Ideal untuk: apa pun yang Anda anggap statis untuk "versi" situs tertentu. Anda seharusnya telah meng-cachenya dalam peristiwa penginstalan, sehingga Anda dapat mengandalkannya.
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));
});
…meskipun Anda tidak sering perlu menangani kasus ini secara khusus, Cache, falling back to network membahasnya.
Khusus jaringan

Ideal untuk: hal-hal yang tidak memiliki padanan offline, seperti ping analisis, permintaan non-GET.
self.addEventListener('fetch', function (event) {
event.respondWith(fetch(event.request));
// or simply don't call event.respondWith, which
// will result in default browser behavior
});
…meskipun Anda tidak sering perlu menangani kasus ini secara khusus, Cache, falling back to network membahasnya.
Cache, kembali ke jaringan

Cocok untuk: mem-build offline-first. Dalam kasus tersebut, berikut cara menangani sebagian besar permintaan. Pola lainnya akan menjadi pengecualian berdasarkan permintaan yang masuk.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
}),
);
});
Hal ini memberi Anda perilaku "khusus cache" untuk hal-hal dalam cache dan perilaku "khusus jaringan" untuk hal apa pun yang tidak di-cache (yang mencakup semua permintaan non-GET, karena tidak dapat di-cache).
Perlombaan cache dan jaringan

Ideal untuk: aset kecil yang Anda gunakan untuk mengejar performa di perangkat dengan akses disk lambat.
Dengan beberapa kombinasi hard drive lama, pemindai virus, dan koneksi internet yang lebih cepat, mendapatkan resource dari jaringan dapat lebih cepat daripada membuka disk. Namun, membuka jaringan saat pengguna memiliki konten di perangkatnya dapat membuang-buang data, jadi perhatikan hal itu.
// Promise.race is no good to us because it rejects if
// a promise rejects before fulfilling. Let's make a proper
// 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)]));
});
Jaringan kembali ke cache

Ideal untuk: perbaikan cepat untuk resource yang sering diperbarui, di luar "versi" situs. Misalnya, artikel, avatar, linimasa media sosial, dan papan peringkat game.
Artinya, Anda memberi pengguna online konten terbaru, tetapi pengguna offline mendapatkan versi cache yang lebih lama. Jika permintaan jaringan berhasil, Anda kemungkinan besar ingin memperbarui entri cache.
Namun, metode ini memiliki kekurangan. Jika pengguna memiliki koneksi yang terputus-putus atau lambat, mereka harus menunggu jaringan gagal sebelum mendapatkan konten yang dapat diterima dengan sempurna di perangkat mereka. Hal ini dapat memerlukan waktu yang sangat lama dan merupakan pengalaman pengguna yang menjengkelkan. Lihat pola berikutnya, Cache lalu jaringan, untuk solusi yang lebih baik.
self.addEventListener('fetch', function (event) {
event.respondWith(
fetch(event.request).catch(function () {
return caches.match(event.request);
}),
);
});
Cache lalu jaringan

Ideal untuk: konten yang sering diperbarui. Misalnya, artikel, linimasa media sosial, dan papan peringkat game.
Hal ini mengharuskan halaman membuat dua permintaan, satu ke cache, dan satu ke jaringan. Idenya adalah menampilkan data yang di-cache terlebih dahulu, lalu memperbarui halaman saat/jika data jaringan tiba.
Terkadang, Anda hanya dapat mengganti data saat ini saat data baru tiba (misalnya, papan peringkat game), tetapi hal itu dapat mengganggu konten yang lebih besar. Pada dasarnya, jangan "menghilangkan" sesuatu yang mungkin dibaca atau berinteraksi dengan pengguna.
Twitter menambahkan konten baru di atas konten lama dan menyesuaikan posisi scroll sehingga pengguna tidak terganggu. Hal ini dimungkinkan karena Twitter sebagian besar mempertahankan urutan yang sebagian besar bersifat linear untuk konten. Saya menyalin pola ini untuk trained-to-thrill agar konten ditampilkan di layar secepat mungkin, sekaligus menampilkan konten terbaru segera setelah konten tersebut tiba.
Kode di halaman:
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);
Kode di Pekerja Layanan:
Anda harus selalu membuka jaringan dan mengupdate cache saat Anda melakukannya.
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;
});
}),
);
});
Di trained-to-thrill, saya mengatasi hal ini dengan menggunakan XHR, bukan fetch, dan menyalahgunakan header Accept untuk memberi tahu Pekerja Layanan tempat mendapatkan hasilnya (kode halaman, kode Pekerja Layanan).
Fallback umum

Jika Anda gagal menayangkan sesuatu dari cache dan/atau jaringan, sebaiknya berikan penggantian generik.
Ideal untuk: gambar sekunder seperti avatar, permintaan POST yang gagal, dan halaman "Tidak tersedia saat offline".
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.
}),
);
});
Item yang Anda gunakan sebagai pengganti kemungkinan adalah dependensi penginstalan.
Jika halaman Anda memposting email, pekerja layanan Anda mungkin kembali menyimpan email di 'kotak keluar' IndexedDB dan merespons dengan memberi tahu halaman bahwa pengiriman gagal, tetapi data berhasil dipertahankan.
Template sisi pekerja layanan

Ideal untuk: halaman yang respons servernya tidak dapat di-cache.
Merender halaman di server akan mempercepat proses, tetapi hal itu dapat berarti menyertakan data status yang mungkin tidak masuk akal dalam cache, misalnya "Login sebagai…". Jika halaman Anda dikontrol oleh pekerja layanan, Anda dapat memilih untuk meminta data JSON beserta template, dan merendernya.
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',
},
});
}),
);
});
Menyatukan
Anda tidak terbatas pada salah satu metode ini. Bahkan, Anda mungkin akan menggunakan banyak URL tersebut, bergantung pada URL permintaan. Misalnya, trained-to-thrill menggunakan:
- cache saat penginstalan, untuk UI dan perilaku statis
- cache pada respons jaringan, untuk gambar dan data Flickr
- mengambil dari cache, kembali ke jaringan, untuk sebagian besar permintaan
- ambil dari cache, lalu jaringan, untuk hasil penelusuran Flickr
Cukup lihat permintaan dan tentukan tindakan yang akan dilakukan:
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);
}),
);
});
…Anda mengerti maksudnya.
Kredit
…untuk ikon yang menarik:
- Code oleh buzzyrobot
- Kalender oleh Scott Lewis
- Network by Ben Rizzo
- SD oleh Thomas Le Bas
- CPU oleh iconsmind.com
- Sampah oleh trasnik
- Notifikasi oleh @daosme
- Layout oleh Mister Pixel
- Cloud oleh P.J. Onori
Dan terima kasih kepada Jeff Posnick karena telah menemukan banyak error yang parah sebelum saya menekan "publikasikan".
Bacaan lebih lanjut
- Pekerja Layanan—Pengantar
- Apakah Service Worker sudah siap?—melacak status penerapan di seluruh browser utama
- Promise JavaScript—Pengantar - panduan promise