Dengan Pekerja Layanan, kami berhenti mencoba menyelesaikan masalah secara offline, dan memberi developer bagian yang bergerak untuk menyelesaikannya sendiri. Hal ini memberi Anda kontrol atas penyimpanan cache dan cara penanganan permintaan. Artinya, Anda bisa membuat pola sendiri. Mari kita lihat beberapa kemungkinan pola secara terpisah, tetapi dalam praktiknya, Anda mungkin akan menggunakan banyak pola tersebut secara bersamaan, bergantung pada URL dan konteks.
Untuk melihat demo praktis dari beberapa pola ini, lihat Trained-to-thrill, dan video ini yang menunjukkan dampak performa.
Mesin cache—kapan menyimpan resource
Pekerja Layanan memungkinkan Anda menangani permintaan secara terpisah dari cache, jadi saya akan mendemonstrasikannya secara terpisah. Pertama, penyimpanan dalam cache, kapan harus dilakukan?
Saat penginstalan—sebagai dependensi
Service Worker memberi Anda peristiwa install
. Anda dapat menggunakannya untuk menyiapkan berbagai hal, hal yang harus siap sebelum Anda menangani peristiwa lainnya. Saat ini terjadi, Service Worker versi sebelumnya masih berjalan dan menayangkan halaman, sehingga hal-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 tersebut.
Hal-hal berikut akan membuat situs Anda sepenuhnya tidak berfungsi jika gagal diambil, hal-hal yang akan dijadikan 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 ditolak, penginstalan akan dianggap gagal dan Service Worker ini akan diabaikan (jika versi lama masih berjalan, versi tersebut akan dibiarkan). caches.open()
dan cache.addAll()
menampilkan promise.
Jika ada resource yang gagal diambil, panggilan cache.addAll()
akan ditolak.
Di training-to-thrill, saya menggunakan ini untuk menyimpan cache aset statis.
Saat menginstal—bukan sebagai dependensi
Hal ini serupa dengan di atas, tetapi tidak akan menunda penyelesaian penginstalan dan tidak akan menyebabkan penginstalan gagal jika caching gagal.
Ideal untuk: resource lebih besar yang tidak langsung diperlukan, seperti aset untuk level game selanjutnya.
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
. Jadi, meskipun gagal, game akan tetap tersedia secara offline. Tentu saja, Anda harus
menangani kemungkinan tidak adanya level tersebut dan mencoba lagi dalam cache jika
tidak ada level tersebut.
Service Worker dapat dimatikan saat level 11–20 didownload karena sudah selesai menangani peristiwa, yang berarti tidak akan disimpan dalam cache. Ke depannya, 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 pengaktifan
Ideal untuk: pembersihan dan migrasi.
Setelah Service Worker baru diinstal dan versi sebelumnya tidak digunakan, Service Worker baru akan diaktifkan, dan Anda akan mendapatkan peristiwa activate
. Karena versi lama sudah tidak diperlukan, inilah saat 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
akan dimasukkan ke dalam antrean, sehingga aktivasi yang lama berpotensi memblokir pemuatan halaman. Jaga aktivasi Anda seramping mungkin, dan hanya gunakan untuk hal-hal yang tidak bisa Anda lakukan saat versi lama aktif.
Di training-to-thrill, saya menggunakan ini untuk menghapus cache lama.
Tentang interaksi pengguna
Ideal untuk: ketika seluruh situs tidak dapat diakses secara offline, dan Anda memilih untuk mengizinkan pengguna memilih konten yang ingin tersedia secara offline. Mis. video tentang YouTube, artikel di Wikipedia, atau galeri khusus di Flickr.
Berikan tombol "Baca nanti" atau "Simpan untuk offline". Saat diklik, ambil yang Anda butuhkan dari jaringan dan munculkan 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);
});
});
});
cache API tersedia dari halaman serta pekerja layanan, yang berarti Anda dapat menambahkan ke cache langsung dari halaman.
Di respons jaringan
Ideal untuk: referensi yang sering diperbarui, seperti kotak masuk pengguna atau konten artikel. Juga berguna untuk konten tidak penting seperti avatar, tetapi diperlukan kehati-hatian.
Jika permintaan tidak cocok dengan apa pun dalam cache, dapatkan permintaan dari jaringan, kirim ke halaman, dan tambahkan ke cache secara bersamaan.
Jika Anda melakukan hal ini untuk berbagai URL, seperti avatar, Anda harus berhati-hati agar tidak menggelembungkan penyimpanan origin. Jika pengguna perlu memulihkan kapasitas disk, Anda tidak ingin menjadi kandidat utama. Pastikan Anda membuang item dalam cache yang tidak diperlukan lagi.
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;
})
);
});
}),
);
});
Agar penggunaan memori efisien, Anda hanya dapat membaca isi respons/permintaan satu kali. Kode
di atas menggunakan .clone()
untuk membuat salinan tambahan
yang dapat dibaca secara terpisah.
Saat trainer-to-thrill menggunakan ini untuk menyimpan gambar Flickr dalam cache.
Tidak berlaku saat divalidasi ulang
Ideal untuk: sering mengupdate resource jika versi terbaru tidak terlalu penting. Avatar dapat termasuk dalam kategori ini.
Gunakan versi yang di-cache, 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-temporary-revalidate HTTP.
Di pesan push
Push API adalah fitur lain yang dibuat selain Service Worker. Hal ini memungkinkan Service Worker diaktifkan sebagai respons terhadap pesan dari layanan pesan OS. Hal ini terjadi meskipun pengguna tidak memiliki tab yang terbuka untuk situs Anda. Hanya Pekerja Layanan yang diaktifkan. Anda akan meminta izin untuk melakukannya dari halaman dan pengguna akan diminta.
Ideal untuk: konten yang terkait dengan notifikasi, seperti pesan chat, artikel berita terbaru, atau email. Selain itu, perubahan konten yang jarang terjadi yang memanfaatkan sinkronisasi langsung, seperti pembaruan daftar tugas atau perubahan kalender.
Hasil akhir yang umum adalah notifikasi yang, saat diketuk, akan membuka/memfokuskan halaman yang relevan. Namun, pembaruan cache sebelum hal ini terjadi extremely penting. Pengguna jelas online pada saat menerima pesan push, tetapi mungkin tidak demikian ketika akhirnya berinteraksi dengan notifikasi, jadi membuat konten ini tersedia secara offline adalah hal yang penting.
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/');
}
});
Saat sinkronisasi latar belakang
Sinkronisasi latar belakang adalah fitur lain yang dibuat di atas Pekerja Layanan. Fitur ini memungkinkan Anda meminta sinkronisasi data latar belakang sebagai satu kali atau pada interval (yang sangat heuristik). Hal ini terjadi meskipun pengguna tidak memiliki tab yang terbuka ke situs Anda. Hanya Service Worker yang diaktifkan. Anda meminta izin untuk melakukannya dari halaman dan pengguna akan diminta untuk melakukannya.
Ideal untuk: update yang tidak mendesak, terutama update 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 diinginkannya. Ruang kosong tersebut digunakan bersama oleh semua penyimpanan asal: (local) Storage, IndexedDB, File System Access, dan tentu saja Cache.
Jumlah yang Anda dapatkan tidak ditentukan. Jumlah ini akan berbeda bergantung pada perangkat dan kondisi penyimpanan. Anda dapat mengetahui jumlah yang Anda peroleh melalui:
navigator.storageQuota.queryInfo('temporary').then(function (info) {
console.log(info.quota);
// Result: <quota in bytes>
console.log(info.usage);
// Result: <used data in bytes>
});
Namun, seperti semua penyimpanan browser, browser bebas membuang data Anda jika perangkat mengalami tekanan penyimpanan. Sayangnya, browser tidak dapat membedakan antara film yang ingin Anda pertahankan dengan segala biaya, dan game yang tidak terlalu penting bagi Anda.
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.
Penting untuk menjadikan pengguna sebagai bagian dari alur ini, karena sekarang kita dapat mengharapkan pengguna dapat mengontrol penghapusan. Jika perangkat mereka mengalami tekanan penyimpanan, dan menghapus data yang tidak penting tidak menyelesaikan masalah, pengguna dapat menilai item mana yang akan disimpan dan dihapus.
Agar berfungsi, sistem operasi harus memperlakukan origin yang "tahan lama" sebagai setara dengan aplikasi khusus platform dalam pengelompokan penggunaan penyimpanannya, bukan melaporkan browser sebagai item tunggal.
Saran Penayangan—merespons permintaan
Tidak masalah berapa banyak penyimpanan cache yang Anda lakukan, pekerja layanan tidak akan menggunakan cache kecuali jika Anda memberitahukan kapan dan bagaimana caranya. Berikut adalah beberapa pola untuk menangani permintaan:
Khusus cache
Ideal untuk: apa pun yang Anda anggap statis untuk "versi" tertentu situs Anda. Anda harus menyimpannya dalam cache saat peristiwa instal, 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 menangani kasus ini secara khusus, Cache, fallback ke jaringan akan mencakupnya.
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 menangani kasus ini secara khusus, Cache, fallback ke jaringan akan mencakupnya.
Cache, fallback ke jaringan
Ideal untuk: membangun offline-first. Dalam kasus tersebut, beginilah cara Anda akan menangani sebagian besar permintaan. Pola lainnya akan menjadi pengecualian berdasarkan permintaan masuk.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
}),
);
});
Ini akan memberi Anda perilaku "hanya cache" untuk item yang ada di cache dan perilaku "khusus jaringan" untuk apa pun yang tidak di-cache (yang mencakup semua permintaan non-GET, karena permintaan tersebut tidak dapat di-cache).
Ras cache dan jaringan
Ideal untuk: aset kecil yang performanya di perangkat dengan akses disk yang lambat.
Dengan beberapa kombinasi hard drive lama, pemindai virus, dan koneksi internet yang lebih cepat, mendapatkan resource dari jaringan bisa lebih cepat daripada membuka disk. Namun, masuk ke jaringan ketika pengguna memiliki konten di perangkat mereka dapat menjadi pemborosan data, jadi perlu diingat hal ini.
// 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 fallback ke cache
Ideal untuk: perbaikan cepat untuk resource yang sering diupdate, di luar "versi" situs. Misalnya, artikel, avatar, linimasa media sosial, dan papan skor game.
Artinya, Anda memberikan konten terbaru kepada pengguna online, tetapi pengguna offline mendapatkan versi lama yang di-cache. Jika permintaan jaringan berhasil, kemungkinan besar Anda perlu memperbarui entri cache.
Namun, metode ini memiliki kekurangan. Jika koneksinya terputus-putus atau lambat, pengguna harus menunggu sampai jaringan gagal sebelum mendapatkan konten yang dapat diterima di perangkat mereka. Hal ini dapat memakan 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. Mis. artikel, linimasa media sosial, dan game. papan peringkat.
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 dapat langsung mengganti data saat ini saat data baru tiba (mis. papan peringkat game), tetapi hal tersebut dapat mengganggu konten yang lebih besar. Pada dasarnya, jangan "menghilangkan" sesuatu yang mungkin sedang dibaca atau berinteraksi dengan pengguna.
Twitter menambahkan konten baru di atas konten lama dan menyesuaikan posisi scroll sehingga pengguna tidak terganggu. Hal ini memungkinkan karena Twitter sebagian besar mempertahankan urutan yang sebagian besar bersifat linier ke konten. Saya menyalin pola ini untuk train-to-thrill agar bisa menampilkan konten di layar secepat mungkin, sekaligus menampilkan konten terbaru segera setelah konten tersebut masuk.
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 Service Worker:
Anda harus selalu masuk ke jaringan dan memperbarui cache selama Anda berjalan.
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 train-to-thrill saya mengatasi hal ini dengan menggunakan XHR, bukan pengambilan, dan menyalahgunakan header Accept untuk memberi tahu Service Worker tempat mendapatkan hasil (kode halaman, kode Service Worker).
Penggantian umum
Jika gagal menayangkan sesuatu dari cache dan/atau jaringan, Anda mungkin perlu memberikan fallback 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 menjadi tujuan penggantian kemungkinan besar adalah dependensi penginstalan.
Jika halaman Anda memposting email, pekerja layanan Anda dapat melakukan fallback untuk menyimpan email di 'outbox' 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 itu dapat berarti menyertakan data status yang mungkin tidak masuk akal dalam cache, mis. "Login sebagai...". Jika halaman 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 dibatasi pada salah satu metode ini. Bahkan, Anda mungkin akan menggunakan banyak di antaranya, bergantung pada URL permintaan. Misalnya, train-to-thrill menggunakan:
- cache saat penginstalan, untuk UI statis dan perilaku
- 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 tersebut dan putuskan apa yang harus 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);
}),
);
});
...begitulah gambarannya.
Kredit
...untuk ikon-ikon yang menarik:
- Kode oleh buzzyrobot
- Kalender oleh Scott Lewis
- Jaringan oleh Ben Rizzo
- SD oleh Thomas Le Bas
- CPU dari iconmind.com
- Sampah oleh trasnik
- Notification dari @daosme
- Layout berdasarkan Mister Pixel
- Cloud oleh P.J. Onori
Dan terima kasih kepada Jeff Posnick karena menemukan banyak error sebelum saya mengklik "publish".
Bacaan lebih lanjut
- Pekerja Layanan—Pengantar
- Is Service Worker ready?—melacak status implementasi di seluruh browser utama
- Promise JavaScript—Pengantar - panduan untuk promise