The #ChromeDevSummit site is live, happening Nov 12-13 in San Francisco, CA
Check it out for details and request an invite. We'll be diving deep into modern web tech & looking ahead to the platform's future.

Daur Hidup Service Worker

Daur hidup service worker adalah bagiannya yang paling rumit. Jika Anda tidak tahu apa yang berusaha dilakukan dan apa manfaatnya, maka hal ini bisa terasa berat bagi Anda. Namun setelah Anda mengetahui cara kerjanya, Anda bisa menghasilkan pembaruan yang mulus dan tidak kentara pada pengguna, dengan memadukan yang aspek terbaik dari web dan pola asli.

Ini merupakan pendalaman, namun poin-poin di awal setiap bagian membahas hampir semua hal yang perlu Anda ketahui.

Maksud

Maksud dari daur hidup adalah untuk:

  • Memungkinkan offline-terlebih dahulu.
  • Biarkan service worker baru menyiapkan diri sendiri tanpa mengganggu apa yang ada.
  • Memastikan laman dalam-cakupan dikontrol oleh service worker yang sama (atau tanpa service worker) seluruhnya.
  • Memastikan hanya ada satu versi untuk situs Anda yang dijalankan pada satu waktu.

Hal terakhir itu sangat penting. Tanpa service worker, pengguna bisa memuat satu tab ke situs Anda, kemudian membuka tab lain nanti. Hal ini bisa mengakibatkan dua versi situs Anda dijalankan pada waktu yang sama. Kadang-kadang hal ini boleh saja, namun jika Anda sedang berurusan dengan storage, Anda bisa dengan mudah mengakibatkan dua tab memiliki opini sangat berbeda mengenai cara keduanya menangani penyimpanan bersama. Hal ini bisa mengakibatkan kesalahan, atau lebih buruk lagi, kehilangan data.

Perhatian: Pengguna tidak suka kehilangan data. Hal itu akan menyebabkan mereka sangat bersedih.

Service worker pertama

Singkatnya:

  • Kejadian install adalah kejadian pertama yang diambil service worker, dan ini hanya terjadi sekali.
  • Sebuah promise diteruskan ke installEvent.waitUntil() yang akan menunjukkan durasi serta keberhasilan atau kegagalan pemasangan.
  • Service worker tidak akan menerima kejadian seperti fetch dan push sebelum ia berhasil menyelesaikan pemasangan dan menjadi "aktif".
  • Secara default, pengambilan oleh laman tidak akan melalui service worker kecuali jika permintaan laman itu sendiri melalui service worker. Jadi Anda nanti perlu menyegarkan laman untuk melihat efek service worker.
  • clients.claim() bisa menggantikan default ini, dan mengambil kontrol atas laman yang tidak dikontrol.

Perhatikan HTML ini:

<!DOCTYPE html>
Sebuah gambar akan muncul di sini dalam waktu 3 detik:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

Ini mendaftarkan sebuah service worker, dan menambahkan gambar anjing setelah 3 detik.

Inilah service worker tersebut, sw.js:

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

Ia menyimpan cache gambar kucing di cache, dan menyajikannya bila ada permintaan untuk /dog.svg. Akan tetapi, jika Anda menjalankan contoh di atas, Anda akan melihat anjing saat pertama memuat laman. Klik segarkan, dan Anda akan melihat gambar kucing tersebut.

Cakupan dan kontrol

Cakupan default pendaftaran service worker ./ relatif terhadap URL skrip. Berarti, jika Anda mendaftarkan service worker di //example.com/foo/bar.js, ini akan memiliki cakupan default //example.com/foo/.

Kita memanggil laman, service worker, dan service worker bersama berupa clients. Service worker hanya bisa mengontrol klien yang berada dalam cakupan. Setelah klien "dikontrol", pengambilannya akan melalui service worker dalam-cakupan. Anda bisa mendeteksi jika klien dikontrol lewat navigator.serviceWorker.controller yang akan berupa nol atau instance service worker.

Unduh, parse, dan eksekusi

Service worker pertama Anda akan diunduh bila Anda memanggil .register(). Jika skrip Anda gagal mengunduh, mem-parse, atau melontarkan kesalahan dalam eksekusi pertamanya, promise register akan menolak, dan service worker akan dibuang.

Chrome DevTools menampilkan kesalahan di konsol, dan di bagian service worker pada tab aplikasi:

Kesalahan yang ditampilkan di tab DevTools pada service worker

Memasang

Kejadian pertama yang diambil service worker adalah install. Ini akan dipicu begitu service worker dieksekusi, dan hanya dipanggil sekali per service worker. Jika Anda mengubah skrip service worker, browser akan menganggapnya sebagai service worker berbeda, dan akan mendapatkan kejadian install sendiri. Saya akan membahas pembaruan secara detail nanti.

Kejadian install adalah kesempatan Anda untuk meng-cache segala sesuatu yang Anda butuhkan sebelum dapat mengontrol klien. Promise yang Anda teruskan ke event.waitUntil() memungkinkan browser mengetahui kapan Anda selesai memasang, dan apakah pemasangan itu berhasil.

Jika promise Anda ditolak, ini menandakan pemasangan gagal, dan browser membuang service worker. Ia tidak akan pernah mengontrol klien. Ini berarti kita bisa mengandalkan "cat.svg" yang ada di cache dalam kejadian fetch kita. Ini adalah dependensi.

Mengaktifkan

Setelah service worker Anda siap mengontrol klien dan menangani kejadian fungsional seperti push dan sync, Anda akan mendapatkan kejadian activate. Namun itu tidak berarti laman yang disebut .register() akan dikontrol.

Saat pertama Anda memuat demo, walaupun dog.svg diminta lama setelah service worker diaktifkan, ia tidak menangani permintaan tersebut, dan Anda tetap melihat gambar anjing. Default-nya adalah konsistensi, jika laman dimuat tanpa service worker, tidak ada yang akan menjadi sub-sumber dayanya. Jika Anda memuat demo untuk kedua kali (dengan kata lain menyegarkan laman), ia akan dikontrol. Baik laman maupun gambar akan melalui kejadian fetch, dan Anda akan melihat gambar kucing sebagai gantinya.

clients.claim

Anda bisa mengontrol klien yang tidak dikontrol dengan memanggil clients.claim() dalam service worker setelah ia diaktifkan.

Inilah variasi demo di atas yang memanggil clients.claim() dalam kejadian activate-nya. Anda seharusnya akan melihat gambar kucing untuk pertama kali. Saya katakan "seharusnya", karena ini adalah sesuatu yang peka terhadap waktu. Anda hanya akan melihat kucing jika service worker diaktifkan dan clients.claim() berlaku sebelum gambar berusaha dimuat.

Jika Anda menggunakan service worker untuk memuat laman secara berbeda dengan laman yang dimuat lewat jaringan, clients.claim() nanti bisa menyulitkan, karena service worker Anda akan mengakhiri kontrol atas beberapa klien yang telah dimuat tanpa service worker.

Memperbarui service worker

Singkatnya:

  • Pembaruan dipicu:
    • Pada navigasi ke laman dalam-cakupan.
    • Pada kejadian fungsional seperti push dan sync, kecuali jika ada pemeriksaan pembaruan dalam 24 jam sebelumnya.
    • Pada pemanggilan .register() hanya jika URL service worker telah berubah.
  • Header caching pada skrip service worker dipatuhi (hingga 24 jam) saat mengambil pembaruan. Kita akan membuat perilaku penyertaan ini, karen ia akan menemukan orang. Anda barangkali ingin max-age berupa 0 pada skrip service worker.
  • Service worker Anda dianggap diperbarui jika berbeda sedikit saja dengan service worker yang sudah dimiliki browser. (Kita memperluasnya untuk menyertakan juga skrip/modul yang telah diimpor.)
  • Service worker yang telah diperbarui diluncurkan bersama yang sudah ada, dan mendapatkan kejadian install-nya sendiri.
  • Jika service worker baru Anda memiliki kode status bukan OK (misalnya, 404), gagal mem-parse, melontarkan kesalahan selama eksekusi, atau ditolak selama pemasangan, service worker baru akan dibuang, namun yang ada saat ini akan tetap aktif.
  • Setelah berhasil dipasang, service worker yang telah diperbarui akan wait hingga service worker yang ada mengontrol nol klien. (Perhatikan, klien akan tumpang tindih selama penyegaran.)
  • self.skipWaiting() mencegah waiting, yang berarti service worker akan diaktifkan begitu selesai dipasang.

Anggaplah kita mengubah skrip service worker untuk merespons dengan gambar kuda, bukan kucing:

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

Lihat demo di atas. Anda seharusnya tetap melihat gambar kucing. Inilah sebabnya...

Memasang

Perhatikan, saya telah mengubah nama cache dari static-v1 menjadi static-v2. Ini berarti saya bisa menyiapkan cache baru tanpa menimpa apa yang ada di cache saat ini, yang masih digunakan oleh service worker lama.

Pola ini akan membuat cache versi spesifik, semacam aset yang akan dibundel oleh aplikasi asli bersama file yang dapat dieksekusi. Anda mungkin juga memiliki cache yang bukan versi spesifik, misalnya avatars.

Setelah berhasil memasangnya, service worker yang telah diperbarui akan menunda aktivasi hingga service worker yang ada tidak lagi mengontrol klien. Keadaan ini disebut "menunggu", dan inilah cara browser memastikan bahwa hanya ada satu versi service worker yang berjalan untuk satu waktu.

Jika Anda menjalankan demo yang telah diperbarui, Anda seharusnya tetap melihat gambar kucing, karena service worker V2 belum diaktifkan. Anda bisa melihat service worker baru menunggu di tab "Application" pada DevTools:

DevTools menampilkan service worker baru yang sedang menunggu

Bahkan jika Anda hanya memiliki satu tab dibuka ke demo, penyegaran laman tidak cukup untuk memungkinkan versi baru mengambil alih. Hal ini dikarenakan cara kerja navigasi browser. Bila Anda mengarahkan, laman saat ini tidak akan hilang hingga header respons diterima, dan bahkan laman saat ini mungkin tetap dibuka jika respons memiliki header Content-Disposition. Karena tumpang tindih ini, service worker saat ini selalu mengontrol klien selama penyegaran.

Untuk mendapatkan pembaruan, tutup atau arahkan meninggalkan semua tab dengan menggunakan service worker saat ini. Maka, bila Anda mengarahkan ke demo lagi, Anda akan melihat gambar kuda.

Pola ini serupa dengan cara pembaruan Chrome. Pembaruan pada unduhan Chrome di latar belakang, namun tidak diterapkan hingga Chrome dimulai ulang. Pada saat ini, Anda bisa tetap menggunakan versi saat ini tanpa kendala. Akan tetapi, hal ini menjengkelkan selama development, namun DevTools memiliki cara untuk membuatnya lebih mudah, yang akan saya bahas nanti dalam artikel ini.

Mengaktifkan

Ini akan aktif setelah service worker lama hilang, dan service worker baru dapat mengontrol klien. Inilah saat yang ideal untuk melakukan hal-hal yang tidak bisa Anda lakukan saat service worker lama sedang digunakan, misalnya melakukan migrasi database dan mengosongkan cache.

Dalam demo di atas, saya memelihara daftar cache yang saya harapkan akan ada, dan dalam kejadian activate saya menghilangkan yang lainnya, yang akan membuang cache static-v1 lama.

Perhatian: Anda tidak boleh memperbarui dari versi sebelumnya. Service worker mungkin memiliki banyak versi lama.

Jika Anda meneruskan promise ke event.waitUntil() akan menjadi penyangga kejadian fungsional (fetch, push, sync dll.) hingga promise teratasi. Jadi bila kejadian fetch Anda dipicu, aktivasi akan selesai sepenuhnya.

Perhatian: Cache Storage API adalah "storage asal" (seperti localStorage, dan IndexedDB). Jika Anda menjalankan banyak situs pada asal yang sama (misalnya, yourname.github.io/myapp), berhati-hatilah agar Anda tidak menghapus cache untuk situs Anda yang lainnya. Untuk menghindarinya, berikan awalan yang unik pada nama cache Anda pada situs saat ini, misalnya myapp-static-v1, dan jangan sentuh cache kecuali jika memulai dengan myapp-.

Lewati tahap menunggu

Tahap menunggu berarti Anda hanya menjalankan satu versi situs saat itu, namun jika Anda tidak membutuhkan fitur itu, Anda bisa mengaktifkan service worker baru lebih dini dengan memanggil self.skipWaiting().

Ini menyebabkan service worker Anda menyingkirkan service worker yang saat ini aktif dan mengaktifkannya sendiri begitu memasuki tahap menunggu (atau segera jika sudah dalam tahap menunggu). Ini tidak menyebabkan service worker Anda melewati pemasangan, cuma menunggu.

Hal ini tidak begitu penting bila Anda memanggil skipWaiting(), asalkan hal itu selama menunggu atau sebelum menunggu. Sudah umum memanggilnya dalam kejadian install:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

Namun Anda mungkin perlu memanggilnya sebagai hasil postMessage() ke service worker. Anda mungkin perlu skipWaiting() interaksi pengguna berikut.

Inilah demo yang menggunakan skipWaiting(). Anda seharusnya akan melihat gambar sapi tanpa harus mengarahkan navigasi ke lain. Seperti clients.claim() ini menjadi balapan, jadi Anda hanya akan melihat sapi jika service worker baru mengambil, memasang, dan mengaktifkan sebelum laman berusaha memuat gambar.

Perhatian: skipWaiting() berarti service worker baru Anda mungkin saja mengontrol laman yang telah dimuat bersama versi lama. Ini berarti sebagian pengambilan laman Anda akan ditangani oleh service worker Anda yang lama, namun service worker baru akan menangani pengambilan selanjutnya. Jika hal itu akan merusak suatu hal, jangan gunakan skipWaiting().

Pembaruan manual

Sebagaimana disebutkan sebelumnya, browser akan memeriksa pembaruan secara otomatis setelah kejadian fungsional dan navigasi, namun Anda juga bisa memicunya secara manual:

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

Jika Anda memperkirakan pengguna akan menggunakan situs dalam waktu lama tanpa memuat ulang, Anda mungkin perlu memanggil update() dengan interval (misalnya setiap jam).

Hindari mengubah URL skrip service worker Anda

Jika Anda telah membaca entri blog saya mengenai praktik terbaik melakukan cache, Anda mungkin mempertimbangkan memberikan URL unii ke setiap versi service worker. Jangan lakukan ini! Ini biasanya adalah kebiasaan buruk untuk service worker, cukup perbarui skrip di lokasi saat ini.

Ini akan menghadapkan Anda pada masalah seperti ini:

  1. index.html mendaftarkan sw-v1.js sebagai service worker.
  2. sw-v1.js menyimpan ke cache dan menyajikan index.html sehingga ia bekerja offline terlebih dahulu.
  3. Anda memperbarui index.html sehingga ia mendaftarkan sw-v2.js Anda yang baru dan cemerlang.

Jika Anda melakukan hal di atas, pengguna tidak akan mendapatkan sw-v2.js, karena sw-v1.js menyajikan versi lama index.html dari cache-nya. Anda menempatkan diri pada posisi di mana Anda perlu memperbarui service worker agar dapat memperbarui service worker. Ew.

Akan tetapi, untuk demo di atas, saya telah mengubah URL service worker. Begitulah, demi demo, Anda bisa beralih antar versi. Ini bukan sesuatu yang akan saya lakukan di produksi.

Mamudahkan development

Daur hidup service worker dibuat dengan mempertimbangkan pengguna, namun selama development ini agak menyakitkan. Syukurlah ada beberapa alat untuk membantu:

Perbarui saat muat ulang

Inilah favorit saya.

DevTools menampilkan 'update on reload'

Ini mengubah daur hidup menjadi ramah-developer. Setiap navigasi akan:

  1. Ambil ulang service worker.
  2. Pasang sebagai versi baru sekalipun secara byte identik, maksudnya kejadian install Anda akan dijalankan dan cache diperbarui.
  3. Lewati tahap menunggu sehingga service worker baru diaktifkan.
  4. Arahkan ke laman.

Ini berarti Anda akan mendapatkan pembaruan pada setiap navigasi (termasuk penyegaran) tanpa harus memuat ulang atau menutup tab.

Lewati menunggu

DevTools menampilkan 'skip waiting'

Jika Anda memiliki service worker yang sedang menunggu, Anda bisa memilih "skip waiting" di DevTools untuk segera mempromosikannya ke "active".

Muat ulang geser

Jika Anda memaksa muat ulang laman (muat ulang geser) ini akan melangkahi service worker sama sekali. Ini tidak akan dikontrol. Fitur ini ada dalam spesifikasi, sehingga akan berfungsi di browser lain yang mendukung service worker.

Menangani pembaruan

Service worker didesain sebagai bagian dari web yang dapat diperluas. Gagasannya adalah karena kita, sebagai developer browser, mengakui bahwa kita tidak lebih baik dalam hal development web dibandingkan developer web. Dan dengan demikian, kita seharusnya tidak menyediakan API tingkat tinggi yang sempit yang mengatasi masalah tertentu dengan menggunakan pola yang kita sukai, dan sebagai gantinya memberi Anda akses ke isi perut browser dan memungkinkan Anda melakukannya sesuka hati, dengan cara yang paling baik bagi para pengguna Anda.

Jadi, untuk memungkinkan banyak pola sebisa kita, daur pembaruan keseluruhan dapat diamati:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has as skipped waiting and become
  // the new active worker. 
});

Anda telah lolos!

Fiuh! Itu adalah teori teknis yang banyak. Tetap ikuti dalam beberapa minggu ke depan karena kita akan mendalami beberapa aplikasi praktis dari hal tersebut di atas.