Pekerja Layanan dalam Produksi

Screenshot potret

Ringkasan

Pelajari cara kami menggunakan library pekerja layanan untuk menjadikan aplikasi web Google I/O 2015 cepat dan mengutamakan penggunaan offline.

Ringkasan

Aplikasi web Google I/O 2015 tahun ini ditulis oleh tim Developer Relations Google, berdasarkan desain teman kami di Instrument, yang menulis eksperimen audio/visual keren. Misi tim kami adalah memastikan bahwa aplikasi web I/O (yang akan saya sebut nama kodenya, IOWA) menampilkan semua yang dapat dilakukan oleh web modern. Pengalaman offline-first yang lengkap berada di bagian atas daftar fitur yang harus dimiliki.

Jika Anda baru-baru ini membaca artikel lain di situs ini, pasti Anda telah bertemu dengan pekerja layanan, dan Anda tidak akan terkejut mendengar bahwa dukungan offline IOWA sangat bergantung pada mereka. Termotivasi oleh kebutuhan IOWA di dunia nyata, kami mengembangkan dua library untuk menangani dua kasus penggunaan offline yang berbeda: sw-precache untuk mengotomatiskan precaching resource statis, dan sw-toolbox untuk menangani cache runtime dan strategi penggantian.

Library tersebut saling melengkapi dengan baik, dan memungkinkan kami untuk mengimplementasikan strategi yang berperforma baik, yaitu “shell” konten statis IOWA selalu disalurkan langsung dari cache, dan resource dinamis atau jarak jauh disalurkan dari jaringan, dengan fallback ke respons yang di-cache atau statis jika diperlukan.

Precaching dengan sw-precache

Resource statis IOWA—HTML, JavaScript, CSS, dan gambarnya—menyediakan shell inti untuk aplikasi web. Ada dua persyaratan khusus yang penting saat mempertimbangkan untuk meng-cache resource ini: kami ingin memastikan bahwa sebagian besar resource statis telah di-cache, dan selalu diupdate. sw-precache dibuat dengan mempertimbangkan persyaratan tersebut.

Integrasi Waktu Build

sw-precache dengan proses build berbasis gulp IOWA, dan kami mengandalkan serangkaian pola glob untuk memastikan bahwa kami membuat daftar lengkap semua resource statis yang digunakan IOWA.

staticFileGlobs: [
    rootDir + '/bower_components/**/*.{html,js,css}',
    rootDir + '/elements/**',
    rootDir + '/fonts/**',
    rootDir + '/images/**',
    rootDir + '/scripts/**',
    rootDir + '/styles/**/*.css',
    rootDir + '/data-worker-scripts.js'
]

Pendekatan alternatif, seperti melakukan hard coding pada daftar nama file ke dalam array, dan mengingat untuk menambahkan nomor versi cache setiap kali perubahan file tersebut terlalu rentan terhadap error, terutama karena kami memiliki beberapa anggota tim yang memeriksa kode. Tidak ada yang ingin merusak dukungan offline dengan membiarkan file baru dalam array yang dikelola secara manual. Dengan integrasi waktu build, kami dapat membuat perubahan pada file yang ada dan menambahkan file baru tanpa perlu mengkhawatirkannya.

Memperbarui Resource yang Di-Cache

sw-precache membuat skrip pekerja layanan dasar yang menyertakan hash MD5 unik untuk setiap resource yang di-precache. Setiap kali resource yang ada berubah, atau resource baru ditambahkan, skrip pekerja layanan akan dibuat ulang. Tindakan ini akan otomatis memicu alur update pekerja layanan, tempat resource baru di-cache dan resource yang sudah tidak berlaku akan dihapus permanen. Semua resource yang ada dan memiliki hash MD5 identik akan dibiarkan apa adanya. Artinya, pengguna yang telah mengunjungi situs tersebut hanya mendownload kumpulan minimal resource yang diubah, sehingga menghasilkan pengalaman yang jauh lebih efisien dibandingkan jika seluruh cache telah habis masa berlakunya secara massal.

Setiap file yang cocok dengan salah satu pola glob akan didownload dan di-cache saat pertama kali pengguna mengunjungi IOWA. Kami berupaya untuk memastikan bahwa hanya resource penting yang diperlukan untuk merender halaman yang di-precache. Konten sekunder, seperti media yang digunakan dalam eksperimen audio/visual, atau gambar profil pembicara sesi, sengaja tidak di-cache di awal, dan sebagai gantinya kami menggunakan library sw-toolbox untuk menangani permintaan offline untuk resource tersebut.

sw-toolbox, untuk Semua Kebutuhan Dinamis Kami

Seperti yang disebutkan, melakukan precache setiap resource yang diperlukan situs agar berfungsi secara offline tidak memungkinkan. Beberapa resource terlalu besar atau jarang digunakan agar bermanfaat, dan resource lainnya bersifat dinamis, seperti respons dari API atau layanan jarak jauh. Namun, hanya karena permintaan tidak di-precache, bukan berarti permintaan tersebut harus menghasilkan NetworkError. sw-toolbox memberi kami fleksibilitas untuk mengimplementasikan pengendali permintaan yang menangani caching runtime untuk beberapa resource dan penggantian kustom untuk yang lain. Kami juga menggunakannya untuk memperbarui resource yang di-cache sebelumnya sebagai respons terhadap notifikasi push.

Berikut adalah beberapa contoh pengendali permintaan kustom yang kami bangun di atas sw-toolbox. Sangat mudah untuk mengintegrasikannya dengan skrip pekerja layanan dasar melalui importScripts parameter sw-precache, yang menarik file JavaScript mandiri ke dalam cakupan pekerja layanan.

Eksperimen Audio/Visual

Untuk eksperimen audio/visual, kami menggunakan strategi cache networkFirst sw-toolbox. Semua permintaan HTTP yang cocok dengan pola URL untuk eksperimen akan dibuat terhadap jaringan terlebih dahulu, dan jika respons yang berhasil ditampilkan, respons tersebut kemudian akan disimpan menggunakan Cache Storage API. Jika permintaan berikutnya dibuat saat jaringan tidak tersedia, respons yang sebelumnya di-cache akan digunakan.

Karena cache diperbarui secara otomatis setiap kali respons jaringan yang berhasil kembali, kita tidak perlu membuat versi resource atau entri berakhir secara khusus.

toolbox.router.get('/experiment/(.+)', toolbox.networkFirst);

Gambar Profil Pembicara

Untuk gambar profil pembicara, tujuan kita adalah menampilkan versi gambar pembicara tertentu yang sebelumnya di-cache jika tersedia, dan kembali ke jaringan untuk mengambil gambar jika tidak tersedia. Jika permintaan jaringan tersebut gagal, sebagai penggantian akhir, kami menggunakan gambar placeholder umum yang telah di-cache (sehingga selalu tersedia). Ini adalah strategi umum yang digunakan saat menangani gambar yang dapat diganti dengan placeholder generik, dan mudah diterapkan dengan menggabungkan pengendali cacheFirst dan cacheOnly sw-toolbox.

var DEFAULT_PROFILE_IMAGE = 'images/touch/homescreen96.png';

function profileImageRequest(request) {
    return toolbox.cacheFirst(request).catch(function() {
    return toolbox.cacheOnly(new Request(DEFAULT_PROFILE_IMAGE));
    });
}

toolbox.precache([DEFAULT_PROFILE_IMAGE]);
toolbox.router.get('/(.+)/images/speakers/(.*)',
                    profileImageRequest,
                    {origin: /.*\.googleapis\.com/});
Gambar profil dari halaman sesi
Gambar profil dari halaman sesi.

Pembaruan pada Jadwal Pengguna

Salah satu fitur utama IOWA adalah memungkinkan pengguna yang login untuk membuat dan mempertahankan jadwal sesi yang mereka rencanakan untuk dihadiri. Seperti yang Anda harapkan, pembaruan sesi dilakukan melalui permintaan POST HTTP ke server backend, dan kami menghabiskan waktu untuk mencari cara terbaik untuk menangani permintaan perubahan status tersebut saat pengguna sedang offline. Kami menghasilkan kombinasi permintaan yang gagal dalam antrean di IndexedDB, ditambah dengan logika di halaman web utama yang memeriksa IndexedDB untuk permintaan dalam antrean dan mencoba lagi permintaan yang ditemukan.

var DB_NAME = 'shed-offline-session-updates';

function queueFailedSessionUpdateRequest(request) {
    simpleDB.open(DB_NAME).then(function(db) {
    db.set(request.url, request.method);
    });
}

function handleSessionUpdateRequest(request) {
    return global.fetch(request).then(function(response) {
    if (response.status >= 500) {
        return Response.error();
    }
    return response;
    }).catch(function() {
    queueFailedSessionUpdateRequest(request);
    });
}

toolbox.router.put('/(.+)api/v1/user/schedule/(.+)',
                    handleSessionUpdateRequest);
toolbox.router.delete('/(.+)api/v1/user/schedule/(.+)',
                        handleSessionUpdateRequest);

Karena percobaan ulang dilakukan dari konteks halaman utama, kami dapat memastikan bahwa percobaan ulang tersebut menyertakan kumpulan kredensial pengguna yang baru. Setelah percobaan ulang berhasil, kami menampilkan pesan untuk memberi tahu pengguna bahwa update yang sebelumnya diantrekan telah diterapkan.

simpleDB.open(QUEUED_SESSION_UPDATES_DB_NAME).then(function(db) {
    var replayPromises = [];
    return db.forEach(function(url, method) {
    var promise = IOWA.Request.xhrPromise(method, url, true).then(function() {
        return db.delete(url).then(function() {
        return true;
        });
    });
    replayPromises.push(promise);
    }).then(function() {
    if (replayPromises.length) {
        return Promise.all(replayPromises).then(function() {
        IOWA.Elements.Toast.showMessage(
            'My Schedule was updated with offline changes.');
        });
    }
    });
}).catch(function() {
    IOWA.Elements.Toast.showMessage(
    'Offline changes could not be applied to My Schedule.');
});

Google Analytics Offline

Dengan cara yang sama, kami menerapkan pengendali untuk mengantrekan permintaan Google Analytics yang gagal dan mencoba memutarnya ulang nanti, saat jaringan tersebut sekiranya tersedia. Dengan pendekatan ini, menjadi offline tidak berarti mengorbankan insight yang ditawarkan Google Analytics. Kami menambahkan parameter qt ke setiap permintaan dalam antrean, yang ditetapkan ke jumlah waktu yang telah berlalu sejak permintaan pertama kali dicoba, untuk memastikan bahwa waktu atribusi peristiwa yang tepat telah sampai ke backend Google Analytics. Google Analytics secara resmi mendukung nilai untuk qt hingga 4 jam saja, sehingga kami melakukan upaya terbaik untuk memutar ulang permintaan tersebut sesegera mungkin, setiap kali pekerja layanan dimulai.

var DB_NAME = 'offline-analytics';
var EXPIRATION_TIME_DELTA = 86400000;
var ORIGIN = /https?:\/\/((www|ssl)\.)?google-analytics\.com/;

function replayQueuedAnalyticsRequests() {
    simpleDB.open(DB_NAME).then(function(db) {
    db.forEach(function(url, originalTimestamp) {
        var timeDelta = Date.now() - originalTimestamp;
        var replayUrl = url + '&qt=' + timeDelta;
        fetch(replayUrl).then(function(response) {
        if (response.status >= 500) {
            return Response.error();
        }
        db.delete(url);
        }).catch(function(error) {
        if (timeDelta > EXPIRATION_TIME_DELTA) {
            db.delete(url);
        }
        });
    });
    });
}

function queueFailedAnalyticsRequest(request) {
    simpleDB.open(DB_NAME).then(function(db) {
    db.set(request.url, Date.now());
    });
}

function handleAnalyticsCollectionRequest(request) {
    return global.fetch(request).then(function(response) {
    if (response.status >= 500) {
        return Response.error();
    }
    return response;
    }).catch(function() {
    queueFailedAnalyticsRequest(request);
    });
}

toolbox.router.get('/collect',
                    handleAnalyticsCollectionRequest,
                    {origin: ORIGIN});
toolbox.router.get('/analytics.js',
                    toolbox.networkFirst,
                    {origin: ORIGIN});

replayQueuedAnalyticsRequests();

Halaman Landing Notifikasi Push

Pekerja layanan tidak hanya menangani fungsi offline IOWA—mereka juga mendukung notifikasi push yang kami gunakan untuk memberi tahu pengguna tentang pembaruan sesi yang di-bookmark. Halaman landing yang terkait dengan notifikasi tersebut menampilkan detail sesi yang diperbarui. Halaman landing tersebut sudah di-cache sebagai bagian dari keseluruhan situs, sehingga halaman tersebut sudah berfungsi secara offline, tetapi kami perlu memastikan bahwa detail sesi di halaman tersebut sudah yang terbaru, bahkan saat dilihat secara offline. Untuk melakukannya, kami mengubah metadata sesi yang sebelumnya di-cache dengan pembaruan yang memicu notifikasi push, dan kami menyimpan hasilnya dalam cache. Info terbaru ini akan digunakan saat berikutnya halaman detail sesi dibuka, baik yang berlangsung secara online maupun offline.

caches.open(toolbox.options.cacheName).then(function(cache) {
    cache.match('api/v1/schedule').then(function(response) {
    if (response) {
        parseResponseJSON(response).then(function(schedule) {
        sessions.forEach(function(session) {
            schedule.sessions[session.id] = session;
        });
        cache.put('api/v1/schedule',
                    new Response(JSON.stringify(schedule)));
        });
    } else {
        toolbox.cache('api/v1/schedule');
    }
    });
});

Gotcha & Pertimbangan

Tentu saja, tidak ada yang mengerjakan proyek skala IOWA tanpa mengalami beberapa gotcha. Berikut adalah beberapa contoh masalah yang kami hadapi, dan cara kami mengatasinya.

Konten Tidak Berlaku

Setiap kali Anda merencanakan strategi penyimpanan dalam cache, baik yang diterapkan melalui pekerja layanan atau dengan cache browser standar, terdapat konsekuensi antara mengirimkan resource secepat mungkin dibandingkan mengirimkan resource terbaru. Melalui sw-precache, kami menerapkan strategi cache-first yang agresif untuk shell aplikasi, yang berarti pekerja layanan tidak akan memeriksa pembaruan jaringan sebelum menampilkan HTML, JavaScript, dan CSS di halaman.

Untungnya, kami dapat memanfaatkan peristiwa siklus proses pekerja layanan untuk mendeteksi kapan konten baru tersedia setelah halaman dimuat. Saat pekerja layanan yang diperbarui terdeteksi, kami akan menampilkan pesan toast kepada pengguna untuk memberi tahu bahwa mereka harus memuat ulang halaman guna melihat konten terbaru.

if (navigator.serviceWorker && navigator.serviceWorker.controller) {
    navigator.serviceWorker.controller.onstatechange = function(e) {
    if (e.target.state === 'redundant') {
        var tapHandler = function() {
        window.location.reload();
        };
        IOWA.Elements.Toast.showMessage(
        'Tap here or refresh the page for the latest content.',
        tapHandler);
    }
    };
}
Toast konten terbaru
Toast "konten terbaru".

Pastikan Konten Statis Bersifat Statis!

sw-precache menggunakan hash MD5 konten file lokal, dan hanya mengambil resource yang hash-nya telah berubah. Ini berarti resource akan tersedia di halaman hampir seketika, tetapi juga berarti bahwa setelah di-cache, resource tersebut akan tetap di-cache sampai ditetapkan hash baru dalam skrip pekerja layanan yang diperbarui.

Kami mengalami masalah dengan perilaku ini selama I/O karena backend kami perlu memperbarui ID video YouTube livestream secara dinamis untuk setiap hari konferensi. Karena file template dasar bersifat statis dan tidak berubah, alur pembaruan pekerja layanan kami tidak dipicu, dan apa yang dimaksudkan sebagai respons dinamis dari server dengan pembaruan video YouTube akhirnya menjadi respons yang di-cache untuk sejumlah pengguna.

Anda dapat menghindari jenis masalah ini dengan memastikan bahwa aplikasi web Anda terstruktur sehingga shell selalu statis dan dapat di-precache dengan aman, sedangkan resource dinamis yang memodifikasi shell dimuat secara independen.

Cache-rusak Permintaan Precaching Anda

Saat membuat permintaan resource untuk melakukan precache, sw-precache akan menggunakan respons tersebut tanpa batas waktu selama menurutnya hash MD5 untuk file tidak berubah. Artinya, sangat penting untuk memastikan bahwa respons terhadap permintaan precache adalah yang baru, dan tidak ditampilkan dari cache HTTP browser. (Ya, permintaan fetch() yang dibuat dalam pekerja layanan dapat merespons dengan data dari cache HTTP browser.)

Untuk memastikan bahwa respons yang dilakukan precache langsung dari jaringan dan bukan cache HTTP browser, sw-precache secara otomatis menambahkan parameter kueri perusak cache ke setiap URL yang diminta. Jika tidak menggunakan sw-precache dan menggunakan strategi respons cache-first, pastikan Anda melakukan hal serupa dalam kode Anda sendiri.

Solusi yang lebih sederhana untuk perusak cache adalah dengan menyetel mode cache dari setiap Request yang digunakan untuk precaching ke reload, yang akan memastikan bahwa respons berasal dari jaringan. Namun, saat tulisan ini dibuat, opsi mode cache tidak didukung di Chrome.

Dukungan untuk Masuk & Keluar

IOWA memungkinkan pengguna login menggunakan Akun Google mereka dan memperbarui jadwal peristiwa yang disesuaikan, tetapi hal ini juga berarti bahwa pengguna mungkin kemudian akan logout. Menyimpan data respons yang dipersonalisasi ke dalam cache jelas merupakan topik yang rumit, dan tidak selalu ada satu pendekatan yang tepat.

Karena melihat jadwal pribadi, bahkan saat offline, adalah inti dari pengalaman IOWA, kami memutuskan bahwa menggunakan data yang di-cache adalah hal yang sesuai. Saat pengguna logout, kami memastikan untuk menghapus data sesi yang sebelumnya di-cache.

    self.addEventListener('message', function(event) {
      if (event.data === 'clear-cached-user-data') {
        caches.open(toolbox.options.cacheName).then(function(cache) {
          cache.keys().then(function(requests) {
            return requests.filter(function(request) {
              return request.url.indexOf('api/v1/user/') !== -1;
            });
          }).then(function(userDataRequests) {
            userDataRequests.forEach(function(userDataRequest) {
              cache.delete(userDataRequest);
            });
          });
        });
      }
    });

Waspadai Parameter Kueri Tambahan.

Bila pekerja layanan memeriksa respons yang di-cache, ia akan menggunakan URL permintaan sebagai kuncinya. Secara default, URL permintaan harus sama persis dengan URL yang digunakan untuk menyimpan respons yang di-cache, termasuk parameter kueri apa pun di bagian penelusuran URL.

Hal ini pada akhirnya menyebabkan masalah bagi kami selama pengembangan, saat kami mulai menggunakan parameter URL untuk melacak asal traffic kami. Misalnya, kami menambahkan parameter utm_source=notification ke URL yang dibuka saat mengklik salah satu notifikasi, dan menggunakan utm_source=web_app_manifest di start_url untuk manifes aplikasi web. URL yang sebelumnya cocok dengan respons yang di-cache akan muncul sebagai URL yang terlewat saat parameter tersebut ditambahkan.

Hal ini sebagian ditangani oleh opsi ignoreSearch yang dapat digunakan saat memanggil Cache.match(). Sayangnya, Chrome belum mendukung ignoreSearch, dan meskipun mendukungnya, itu adalah perilaku yang tidak ada sama sekali. Yang kami butuhkan adalah cara untuk mengabaikan beberapa parameter kueri URL dan mempertimbangkan parameter lainnya yang bermakna.

Kami akhirnya memperluas sw-precache untuk menghapus beberapa parameter kueri sebelum memeriksa kecocokan cache, dan memungkinkan developer menyesuaikan parameter mana yang diabaikan melalui opsi ignoreUrlParametersMatching. Berikut implementasi yang mendasarinya:

function stripIgnoredUrlParameters(originalUrl, ignoredRegexes) {
    var url = new URL(originalUrl);

    url.search = url.search.slice(1)
    .split('&')
    .map(function(kv) {
        return kv.split('=');
    })
    .filter(function(kv) {
        return ignoredRegexes.every(function(ignoredRegex) {
        return !ignoredRegex.test(kv[0]);
        });
    })
    .map(function(kv) {
        return kv.join('=');
    })
    .join('&');

    return url.toString();
}

Apa Artinya Bagi Anda

Integrasi pekerja layanan di Aplikasi Web Google I/O kemungkinan merupakan penggunaan di dunia nyata yang paling kompleks yang telah di-deploy ke tahap ini. Kami menantikan komunitas developer web yang menggunakan alat yang kami buat sw-precache dan sw-toolbox serta teknik yang kami jelaskan untuk mendukung aplikasi web Anda sendiri. Pekerja layanan adalah progressive enhancement yang dapat Anda gunakan sekarang, dan jika digunakan sebagai bagian dari aplikasi web yang terstruktur dengan benar, kecepatan dan manfaat offline-nya signifikan bagi pengguna Anda.