先日からWebKit の ServiceWorker(以後SW) が InDevelopment になったとかなんとかでインターネットがざわついてるよう。
結局国内のトラフィックの大部分を占めている iOS が今まで全然対応がされていなかったがために PWA がまだまだ未来のもののように感じられていたかもしれないが、これでおそらく一気に身近なものになる(なった)のでは無いだろうか。
SW の機能と offline-plugin
SWはWeb Workerの一種であるが、SWがもたらす機能には主に次の3つがある。
- プッシュ通知 (Web Push Notification)
- AppCacheの失敗を踏まえてコントロールしやすくなったキャッシュ
- バックグラウンド同期
このうち、今回紹介する offline-plugin ではキャッシュを簡単に webpack を利用しているプロジェクトに組み込むことが出来る。
なお、 PWA や SW について記されている記事についてはもう既に内容が古くなっているものも多々存在するため注意されたい。
SW のキャッシュで得られる恩恵
なんといってもキャッシュであるので、一度取得したアセットを2度目以降取得する必要をなくし、レスポンスの高速化や通信量の削減が行える。特に通信の不安定な携帯端末にはとても重要な話で、「火星でもみれる」(極端に通信に時間のかかるたとえ)などとも言われる。
もちろん、AppCacheから続くアプリとして必要な機能として「オフラインでもキャッシュを返せる」というものがある。このあたりのコントロールもまた柔軟で、「キャッシュがあればキャッシュを返し、ヒットしなければフェッチする」ことも「ネットワークがオフラインで、キャッシュがあればキャッシュを返す」こともアプリが制御できる。
キャッシュとしての役割を考えると、ナビゲーション(要するにhtml)の読み込みは後者、それ以外のリソースはすべて前者というのが妥当だろうか。
また、必要になるリソースをワーカーで先行してフェッチし、キャッシュしておくことも出来る。例えばこれによって「トンネルに入ってページ遷移したらエラー」なんて問題も防げるかもしれない。1
このエントリで扱う前提条件
もし、あなたの Web アプリが次のような条件を満たしている場合、スムーズに、面倒な作業なしにSWのキャッシュを導入することが出来るだろう。
- アプリはSPAである
- PWA としてルーティングはハッシュフラグメント (
#!
)ではなく完全なURIをHistory APIにて行っているのが好ましい
- PWA としてルーティングはハッシュフラグメント (
- アプリのアセットを webpack でバンドルしている
- アセットにはハッシュを付加してい、 HTML Webpack PluginなどでHTMLに注入してる
- まだServiceWorkerをアプリで使っていない
- FWライブラリ等は基本的に問わない
かならずしもすべてを満たす必要はないかもしれないが、今時のSPAとしては一般的な構成であり、キャッシュの扱いやすさも含め妥当なあたりとした。
導入手順
導入はたった3手順。マジ。
offline-plugin のインストール
プロジェクトに offline-plugin を追加する。
1 2 3 4 |
## -D フラグを付けているが必要に応じて。 yarn add -D offline-plugin # または npm i -D offline-plugin |
webpack の conf に offline-plugin を追加
webpackのプラグインとして登録する。オプションはドキュメントやサンプルやデモなどを参考に。
AppCacheも扱うことが出来るが、今回は面倒なのでなかったことにする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
const OfflinePlugn = require('offline-plugin'); ... plugins: [ ... new OfflinePlugn({ caches: { main: [ 'static/js/manifest.*.js', 'static/css/main.*.css', 'static/js/vendor.*.js', 'static/js/main.*.js', ], additional: [ ':externals:' ], optional: [ ':rest:' ] }, // responseStrategy: 'network-first', externals: [ '/' ], ServiceWorker: { navigateFallbackURL: '/', events: true, }, AppCache: false, safeToUseOptionalCaches: true, }), ... |
caches
ではSWでキャッシュするファイルを指定するが、webpack で吐き出されるファイルをもとに生成される。(デフォルトの'all'
はすべてのwebpackで吐き出されるリソース+externals
で指定したものらしく、これでもいいかもしれない。)パターンマッチ(minimatch)を利用した場合、webpackの出力ファイルでマッチするものが対象となる。
main
にあるアセットはSWのインストール時にfetch&キャッシュ、additional
にあるアセットはSWのactivated
時にfetch&キャッシュされる。
optional
については実際にフェッチされた際にキャッシュされる。
詳しくはドキュメント参照。
responseStrategy
の行をコメントアウトしているが、これをアンコメントするとキャッシュよりもネットワークリソースを優先する。
SW登録スクリプトを追記
クライアントで読み込むjsにsw.js
を読み込ませるためのコードを追加する。といっても、必要なコードは自動生成されるので最短コードはこんな感じ。
1 2 3 |
import OfflinePlugin from "offline-plugin/runtime"; OfflinePlugin.install(); |
はじめなり最後なり好きなところに書いておけばいい。(まあ最初でいいでしょ)
ところで、sw.js
はビルドの度に更新される。ページ読み込み時にブラウザはsw.jsを毎回フェッチするので変更を検出することは出来、インストールはされるが、すべてのセッションがページから離れるまでアクティベートされず古いワーカーが生きていたりする。 つまりはオフライン時に古いキャッシュが返されたりする。
Service Worker のライフサイクル | Web | Google Developers
ServiceWorkerから送られてくるイベントを拾ってやることで即座に有効化させることも出来る。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
OfflinePlugin.install({ onInstalled() { console.log("[SW] Installed"); }, onUpdateReady() { console.log("[SW] Update ready"); OfflinePlugin.applyUpdate(); }, onUpdated() { console.log("[SW] Updated"); // location.reload(); }, onUpdateFailed() { console.log("[SW] Update failed"); } }); |
キャッシュが邪魔して最新版が送られてこないとかがないならばリロードはしないでいいかな、といったところ。(どう?)
ServiceWorker and AppCache update process
なお、TypeScript もサポートされている。 (d.ts
同梱)
試す
セットアップしたらビルドしてサーブしてブラウザで開いてみましょう。ServiceWorkerは本来httpsでのみ利用できるが、別途例外として127.*.*.*
のループバックアドレスやlocalhost
であればhttpsでなくても利用できる。
Chromeで確認してみる。
ServiceWorkerが登録されると、ChromeのDeveloperToolのApplicationタブでServiceWorkerが追加されてるのが確認できる。
また、Networkタブを見てみるとServiceWorkerを経由したリクエストは from ServiceWorker となっている。
ただし、これだけではその時fetchが走ったかわからない。SWからのfetchは歯車のアイコンがつく。キャッシュがないファイルを読みに行った場合、1つのリクエストに2つのリクエストが表示される。
オフラインで
キャッシュの醍醐味、オフライン。
ServiceWorkerタブやNetworkタブにはOfflineのチェックボックスがあるのでチェックを入れれば任意にオフラインのエミュレートが出来る。さて、オフラインにしても例の恐竜が出てこず、アプリが表示されただろうか。sw.js含め幾つかのリソースはエラーになるかもしれないが、ページは正常に表示できることが期待される。
ページの構成によってはツメが甘いと外部リソースの取得などがうまくいかず意図せぬ状態になってしまうかもしれない。そもそもそれは元から問題があるのでなおすべきだろう。
スーパーリロード
通常のキャッシュ同様、Ctrl(Cmd)-Shift–Rのようなスーパーリロードを入力した場合はSWのキャッシュは無視される。表示がおかしければスーパーリロードしてみて……は変わらないのか。
オフラインのままスーパーリロードをすると……例の恐竜が今度こそ現れるはず。
……まあSW自体は生きたままなのでついでにも一回今度は普通にリロードしてみるとまたキャッシュから読み込まれる。(はず)
変更の反映
まずはオフライン解除して……
JS等を書き換えた際に変更が正しく反映されるか。
ドキュメントでは 「 alert('hoge')
でも1行足して試してみると分かりやすいよ」みたいなことが書いてるけどまあなんでもいい。
JSメインエントリの一番上にでも
1 |
alert('ServiceWorker Cache Test'); |
とでもなんでも書いて再ビルドする。出来上がったらリロードしてみてalertがちゃんと表示されれば成功。逆も然り、その行を戻して再ビルド。アラートが出なくなっているか。
(おまけ) PWA達成度とかのテスト
Chrome 60 からLighthouseがDeveloper Tool内に標準搭載されるようになった。
Auditsタブから
- PWA Checklist 達成度
- パフォーマンス評価
- アクセシビリティ評価
- BestPractice テスト
などができる。もちろん改善法も。
むすび
webpackで簡単に Service Worker のキャッシュが試せる offline-plugin を試してみた。
ここではconfのどこに書くとか具体的な指示はあまり行わなかったが、実用的に考えるとproductionビルドでのみバンドルし、開発中は機能しないようにしておいたほうがいいかと思う。もちろんSWのことは念頭に置きながら。
もちろん、開発中もSWを使うことも問題なくできそう。webpack watch
する場合はversion
オプションを変えなきゃなのかな?(いらないかも)
まだまだServiceWorkerが使える世界はChrome, Firefoxだけと狭いが、Webkit や Edge でもそう遠くないうちに使えるようになりそうなので PWA もやっと………期待しよう。
- え?最近はトンネルでも携帯電波入るって? ↩