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サポートを後からでもやってくれる。実行した結果、以下のファイルがプロジェクトに追加された。
service workerを独自にカスタマイズ
プロジェクトのrootにvue.config.jsを作成し、pwaの設定を書いていく。
ここの各設定の詳細は以下の記事がわかりやすかった。
https://tech.mercari.com/entry/2017/12/19/workbox
https://blog.nagisa-inc.jp/archives/1132
公式のドキュメントはこちら
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',
}
}
]
},
},
}
workboxPluginMode
は GenerateSW
か injectManifest
を指定する。デフォルトはGenerateSW
。
(injectManifest
は細かい設定をしたい時用。事前にworkboxのコードを自分で書いておき、swSrc
でパス指定する感じの使い方だが、よくわかっていない)
serviceworkerのキャッシュには PreCache
と RuntimeCache
の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が以下。
/**
* 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
で読み込まれる。
/* 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
動作確認
初回のロード
2回目のロード
RuntimeCache
がassetsの方しか存在しない。
vue.config.jsで以下のように設定したapiのRuntimeCacheが存在しない。
{
urlPattern: /.*firebase.*/,
handler: 'networkFirst',
options: {
cacheName: 'api',
}
},
この状態だと、PreCache
と assetsの RuntimeCache
はserviceWorkerが返してくれるようになってるが、firebaseの方は通常通りのリクエストが行われている。
realtimeDatabseはwebsocket
realtimeDatabseからデータ取得する際、 wss://s-usc1c-nss-204.firebaseio.com/
にアクセスしており、正規表現的には
urlPattern: /.*firebase.*/,
で一致する。
一致するのにキャッシュができてない原因は、もしかしたらwebsocketなのがダメなんじゃないかと思い、検索してみたらそれっぽいのが見つかった。
The Firebase Realtime API uses websockets to communicate with the server, and websockets can't be intercepted by service workers.
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の知見が全然ないので方法があればコメントなどで教えていただきたいです。