Ringkasan
Pelajari cara kami menggunakan library pekerja layanan untuk membuat aplikasi web Google I/O 2015 cepat, dan offline-first.
Ringkasan
Aplikasi web Google I/O 2015 tahun ini ditulis oleh tim Developer Relations Google, berdasarkan desain dari teman-teman kami di Instrument, yang menulis eksperimen audio/visual yang bagus. Misi tim kami adalah memastikan bahwa aplikasi web I/O (yang akan saya sebut dengan nama kodenya, IOWA) menampilkan semua yang dapat dilakukan web modern. Pengalaman offline-first lengkap berada di bagian atas daftar fitur yang harus dimiliki.
Jika baru-baru ini Anda membaca artikel lain di situs ini, Anda pastinya telah menemukan pekerja layanan, dan Anda tidak akan terkejut mendengar bahwa dukungan offline IOWA sangat bergantung pada pekerja layanan. Termotivasi oleh kebutuhan IOWA dunia nyata, kami mengembangkan dua
library untuk menangani dua kasus penggunaan offline yang berbeda:
sw-precache
untuk mengotomatiskan
precache resource statis, dan
sw-toolbox
untuk menangani
strategi penggantian dan caching runtime.
Library ini saling melengkapi dengan baik, dan memungkinkan kami menerapkan strategi yang berperforma tinggi, yaitu “shell” konten statis IOWA selalu ditayangkan langsung dari cache, dan resource dinamis atau jarak jauh ditayangkan dari jaringan, dengan penggantian ke respons statis atau yang di-cache jika diperlukan.
Menyimpan dalam cache sebelumnya 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 di-cache, dan bahwa resource tersebut selalu diperbarui.
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 kita 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 code pada daftar nama file ke dalam array, dan mengingat untuk menaikkan 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 menghentikan dukungan offline dengan meninggalkan file baru dalam array yang dikelola secara manual. Integrasi waktu build berarti kita dapat membuat perubahan pada file yang ada dan menambahkan file baru tanpa khawatir.
Mengupdate Resource yang Di-cache
sw-precache
menghasilkan skrip pekerja layanan dasar
yang menyertakan hash MD5 unik untuk setiap
resource yang dipra-cache. Setiap kali resource yang ada berubah,
atau resource baru ditambahkan, skrip pekerja layanan akan dibuat ulang. Tindakan ini
otomatis memicu alur update pekerja layanan,
tempat resource baru di-cache dan resource yang sudah tidak berlaku dihapus.
Setiap resource yang ada dan memiliki hash MD5 identik akan dibiarkan apa adanya. Artinya,
pengguna yang telah mengunjungi situs sebelumnya hanya mendownload
kumpulan minimal resource yang diubah, sehingga menghasilkan pengalaman yang jauh lebih efisien
dibandingkan jika seluruh cache berakhir masa berlakunya secara massal.
Setiap file yang cocok dengan salah satu pola glob didownload dan di-cache saat pertama kali pengguna mengunjungi IOWA. Kami berupaya memastikan bahwa hanya resource kritis
yang diperlukan untuk merender halaman yang dipra-cache. Konten sekunder, seperti
media yang digunakan dalam eksperimen audio/visual,
atau gambar profil pembicara sesi, sengaja tidak
di-pra-cache, dan sebagai gantinya kami menggunakan library
sw-toolbox
untuk menangani permintaan offline untuk resource tersebut.
sw-toolbox
, untuk Semua Kebutuhan Dinamis Kita
Seperti yang telah disebutkan, mem-pracache setiap resource yang diperlukan situs agar dapat berfungsi secara offline tidak
memungkinkan. Beberapa resource terlalu besar atau jarang digunakan sehingga tidak
bermanfaat, dan resource lainnya bersifat dinamis, seperti respons dari API atau layanan
jarak jauh. Namun, hanya karena permintaan tidak dipra-cache, bukan berarti permintaan tersebut harus menghasilkan NetworkError
.
sw-toolbox
memberi kita
fleksibilitas untuk menerapkan pengendali permintaan
yang menangani penyimpanan dalam cache runtime untuk beberapa resource dan penggantian kustom untuk
resource lainnya. Kami juga menggunakannya untuk memperbarui resource yang sebelumnya di-cache sebagai respons
terhadap notifikasi push.
Berikut adalah beberapa contoh pengendali permintaan kustom yang kami buat 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 terlebih dahulu terhadap jaringan, dan jika respons yang berhasil
ditampilkan, respons tersebut akan disimpan menggunakan
Cache Storage API.
Jika permintaan berikutnya dilakukan saat jaringan tidak tersedia, respons yang sebelumnya di-cache akan digunakan.
Karena cache otomatis diperbarui setiap kali respons jaringan yang berhasil kembali, kita tidak perlu membuat versi resource secara khusus atau berakhir masa berlakunya entri.
toolbox.router.get('/experiment/(.+)', toolbox.networkFirst);
Gambar Profil Pembicara
Untuk gambar profil pembicara, sasaran kami adalah menampilkan versi gambar pembicara
tertentu yang di-cache sebelumnya jika tersedia, dengan kembali ke jaringan untuk mengambil
gambar jika tidak. Jika permintaan jaringan tersebut gagal, sebagai penggantian akhir, kami menggunakan
gambar placeholder generik yang dipra-cache (sehingga akan selalu
tersedia). Ini adalah strategi umum yang digunakan saat menangani gambar yang
dapat diganti dengan placeholder umum, dan mudah diterapkan dengan
mengaitkan 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/});
Pembaruan pada Jadwal Pengguna
Salah satu fitur utama IOWA adalah memungkinkan pengguna yang login membuat dan
mengelola jadwal sesi yang ingin mereka hadiri. Seperti yang Anda harapkan,
update sesi dilakukan melalui permintaan POST
HTTP ke server backend, dan kami
menghabiskan waktu untuk mencari cara terbaik guna menangani permintaan
yang mengubah status tersebut saat pengguna offline. Kami menemukan kombinasi
permintaan yang gagal dalam antrean di IndexedDB, yang digabungkan dengan logika di halaman web utama
yang memeriksa IndexedDB untuk permintaan yang diantrekan 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, kita dapat memastikan bahwa percobaan ulang tersebut menyertakan kumpulan kredensial pengguna baru. Setelah percobaan ulang berhasil, kami menampilkan pesan untuk memberi tahu pengguna bahwa update mereka 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
Demikian pula, kami menerapkan pengendali untuk mengantrekan permintaan Google Analytics yang gagal dan mencoba memutarnya lagi nanti, saat jaringan diharapkan tersedia. Dengan pendekatan ini, offline bukan berarti mengorbankan insight yang ditawarkan Google Analytics. Kami menambahkan parameter qt
ke setiap permintaan yang diantrekan, yang ditetapkan ke jumlah waktu yang telah berlalu sejak permintaan pertama kali dicoba, untuk memastikan bahwa waktu atribusi peristiwa yang tepat berhasil masuk ke backend Google Analytics. Google Analytics
secara resmi mendukung
nilai untuk qt
hingga 4 jam saja, jadi 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, tetapi juga mendukung notifikasi push yang kami gunakan untuk memberi tahu pengguna tentang pembaruan pada sesi yang mereka bookmark. Halaman landing yang terkait dengan notifikasi tersebut menampilkan detail sesi yang diperbarui. Halaman landing tersebut sudah di-cache sebagai bagian dari situs secara keseluruhan, sehingga sudah berfungsi secara offline, tetapi kami perlu memastikan bahwa detail sesi di halaman tersebut sudah yang terbaru, meskipun dilihat secara offline. Untuk melakukannya, kami mengubah metadata sesi yang di-cache sebelumnya dengan update yang memicu notifikasi push, dan kami menyimpan hasilnya dalam cache. Info terbaru ini akan digunakan pada saat berikutnya halaman detail sesi dibuka, baik 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');
}
});
});
Masalah & Pertimbangan
Tentu saja, tidak ada yang mengerjakan project dengan skala IOWA tanpa mengalami beberapa masalah. Berikut beberapa masalah yang kami temui, 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, ada kompromi antara mengirimkan resource secepat mungkin versus mengirimkan resource terbaru. Melalui sw-precache
, kami menerapkan strategi cache-first agresif
untuk shell aplikasi, yang berarti pekerja layanan kami tidak akan memeriksa
jaringan untuk mengetahui update 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 diupdate terdeteksi, kami akan menampilkan pesan toast kepada pengguna untuk memberi tahu bahwa halaman harus dimuat ulang 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);
}
};
}
Pastikan Konten Statis Bersifat Statis
sw-precache
menggunakan hash MD5 konten file lokal, dan hanya mengambil
resource yang hash-nya telah berubah. Artinya, resource tersedia di halaman
hampir seketika, tetapi juga berarti bahwa setelah sesuatu di-cache, resource tersebut akan
tetap di-cache hingga 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 live stream secara dinamis untuk setiap hari konferensi. Karena file template pokok statis dan tidak berubah, alur update pekerja layanan kami tidak dipicu, dan yang dimaksudkan sebagai respons dinamis dari server dengan memperbarui video YouTube akhirnya menjadi respons yang di-cache untuk sejumlah pengguna.
Anda dapat menghindari jenis masalah ini dengan memastikan aplikasi web Anda terstruktur sehingga shell selalu statis dan dapat dipra-cache dengan aman, sedangkan resource dinamis yang mengubah shell dimuat secara independen.
Menghapus Cache Permintaan Pra-cache
Saat membuat permintaan untuk resource yang akan dipra-cache, sw-precache
akan menggunakan respons tersebut
tanpa batas selama menganggap bahwa hash MD5 untuk file tersebut belum
berubah. Artinya, sangat penting untuk memastikan bahwa respons terhadap
permintaan pra-cache adalah respons baru, dan tidak ditampilkan dari cache HTTP browser. (Ya, permintaan fetch()
yang dibuat di pekerja layanan dapat merespons dengan data dari cache HTTP browser.)
Untuk memastikan bahwa respons yang kami pra-cache langsung berasal dari jaringan, bukan cache HTTP browser, sw-precache
secara otomatis menambahkan parameter kueri cache-busting ke setiap URL yang diminta. Jika Anda tidak menggunakan sw-precache
dan menggunakan strategi respons cache-first, pastikan Anda melakukan hal yang serupa dalam kode Anda sendiri.
Solusi yang lebih bersih untuk cache-busting adalah menetapkan
mode cache
setiap Request
yang digunakan untuk pra-cache ke reload
, yang akan memastikan bahwa
respons berasal dari jaringan. Namun, pada saat penulisan ini, opsi mode cache
tidak didukung
di Chrome.
Dukungan untuk Masuk &Keluar
IOWA mengizinkan pengguna login menggunakan Akun Google mereka dan memperbarui jadwal acara yang disesuaikan, tetapi hal itu juga berarti bahwa pengguna mungkin akan logout nanti. Menyimpan data respons yang dipersonalisasi dalam cache jelas merupakan topik yang rumit, dan tidak selalu ada satu pendekatan yang tepat.
Karena melihat jadwal pribadi Anda, meskipun saat offline, merupakan inti dari pengalaman IOWA, kami memutuskan bahwa penggunaan data dalam cache sudah sesuai. Saat pengguna logout, kami memastikan untuk menghapus data sesi yang di-cache sebelumnya.
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);
});
});
});
}
});
Hati-hati dengan Parameter Kueri Tambahan!
Saat memeriksa respons yang di-cache, pekerja layanan menggunakan URL permintaan sebagai kunci. 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 akhirnya menyebabkan masalah bagi kami selama pengembangan, saat kami mulai menggunakan
parameter URL untuk melacak asal
traffic. Misalnya, kita 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 muncul sebagai tidak cocok 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 telah mendukung, tetap saja perilakunya tidak ada artinya. Yang kami butuhkan adalah
cara untuk mengabaikan beberapa parameter kueri URL sekaligus mempertimbangkan parameter lainnya yang bermakna.
Kami akhirnya memperluas sw-precache
untuk menghapus beberapa parameter kueri sebelum memeriksa kecocokan cache, dan memungkinkan developer menyesuaikan parameter yang diabaikan melalui opsi ignoreUrlParametersMatching
.
Berikut adalah 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();
}
Arti Perubahan Ini bagi Anda
Integrasi pekerja layanan di Aplikasi Web Google I/O mungkin merupakan penggunaan paling kompleks di dunia nyata yang telah di-deploy hingga saat ini. Kami berharap
komunitas developer web 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 mulai gunakan sekarang, dan jika digunakan sebagai bagian dari aplikasi web
yang terstruktur dengan baik, kecepatan dan manfaat offline signifikan bagi pengguna Anda.