ServiceWorkerを使ったアプリケーションキャッシング
参考1: https://developers.google.com/web/fundamentals/codelabs/your-first-pwapp/?hl=ja
参考2: https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle?hl=ja
PWA(Progressive Web Apps)などで、Webアプリケーションをネイティブアプリケーションのような内部にリソースを持つようにWebアプリケーションでもそういうリソースをServiceWorkerでキャッシュさせて利用する事が出来るとのことなのでちょっとやってみた
※上記参考を必読推奨
※あくまで個人的なメモです
前提条件
一番重要なのがServiceWorkerによってfetchによるリソース取得がコントロールされる事なのだが、普通にやってもAPIリクエストなどで使用されるfetchがServiceWorkerによってコントールされるタイミングがずれてしまって、初回アクセス時にはServiceWorkerがコントロール待機中などによりコントロールされなかったりする。それをちゃんと初回アクセス時にもコントロールされるようにする。つまり
navigator.serviceWorker.register("sw.js").then(() => {
fetch("省略");
});
みたいにやっても初回アクセス時はコントロール化に置かれるまではthenからのfetchがコントロール化になってない場合にはキャッシュなどの処理が行われないと思われる。よってfetch処理などはコントロール化に置かれてから処理する必要があるのでそこも考慮する
詳しくはhttps://github.com/Jxck/jxck.io/blob/master/blog.jxck.io/drafts/service-worker-bootcamp.mdを
でここからが本題でWebアプリケーションで使うリソースをServiceWorkerを使ってキャッシュする仕組みを作る
index.html
<!doctype html>
<html>
<body>
<div id="msg"></div>
<script src="app.js"></script>
</body>
</html>
app.js
"use strict";
if ("serviceWorker" in navigator) {
let controller = new Promise((resolve, reject) => {
navigator.serviceWorker.addEventListener("controllerchange", resolve);
});
navigator.serviceWorker.register("service-worker.js" ).then(() => {
if (navigator.serviceWorker.controller) {
return navigator.serviceWorker.controller;
}
return controller;
}).then(() => {
if ("caches" in window) {
caches.match("/data").then(res => {
// キャッシュがあればキャッシュのデータをレンダリング
// キャッシュが無ければAPIリクエストをコールしたのちそれをキャッシュ
if (res) {
res.text().then(data => {
console.log("from cache");
document.querySelector("#msg").innerHTML = "cache data = " + data;
});
} else {
fetch("/data").then(res => {
res.text().then(data => {
console.log("from realtime");
document.querySelector("#msg").innerHTML = "realtime data = " + data;
});
});
}
});
}
});
}
APIを呼び出して結果をキャッシュするようにするので、初回時アクセスであってもキャッシュされるようにServiceWorkerによるコントロール化が行われてからやる必要があるのでcontrollerchangeを利用して出来るとのこと。初回時以外ですでにコントロール済みな場合においてはnavigator.serviceWorker.controllerに入ってるのでそれを利用する
service-worker.js
let cacheName = "sample-pwa-cache-v1";
let cacheDataName = "sample-pwa-data-cache-v1";
let filesToCache = [ "/", "/index.html", "/app.js" ];
self.addEventListener("install", event => {
console.log("install");
self.skipWaiting();
event.waitUntil(
caches.open(cacheName).then(cache => cache.addAll(filesToCache))
);
});
self.addEventListener('activate', event => {
console.log("activate");
event.waitUntil(
caches.keys().then(
keyList => Promise.all(
keyList.map(key => {
console.log(`key = ${key}`);
if (key !== cacheName) {
// activate発生時に特に指定していないのでAPIキャッシュも消える
console.log(`delete ${key}`);
return caches.delete(key);
}
})
)
).then(() => {
return self.clients.claim();
})
);
});
self.addEventListener("fetch", event => {
console.log(`fetch ${event.request.url}`);
let url = new URL(event.request.url);
if (url.pathname === "/data") {
event.respondWith(
fetch(event.request).then(res => {
return caches.open(cacheDataName).then(cache => {
cache.put(event.request.url, res.clone());
console.log("[SW] data cached");
return res;
});
})
);
//event.respondWith(fetch(event.request));
} else {
event.respondWith(
caches.match(event.request).then(res => {
return res || fetch(event.request)
})
);
}
});
結果
初回リクエスト時にはAPIコールにしつつそのデータをキャッシュすることになるので
となる。次回リクエスト時には
となる。またindex.htmlやapp.jsなどもキャッシュしているので初回時にキャッシュされるので再度ロードされる際にはServiceWorkerからのキャッシュとして配信されるようになる。なので一度キャッシュした状態でサーバーを落としてみても結果はキャッシュされたのがServiceWorkerから配信されるようになる
ちなみにこういうのを実現したいのであればそれをサポートしてくれるライブラリ(sw-precache等)があるのでそれを使っても良いかと
今回はAPIコールした結果をキャッシュしてそれがある場合にはそれをレンダリングしているが、本式としてはキャッシュをしつつAPIをコールして得られる結果を取得する前にキャッシュからレンダリングしておいて、リアルタイムなデータをレンダリングするような形になるかと