Edited at

ServiceWorkerと戦って負けた


PWAをやりたい

最近以下のサイトを作った。

https://hatena-hotentry.netlify.com/

このサイトはスクレイピングした結果(データはfirebaseのrealtimeDatabaseに保存)を表示しているだけの、いわば読み物的なサイトなのでオフラインで動作できたらいいなあと思い、PWA化しようとした。そして負けた。


やったこと


既存アプリに@vue/pwaの追加

このサイトはvue-cli 3で雛形を作って作成した。 雛形作成時の vue create を実行した時は特にpwaサポートを選択していなかった。

vue-cli 3のよかったところとして、雛形を元にすでに独自のアプリケーションを作成済みの状態だったとしても、

$ vue add @vue/pwa

を実行すれば、pwaサポートを後からでもやってくれる。実行した結果、以下のファイルがプロジェクトに追加された。

スクリーンショット 2019-03-31 10.35.27.png


service workerを独自にカスタマイズ

プロジェクトのrootにvue.config.jsを作成し、pwaの設定を書いていく。

ここの各設定の詳細は以下の記事がわかりやすかった。

https://tech.mercari.com/entry/2017/12/19/workbox

https://blog.nagisa-inc.jp/archives/1132

公式のドキュメントはこちら


vue.config.js

const path = require('path');

module.exports = {
pwa: {
workboxPluginMode: 'GenerateSW',
workboxOptions: {
swDest: path.join(__dirname, '/dist/service-worker.js'),
globDirectory: path.join(__dirname, '/dist/'),
globPatterns: ['*.{html,js,css}'],
runtimeCaching: [
{
urlPattern: /.*firebase.*/,
handler: 'networkFirst',
options: {
cacheName: 'api',
}
},
{
urlPattern: /\.(png|svg|woff|ttf|eot)/,
handler: 'cacheFirst',
options: {
cacheName: 'assets',
}
}
]
},
},
}


workboxPluginModeGenerateSWinjectManifest を指定する。デフォルトはGenerateSW

injectManifest は細かい設定をしたい時用。事前にworkboxのコードを自分で書いておき、swSrc でパス指定する感じの使い方だが、よくわかっていない)

serviceworkerのキャッシュには PreCacheRuntimeCache の2種類がある。

PreCache はwebpackがバンドルしたjsファイル、cssファイルなどの静的リソースのキャッシュ。ServiceWorkerのインストールと同時にキャッシュを生成するため、初回ロード時でもすでにキャッシュが存在している。

RuntimeCache はAPIのレスポンスなど、動的リソースのキャッシュ。ネットワークリクエストが発生した際にキャッシュが作成されるため、2回目のロードのときにキャッシュが存在している。

この両方のキャッシュを用意しておくことでオフラインの環境になった時でもサイトが動く状態が作れる。


PreCache

上記のvue.config.jsの

      globDirectory: path.join(__dirname, '/dist/'),

globPatterns: ['*.{html,js,css,json}'],

の部分がPreCacheの設定。vue-cliはデフォルトでdist配下にバンドルしたファイルを吐き出す。

globDirectory にそのパスを指定してあげて、 globPatterns の正規表現に一致したファイルが PreCache の対象になる。


RuntimeCache

以下の設定が RuntimeCache の設定。

      runtimeCaching: [

{
urlPattern: /.*firebase.*/,
handler: 'networkFirst',
options: {
cacheName: 'api',
}
},
{
urlPattern: /\.(png|svg|woff|ttf|eot)/,
handler: 'cacheFirst',
options: {
cacheName: 'assets',
}
}
]

api(firebaseのrealtimeDatabseからのデータ取得)からのレスポンスのキャッシュと、画像などのassets系のキャッシュの設定を書いている。

handler に書いている networkFirst はネットワークを優先し、ネットワークに繋がらなかったらキャッシュから返す。

cacheFirst キャッシュを優先しキャッシュになければネットワークから返す。という設定。

そのほか handler に指定できる種類はここを参照


ビルドの実行

yarn build(vue-cli-service build)を実行すれば、swDest に指定したところにservice-worker.jsが生成される。

上記のvue.config.jsで作成されたservice-worker.jsが以下。


dist/service-worker.js

/**

* Welcome to your Workbox-powered service worker!
*
* You'll need to register this file in your web app and you should
* disable HTTP caching for this file too.
* See https://goo.gl/nhQhGp
*
* The rest of the code is auto-generated. Please don't update this file
* directly; instead, make changes to your Workbox build configuration
* and re-run your build process.
* See https://goo.gl/2aRDsh
*/

importScripts("https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js");

importScripts(
"/precache-manifest.bd7ce76c63cdb56febe84b2ebb8ce649.js"
);

workbox.core.setCacheNameDetails({prefix: "hatena-hotentry"});

/**
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/

self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

workbox.routing.registerRoute(/.*firebase.*/, workbox.strategies.networkFirst({ "cacheName":"api", plugins: [] }), 'GET');
workbox.routing.registerRoute(/\.(png|svg|woff|ttf|eot)/, workbox.strategies.cacheFirst({ "cacheName":"assets", plugins: [] }), 'GET');


この作成されたservice-worker.jsは vue add @vue/pwa を実行した際に追加された registerServiceWorker.js で読み込まれる。


registerServiceWorker.js

/* eslint-disable no-console */

import { register } from 'register-service-worker'

if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
)
},
registered () {
console.log('Service worker has been registered.')
},
cached () {
console.log('Content has been cached for offline use.')
},
updatefound () {
console.log('New content is downloading.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
}


ビルドが完了したら Web Server for Chrome を利用して動作確認を行う。

Web Server for Chrome の使い方のイメージはこちらが参考になった。

https://qiita.com/n11sh1/items/5d64c337ef927ac8d5d6


動作確認


初回のロード

service workerはちゃんと動いている。

スクリーンショット 2019-03-31 17.55.39.png

preCacheもちゃんとされている

スクリーンショット 2019-03-31 17.56.51.png


2回目のロード

RuntimeCache がassetsの方しか存在しない。

スクリーンショット 2019-03-31 17.58.02.png

vue.config.jsで以下のように設定したapiのRuntimeCacheが存在しない。

        {

urlPattern: /.*firebase.*/,
handler: 'networkFirst',
options: {
cacheName: 'api',
}
},

この状態だと、PreCache と assetsの RuntimeCache はserviceWorkerが返してくれるようになってるが、firebaseの方は通常通りのリクエストが行われている。

スクリーンショット_2019-03-31_18_01_39.png


realtimeDatabseはwebsocket

realtimeDatabseからデータ取得する際、 wss://s-usc1c-nss-204.firebaseio.com/ にアクセスしており、正規表現的には

          urlPattern: /.*firebase.*/,

で一致する。

スクリーンショット 2019-03-31 18.05.52.png

一致するのにキャッシュができてない原因は、もしかしたらwebsocketなのがダメなんじゃないかと思い、検索してみたらそれっぽいのが見つかった。

https://github.com/GoogleChrome/workbox/issues/624


The Firebase Realtime API uses websockets to communicate with the server, and websockets can't be intercepted by service workers.


https://stackoverflow.com/questions/37741185/is-it-possible-to-intercept-and-cache-websocket-messages-in-a-service-worker-lik


It's not possible for a service worker to intercept Web Socket traffic.


service workerはwebsocketの通信を傍受することはできないらしい。

他の手段でオフライン対応できないかと思って、firebaseSDKのマニュアルをみていたが、iosとかandroidのfirebaseSDKだと、realtimeDatabaseのデータをローカルに保存しておくことでデータを永続させ、オフラインでも利用できるようにする便利メソッドが生えている。でもwebのSDKにはない。

ここら辺で負けた。


最後に

websocketを使ったwebをpwaでオフライン化することはできない?

pwaの知見が全然ないので方法があればコメントなどで教えていただきたいです。