Resep aplikasi offline





Selamat siang teman!



Untuk perhatian Anda, saya persembahkan terjemahan dari artikel Jake Archibald yang sangat bagus "Buku Masakan Offline" yang ditujukan untuk berbagai kasus penggunaan API ServiceWorker dan API Cache.



Diasumsikan bahwa Anda sudah familiar dengan dasar-dasar teknologi ini, karena akan ada banyak kode dan sedikit kata.



Jika belum terbiasa, mulailah dengan MDN lalu kembali. Berikut artikel bagus lainnya tentang pekerja layanan khusus untuk visual.



Tanpa kata pengantar lebih lanjut.



Kapan harus menghemat sumber daya?



Pekerja memungkinkan Anda memproses permintaan secara independen dari cache, jadi kami akan mempertimbangkannya secara terpisah.



Pertanyaan pertama adalah kapan Anda harus menyimpan sumber daya?



Saat dipasang sebagai dependensi






Salah satu peristiwa yang terjadi ketika seorang pekerja sedang berjalan adalah peristiwa penginstalan. Acara ini dapat digunakan untuk persiapan penanganan acara lainnya. Saat pekerja baru diinstal, pekerja lama akan terus melayani halaman, jadi menangani acara install tidak akan merusaknya.



Cocok untuk gaya cache, gambar, skrip, template ... secara umum, untuk semua file statis yang digunakan pada halaman.



Kita berbicara tentang file-file yang tanpanya aplikasi tidak dapat berfungsi seperti file yang disertakan dalam unduhan awal aplikasi asli.



self.addEventListener('install', event => {
    event.waitUntil(
        caches.open('mysite-static-v3')
            .then(cache => cache.addAll([
                '/css/whatever-v3.css',
                '/css/imgs/sprites-v6.png',
                '/css/fonts/whatever-v8.woff',
                '/js/all-min-v4.js'
                //  ..
            ]))
    )
})


event.waitUntil menerima janji untuk menentukan durasi dan hasil penginstalan. Jika janji ditolak, pekerja tidak akan dipasang. caches.open dan cache.addAll mengembalikan janji. Jika salah satu sumber daya tidak tersedia,

panggilan ke cache.addAll akan ditolak.



Saat dipasang bukan sebagai ketergantungan






Ini mirip dengan contoh sebelumnya, tetapi dalam hal ini kami tidak menunggu hingga penginstalan selesai, sehingga tidak akan membatalkan penginstalan.



Cocok untuk sumber daya besar yang tidak diperlukan saat ini, seperti sumber daya untuk level game selanjutnya.



self.addEventListener('install', event => {
    event.waitUntil(
        caches.open('mygame-core-v1')
            .then(cache => {
                cache.addAll(
                    //  11-20
                )
                return cache.addAll(
                    //     1-10
                )
            })
    )
})


Kami tidak meneruskan janji cache.addAll ke event.waitUntil untuk level 11-20, jadi jika ditolak, game akan tetap berjalan offline. Tentu saja, Anda harus menangani kemungkinan masalah dengan caching level pertama dan, misalnya, coba caching lagi jika terjadi kegagalan.



Pekerja bisa dihentikan setelah memproses kejadian sebelum level 11-20 di-cache. Artinya, level ini tidak akan disimpan. Di masa mendatang, direncanakan untuk menambahkan antarmuka pemuatan latar belakang ke pekerja untuk mengatasi masalah ini, serta mengunduh file besar seperti film.



Approx. Per.: Antarmuka ini diimplementasikan pada akhir 2018 dan disebut Background Fetch , tetapi sejauh ini hanya berfungsi di Chrome dan Opera (68% menurut CanIUse ).



Setelah aktivasi






Cocok untuk menghapus cache dan migrasi lama.



Setelah menginstal pekerja baru dan menghentikan yang lama, pekerja baru diaktifkan dan kami menerima acara pengaktifan. Ini adalah peluang bagus untuk mengganti sumber daya dan menghapus cache lama.



self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys()
            .then(cacheNames => Promise.all(
                cacheNames.filter(cacheName => {
                    //  true, ,     ,
                    //  ,      
                }).map(cacheName => caches.delete(cacheName))
            ))
    )
})


Selama aktivasi, peristiwa lain seperti pengambilan dimasukkan ke dalam antrean, sehingga aktivasi yang lama secara teoritis dapat memblokir halaman. Jadi gunakan tahap ini hanya untuk hal-hal yang tidak dapat Anda lakukan dengan pekerja lama.



Saat acara khusus terjadi






Cocok jika seluruh situs tidak dapat dibuat offline. Dalam hal ini, kami memberi pengguna kemampuan untuk memutuskan apa yang akan disimpan dalam cache. Misalnya, video Youtube, halaman Wikipedia, atau galeri gambar di Flickr.



Beri pengguna tombol Baca Nanti atau Simpan. Saat tombol diklik, dapatkan sumber daya dan tulis ke cache.



document.querySelector('.cache-article').addEventListener('click', event => {
    event.preventDefault()

    const id = event.target.dataset.id
    caches.open(`mysite-article ${id}`)
        .then(cache => fetch(`/get-article-urls?id=${id}`)
            .then(response => {
                // get-article-urls     JSON
                //  URL   
                return response.json()
            }).then(urls => cache.addAll(urls)))
})


Antarmuka caching tersedia di halaman, seperti pekerja itu sendiri, jadi kita tidak perlu memanggil pekerja itu untuk menghemat sumber daya.



Saat menerima tanggapan






Cocok untuk sumber daya yang sering diperbarui seperti kotak surat pengguna atau konten artikel. Juga cocok untuk konten kecil seperti avatar, tetapi hati-hati dalam kasus ini.



Jika sumber daya yang diminta tidak ada dalam cache, kami mendapatkannya dari jaringan, mengirimkannya ke klien dan menuliskannya ke cache.



Jika Anda meminta beberapa URL, seperti jalur avatar, pastikan ini tidak melebihi penyimpanan asal (asal - protokol, host, dan port) - jika pengguna perlu mengosongkan ruang disk, Anda tidak boleh menjadi yang pertama. Berhati-hatilah saat menghapus sumber daya yang tidak perlu.



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.open('mysite-dynamic')
            .then(cache => cache.match(event.request)
                .then(response => response || fetch(event.request)
                    .then(response => {
                        cache.put(event.request, response.clone())
                        return response
                    })))
    )
})


Untuk menggunakan memori secara efisien, kami hanya membaca isi respons satu kali. Contoh di atas menggunakan metode klon untuk membuat salinan respons. Ini dilakukan untuk secara bersamaan mengirim respons ke klien dan menuliskannya ke cache.



Selama pemeriksaan kebaruan






Cocok untuk memperbarui sumber daya yang tidak memerlukan versi terbaru. Ini juga bisa diterapkan pada avatar.



Jika sumber daya ada di cache, kami menggunakannya, tetapi mendapatkan pembaruan pada permintaan berikutnya.



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.open('mysite-dynamic')
            .then(cache => cache.match(event.request)
                .then(response => {
                    const fetchPromise = fetch(event.request)
                        .then(networkResponse => {
                            cache.put(event.request, networkResponse.clone())
                            return networkResponse
                        })
                        return response || fetchPromise
                    }))
    )
})


Saat Anda menerima pemberitahuan push






API Push adalah abstraksi atas pekerja. Ini memungkinkan pekerja untuk berjalan sebagai tanggapan atas pesan dari sistem operasi. Selain itu, ini terjadi terlepas dari penggunanya (saat tab browser ditutup). Sebuah halaman biasanya mengirimkan permintaan izin kepada pengguna untuk melakukan tindakan tertentu.



Cocok untuk konten yang bergantung pada notifikasi, seperti pesan chat, feed berita, email. Juga digunakan untuk menyinkronkan konten seperti tugas dalam daftar atau tanda centang di kalender.



Hasilnya adalah pemberitahuan yang, ketika diklik, membuka halaman terkait. Namun, sangat penting untuk menghemat sumber daya sebelum mengirim pemberitahuan. Pengguna sedang online saat pemberitahuan diterima, tetapi ia mungkin sedang offline saat mengkliknya, jadi penting bahwa konten tersedia secara offline pada saat itu. Aplikasi seluler Twitter melakukan ini sedikit salah.



Tanpa koneksi jaringan, Twitter tidak menyediakan konten terkait pemberitahuan. Namun, mengklik notifikasi akan menghapusnya. Jangan lakukan itu!



Kode berikut memperbarui cache sebelum mengirim notifikasi:



self.addEventListener('push', event => {
    if (event.data.text() === 'new-email') {
        event.waitUntil(
            caches.open('mysite-dynamic')
                .then(cache => fetch('/inbox.json')
                    .then(response => {
                        cache.put('/inbox.json', response.clone())
                        return response.json()
                    })).then(emails => {
                        registration.showNotification('New email', {
                            body: `From ${emails[0].from.name}`,
                            tag: 'new-email'
                        })
                    })
        )
    }
})

self.addEventListener('notificationclick', event => {
    if (event.notification.tag === 'new-email') {
        // ,   ,    /inbox/  ,
        // ,   
        new WindowClient('/inbox/')
    }
})


Dengan sinkronisasi latar belakang






Sinkronisasi Latar Belakang adalah abstraksi lain atas pekerja. Ini memungkinkan Anda untuk meminta sinkronisasi data latar belakang satu kali atau berkala. Itu juga independen dari pengguna. Namun, permintaan izin juga dikirimkan kepadanya.



Cocok untuk memperbarui sumber daya yang tidak signifikan, pengiriman pemberitahuan rutin tentang yang akan terlalu sering dan, oleh karena itu, mengganggu pengguna, misalnya, acara baru di jejaring sosial atau artikel baru di umpan berita.



self.addEventListener('sync', event => {
    if (event.id === 'update-leaderboard') {
        event.waitUntil(
            caches.open('mygame-dynamic')
                .then(cache => cache.add('/leaderboard.json'))
        )
    }
})


Menyimpan cache



Sumber Anda memberikan sejumlah ruang kosong. Ruang ini dibagi di antara semua penyimpanan: lokal dan sesi, database yang diindeks, sistem file dan, tentu saja, cache.



Ukuran penyimpanan tidak tetap dan bervariasi menurut perangkat dan kondisi penyimpanan. Anda dapat memeriksanya seperti ini:



navigator.storageQuota.queryInfo('temporary').then(info => {
    console.log(info.quota)
    // : <  >
    console.log(info.usage)
    //  <    >
})


Ketika ukuran penyimpanan ini atau itu mencapai batas, penyimpanan ini dihapus sesuai dengan aturan tertentu yang tidak dapat diubah saat ini.



Untuk mengatasi masalah ini, antarmuka untuk mengirim permintaan izin (requestPersistent) telah diusulkan:



navigator.storage.requestPersistent().then(granted => {
    if (granted) {
        // ,       
    }
})


Tentu saja, pengguna harus memberikan izin untuk ini. Pengguna harus menjadi bagian dari proses ini. Jika memori pada perangkat pengguna penuh dan menghapus data yang tidak penting tidak menyelesaikan masalah, pengguna harus memutuskan data mana yang akan disimpan dan mana yang akan dihapus.



Untuk bekerja, sistem operasi harus memperlakukan penyimpanan browser sebagai item terpisah.



Menjawab permintaan



Tidak peduli berapa banyak sumber daya yang Anda cache, pekerja tidak akan menggunakannya sampai Anda memberi tahu dia kapan dan apa yang harus digunakan. Berikut beberapa template untuk menangani permintaan.



Hanya uang tunai






Cocok untuk sumber daya statis apa pun dari versi halaman saat ini. Anda harus menyimpan sumber daya ini ke dalam cache selama fase penyiapan pekerja agar dapat mengirimnya sebagai tanggapan atas permintaan.



self.addEventListener('fetch', event => {
    //     ,
    //      
    event.respondWith(caches.match(event.request))
})


Jaringan saja






Cocok untuk sumber daya yang tidak dapat disimpan dalam cache, seperti data analitik atau permintaan non-GET.



self.addEventListener('fetch', event => {
    event.respondWith(fetch(event.request))
    //     event.respondWith
    //      
})


Pertama cache, kemudian, pada kegagalan, jaringan






Cocok untuk menangani sebagian besar permintaan dalam aplikasi offline.



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => response || fetch(event.request))
    )
})


Sumber daya yang disimpan dikembalikan dari cache, sumber daya yang tidak disimpan dari jaringan.



Siapapun yang punya waktu, dia makan






Cocok untuk sumber daya kecil dalam mengejar kinerja yang lebih baik untuk perangkat dengan memori rendah.



Kombinasi hard drive lama, antivirus, dan koneksi internet yang cepat dapat membuat pengambilan data dari jaringan lebih cepat daripada mengambil data dari cache. Namun, mengambil data dari jaringan saat data disimpan di perangkat pengguna adalah pemborosan sumber daya.



// Promise.race   ,   
//       .
//   
const promiseAny = promises => new Promise((resolve, reject) => {
    //  promises   
    promises = promises.map(p => Promise.resolve(p))
    //   ,    
    promises.forEach(p => p.then(resolve))
    //     ,   
    promises.reduce((a, b) => a.catch(() => b))
        .catch(() => reject(Error('  ')))
})

self.addEventListener('fetch', event => {
    event.respondWith(
        promiseAny([
            caches.match(event.request),
            fetch(event.request)
        ])
    )
})


Approx. Lane: Sekarang Anda dapat menggunakan Promise.allSettled untuk tujuan ini, tetapi dukungan browsernya adalah 80%: -20% pengguna mungkin terlalu banyak.



Jaringan pertama, kemudian, pada kegagalan, cache






Cocok untuk sumber daya yang sering diperbarui dan tidak memengaruhi versi situs saat ini, misalnya, artikel, avatar, umpan berita di jejaring sosial, peringkat pemain, dll.



Ini berarti Anda menyajikan konten baru kepada pengguna online dan konten lama untuk pengguna offline. Jika permintaan sumber daya dari jaringan berhasil, cache mungkin harus diperbarui.



Pendekatan ini memiliki satu kelemahan. Jika pengguna memiliki masalah koneksi atau lambat, dia harus menunggu permintaan selesai atau gagal alih-alih mengambil konten dari cache secara instan. Penantian ini bisa sangat lama, mengakibatkan pengalaman pengguna yang buruk.



self.addEventListener('fetch', event => {
    event.respondWith(
        fetch(event.request).catch(() => caches.match(event.request))
    )
})


Pertama cache, lalu jaringan






Cocok untuk sumber daya yang sering diperbarui.



Ini mengharuskan halaman untuk mengirim dua permintaan, satu untuk cache dan satu lagi untuk jaringan. Idenya adalah mengembalikan data dari cache dan kemudian menyegarkannya ketika menerima data dari jaringan.



Kadang-kadang Anda dapat mengganti data saat ini ketika Anda menerima yang baru (misalnya, peringkat pemain), tetapi ini bermasalah untuk sebagian besar konten. Hal ini dapat menyebabkan hilangnya apa yang sedang dibaca atau berinteraksi dengan pengguna.



Twitter menambahkan konten baru di atas konten yang ada sambil tetap menggulir: pengguna melihat pemberitahuan tweet baru di bagian atas layar. Ini dimungkinkan berkat urutan linier konten. Saya menyalin template ini untuk menampilkan konten dari cache secepat mungkin dan menambahkan konten baru yang didapat dari web.



Kode di halaman:



const networkDataReceived = false

startSpinner()

//   
const networkUpdate = fetch('/data.json')
    .then(response => response.json())
        .then(data => {
            networkDataReceived = true
            updatePage(data)
        })

//   
caches.match('/data.json')
    .then(response => {
        if (!response) throw Error(' ')
        return response.json()
    }).then(data => {
        //      
        if (!networkDataReceived) {
            updatePage(data)
        }
    }).catch(() => {
        //      ,  -   
        return networkUpdate
    }).catch(showErrorMessage).then(stopSpinner)


Kode pekerja:



Kami mengakses jaringan dan memperbarui cache.



self.addEventListener('fetch', event => {
    event.respondWith(
        caches.open('mysite-dynamic')
            .then(cache => fetch(event.request)
                .then(response => {
                    cache.put(event.request, response.clone())
                    return response
                }))
    )
})


Internet aman






Jika upaya untuk mendapatkan sumber daya dari cache dan jaringan gagal, harus ada penggantian.



Cocok untuk placeholder (mengganti gambar dengan dummy), permintaan POST yang gagal, halaman "Tidak tersedia saat offline".



self.addEventListener('fetch', event => {
    event.respondWith(
        //     
        //   ,   
        caches.match(event.request)
            .then(response => response || fetch(event.request))
            .catch(() => {
                //    ,  
                return caches.match('/offline.html')
                //       
                //    URL   
            })
    )
})


Jika halaman Anda mengirimkan email, pekerja dapat menyimpannya ke database yang diindeks sebelum mengirimkan dan memberi tahu halaman bahwa pengiriman gagal, tetapi email tersebut telah disimpan.



Membuat markup di sisi pekerja






Cocok untuk halaman yang dirender di sisi server dan tidak dapat di-cache.



Proses rendering halaman di sisi server adalah proses yang sangat cepat, tetapi ini membuat penyimpanan konten dinamis dalam cache menjadi tidak berguna karena dapat berbeda untuk setiap render. Jika halaman Anda dikontrol oleh pekerja, Anda dapat meminta sumber daya dan merender halaman langsung di sana.



import './templating-engine.js'

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

    event.respondWith(
        Promise.all([
            caches.match('/article-template.html')
                .then(response => response.text()),
            caches.match(`${requestURL.path}.json`)
                .then(response => response.json())
        ]).then(responses => {
            const template = responses[0]
            const data = responses[1]

            return new Response(renderTemplate(template, data), {
                headers: {
                    'Content-Type': 'text/html'
                }
            })
        })
    )
})


Bersama


Anda tidak harus dibatasi pada satu template. Anda kemungkinan besar harus menggabungkannya tergantung pada permintaan. Misalnya, train-to-thrill menggunakan yang berikut ini:



  • Caching penyiapan pekerja untuk elemen UI yang persisten
  • Caching pada respons server untuk gambar dan data Flickr
  • Mengambil data dari cache, dan kegagalan dari jaringan untuk sebagian besar permintaan
  • Mengambil sumber daya dari cache dan kemudian dari web untuk hasil pencarian Flick


Lihat saja permintaannya dan putuskan apa yang harus dilakukan dengannya:



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

    //       
    if (requestURL.hostname === 'api.example.com') {
        event.respondWith(/*    */)
        return
    }

    //    
    if (requestURL.origin === location.origin) {
        //   
        if (/^\/article\//.test(requestURL.pathname)) {
            event.respondWith(/*    */)
            return
        }
        if (/\.webp$/.test(requestURL.pathname)) {
            event.respondWith(/*    */)
            return
        }
        if (request.method == 'POST') {
            event.respondWith(/*     */)
            return
        }
        if (/cheese/.test(requestURL.pathname)) {
            event.respondWith(
                // . .:    -   ?
                new Response('Flagrant cheese error', {
                //    
                status: 512
                })
            )
            return
        }
    }

    //  
    event.respondWith(
        caches.match(event.request)
            .then(response => response || fetch(event.request))
    )
})


Saya harap artikel ini bermanfaat bagi Anda. Terima kasih atas perhatian Anda.



All Articles