本番環境にService Workerを導入しましたので、そちらの実装方法についてまとめます。
一部キャッシュ管理にworkbox-swを使ってます。
正直Railsはあんまり関係ないかも。
導入方法については下記記事を書きましたので、そちらを参考にして見てください
[Sprockets管理のRails4環境でWebサイトをPWAに対応する方法のまとめ(準備〜デプロイ編)]
(https://qiita.com/ykyk1218/items/f296d9078c71cd27db78)
- 各種イベントハンドリング
- キャッシュ管理
- オフライン対応
- 計測
- エラーハンドリング
上記5つの視点でそれぞれの実装方法を確認します。
javascriptの書き方は今風ではなく、古い書き方になっていますが、ご了承ください........
今回のやりたいこと全体像
カテゴリAの記事にアクセスした時に、カテゴリAの他の記事をキャッシュしてオフラインでも見れるようにしたい、ということをします。
つまり見たページ以外に、関連する他のページをキャッシュします
各種イベントハンドリング
Service Workerで受け取れるイベントには下記があります。
こちらの各種イベントで処理を追加
イベント名 | イベント発生タイミング |
---|---|
Install | Service Workerをブラウザにinstallするタイミング |
Activate | Service Workerがアクティベートされるタイミング ※ |
Fetch | 通信のタイミング |
Message | ブラウザ側でpostMessageが実行されたタイミング |
※ Service Workerのライフサイクルついて
https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle?hl=ja
下記がそれぞれのイベントでおこなう処理内容になります。
CACHE_NAMEにはこちらで指定したキャッシュ名が入ります。
後述するworkboxだけでキャッシュ管理が完結するのであれば、特に何もしなくてもいけそうです。
onInstall
cacheの箱だけ作っておきます
onInstall = function(event) {
event.waitUntil(caches.open(CACHE_NAME))
}
self.addEventListener('install', onInstall);
onActivate
キャッシュ名にキャッシュバージョンをつけて、キャッシュバージョンをあげた時に古いキャッシュを削除するようにします。
onActivate = function(event) {
event.waitUntil(caches.keys().then(function(cacheNames) {
return Promise.all(cacheNames.filter(function(cacheName) {
return cacheName.indexOf(CACHE_VERSION) !== 0;
}).map(function(cacheName) {
return caches["delete"](cacheName);
}));
}));
};
self.addEventListener('activate', onActivate);
onFetch
キャッシュがあればキャッシュを返すようにします
onFetch = function(event) {
event.respondWith(caches.match(event.request.url).then(function(response) {
if (response) {
return response;
}
return fetch(event.request);
}));
};
self.addEventListener('fetch', onFetch);
onMessage
ここはアプリケーションごとの固有処理になってきます。
フロント側から postMessage
でキャッシュするページIDを渡して、キャッシュを生成します。
また古くなったキャッシュは削除するようにしておきます。
この辺りから、少々込み入ってくるので詳細な処理内容については
htmlのキャッシュ で解説します。
serviceworker.js
onMessage = function(event) {
// event.dataにフロントから渡された値が入ってくる
// htmlキャッシュを生成したり、削除する処理
}
self.addEventListener('message', onMessage);
フロント側
フロント側ではpostMessageを実行して、引数にserviceworkerスクリプトに渡したい値を入れます。
実行タイミングは window.onload
などで大丈夫です。
navigator.serviceWorker.controller.postMessage(<servicewokerに渡したい値>)
キャッシュ管理
キャッシュ生成タイミングが2種類あります。
- Runtime Cache: 一度アクセスをして、取得した内容をキャッシュする
- Pre Cache: ブラウザでアクセスする前に必要な要素をキャッシュします
今回はhtmlのキャッシュと、その他のassetsのキャッシュで処理の方法を分けました。
htmlはPre Cache
assetsはRuntime Cache
です。
assetsのキャッシュ
やりたいこと: アクセスしたページで読み込んだcssとjsと一部画像をキャッシュする
workbox-swを使います。
Sprocketsへのworkbox-swの導入は記事を書きましたので、そちらで確認して見てください
Sprocketsへのworkbox-swの導入方法
workbox.routing
モジュールで workbox-runtime
キャッシュの仕組みを使ってキャッシュします。
使い方
workbox.routing.registerRoute(<キャッシュ対象>, <キャッシュ戦略>, callback)
引数 | 値 |
---|---|
キャッシュ対象 | キャッシュ対象のURLを指定します。 正規表現(new Regex) 文字列 workbox.routing.Routeオブジェクト |
キャッシュ方式 | キャッシュの方法を決めます。 ※ |
callback (option) |
コールバックで呼ばれる関数を指定 |
実装例
workbox.routing.registerRoute(
new RegExp('<assetsのURLを正規表現で指定>'),
workbox.strategies.staleWhileRevalidate({
cacheName: "<キャッシュ名>",
plugins: [
new workbox.expiration.Plugin({
maxAgeSeconds: 60 * 60 // キャッシュする時間
})
]
})
)
Railsで使う場合の注意点
application.jsやapplication.cssをキャッシュする場合、
new RegExp('.*application\.js | .*application\.css)
と指定をすると、開発環境では問題ありませんが、productionにデプロイした時にハッシュ値がファイル名について来るため失敗します。
application-3994d44c80aa70ad507ff05c1db46g04.js みたいなの
new RegExp('.*application.*\.js | .*application.*\.css)
こうしときましょう。
※workboxで使えるキャッシュ方式について
色々あります。
名前 | 効果 |
---|---|
cacheFirst | 最初のキャッシュを見て、キャッシュがなけれなネットワークから取得 |
cacheOnly | キャッシュだけを見る |
networkFirst | 最初にネットワークから取得をして、通信できなければキャッシュを見る |
networkOnly | ネットワークからのみ取得 |
staleWhileRevalidate | キャッシュがあればキャッシュを返し、さらにキャッシュの内容をネットワークから取得して更新する |
workbox-routingの詳細はこちら
https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing
htmlのキャッシュ
やりたいこと: 今見ているページと同カテゴリのページhtmlをキャッシュする
workboxのprecacheの仕組みがあるので、それを使えたらよかったのですが
Installイベントでprecacheをしなければならない関係上、取得したいものを動的に変えたい場合は都合が悪かったです。
そのため自前でキャッシュ生成処理を実装します。
- キャッシュ作成方法
- 期限切れキャッシュの削除
キャッシュ作成方法
キャッシュの作成はそれ程難しくないです。
下記のjavascriptを実行すれば、キャッシュが生成されます。
fetch(new Request(<対象ページのURL>, {
<オプション>
})).then(function(response) {
return caches.open(<キャッシュ名>).then(function(cache) {
return cache.put(<対象ページURL>, response);
});
}).catch(function(err) {
//エラー処理
});
期限切れキャッシュの削除
Cache Storageに生成されたキャッシュには有効期限という概念が存在しません。
Chromeの検証ツールで確認をする分には、Time Cachedというカラム?が確認できるのですが、 javascriptでその情報にアクセスすることはできないようです。
そこでキャッシュの有効期限をindexeddbに持たせて管理をします。
キャッシュ削除フロー
- 記事にアクセス
- indexeddbを見て、古いtimestampのレコードを削除
- 古いCache Storageのキャッシュを削除
- 新しくCache Storageにキャッシュを生成
- indexeddbにtimestampを生成
onMessage = function(event) {
// event.dataにフロントから渡された値が入ってくる
// 今回は対象ページの記事IDを想定
var articleId = event.data
urls.forEach(function(articleId, i){
timestampe = new Date().getTime() - 86400000 // 1日前
// 期限切れのキャッシュは全部削除する
deleteExpireCache(timestampe) // indexeddbとcache storageから古いデータを削除する関数
.then(function(result){
// result[1]はarticleIdが入ってくる
return searchCache(result[1]) // 対象ページのキャッシュが既に存在しているか確認
}).then(function(result) {
if(!result[0]) {
//対象ページのキャッシュが存在していなければキャッシュを生成する
fetchArticle(result[1])
}
}).catch(function(err){
})
})
}
最終的にindexeddbに入るデータ
seqはこちらで指定して、こういう名前にしてます。
IDみたいなものだと思ってください。
indexeddbの操作については割と癖があるので、ちょっとハマってしまったりするのですがここでは一旦割愛...
オフライン対応
オフラインの見た目変更
日経さんはオンライン、オフラインで見た目を変更しています。
https://r.nikkei.com/
オンライン | オフライン |
---|---|
こんな感じのことをやりたい場合
オンライン、オフラインは navigator.onLine
で判定出来ます。
フロント側で呼ばれる通常のjavascriptファイルで判定して、表示を変えます。
online = navigator.onLine;
if(online) {
viewOnline()
}else{
viewOffline()
}
window.addEventListener('online', viewOnline)
window.addEventListener('offline', viewOffline)
計測
workboxを使うことでGoogleAnalyticsでオフライン状態の計測できるようになります。
GA側で適当なカスタムディメンジョンを用意して、そこにoffline状態というのが判定できるように値を入れます。
今回はデフォルトで online
の値を入れて、オフライン状態の時に offline
の値を入れます。
GoogleAnalyticsタグ
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-Y', 'auto');
// カスタムディメンジョンのデフォルト値を入れておく
ga('set', 'cd1', 'online');
ga('send', 'pageview');
</script>
serviceworker.js
workbox.googleAnalytics.initialize({
parameterOverrides: {
"cd1": 'offline'
},
hitFilter: function(params) {
var queueTimeInSeconds = Math.round(params.get('qt') / 1000);
params.set('cm1', queueTimeInSeconds);
}
})
参考
https://developers.google.com/web/tools/workbox/modules/workbox-google-analytics
エラーハンドリング
エラー発生時の通知方法です。
- ブラウザへの通知
- sentryへの通知
ブラウザ側への通知
Service Worker → ブラウザ は Client.postMessage
を使います
Service Workerkのエラーをブラウザに通知したいニーズはあまりなさそうですが、念のため。
serviceworker.js
onFetch = function(event) {
sampleFunc()
.then(function(result){
nextFunc(result)
}).catch(function(err){
notifyError(err, event.source.id)
})
}
function notifyError(error, clientId) {
console.error(error)
self.clients.get(clientId).then(function(client) {
client.postMessage({type: 'error', message: error.toString()})
})
}
self.addEventListener('fetch', onFetch);
フロント側
navigator.serviceWorker.addEventListener('message', function(event) {
if(event.data.type == "error") {
alert("エラーが発生しました")
}
sentryへの通知
sentryを使うとjavascriptエラーの通知を受け取れるようになります。
https://sentry.io/welcome/
npm install --save raven-js
この辺りを参考にして、諸々の設定をしてSprocketsで読み込めるようにします。
sentryの設定はこちらを参考してserviceworker.jsに設定をしていきます。
https://docs.sentry.io/clients/javascript/
serviceworker.js
Raven.config("https://<key>@sentry.io/<project>", options).install()
あとはエラーが発生した時に勝手にsentryでエラーが受け取れます。
こちらで意図的に飛ばしたい場合は Raven.captureException(error)
を実行
sampleFunc()
.then(function(result){
nextFunc(result)
}).catch(function(err){
Raven.captureException(err)
notifyError(err, event.source.id)
})