とりあえず途中まで進んだので記録。
表題の通り既存のWebアプリにServiceWorkerを追加して、PWAっぽく使えるようにします。操作対象は最近いろいろいじっている、CastBackgroundです。今回はキャッシュ処理とmanifest.jsonの設定だけします。
実際どうやってるのかを見たい場合は、上記ソースコードを直接確認してください。manifest.json
とsw.js
を見れば大抵わかると思います*1。
実際の手順
既存サイトにServiceWorkerを追加する場合、やるべきことは以下の通り
- (manifest.jsonを追加する)
- ServiceWorker用のJavaScriptファイルを「ルートフォルダに」作成する
- キャッシュ対象となるファイルのリストを作成する
- ServiceWorkerのライフサイクルに対応するイベントハンドラを記述する(最低限必要なのは、install, fetch。たぶんactivate)
とりあえず順を追って確認
(manifest.jsonを追加する)
ServiceWorkerとは直接関係ないですが、まずはmanifest.jsonをつくっておくと、アプリがPWAとして認識されるようになるので、作っておくと便利です。
この辺は参考文献のMDNを参考に。とりあえず記述の通りの内容を書いておけば問題はなさそうです。Edgeで開くと「Web app manifest should have the filename extension 'webmanifest'.」と言われていますがその辺はよくわからないのでとりあえずスキップ
アイコンファイルなどは事前に作成しておくと良いでしょう。
ServiceWorker用のJavaScriptファイルを「ルートフォルダに」作成する
まずは、ServiceWorkerを作成します。インストール処理については大まかにいろんなサイトに書いてある限りで問題なし。
<script type="module"> if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', {scope: "/"}) .then((reg) => { console.log('Service worker registered.', reg); }); } </script>
type="module"
はとりあえず流れで書いてしまっていますが要らないかも(外したときの挙動を確認していないので記載しています)。
第一引数にはServiceWorkerとして動作するためのJavaScriptファイルを指定します。
ServiceWorkerは、サイトの状態などを確認するためにブラウザにインストールされるUIを持たないJavaScriptファイルです。このファイルでオフライン時のキャッシュの制御などの操作を行います。
第二引数のscopeについては、第一引数のJavaScript配下のディレクトリパスを指定します。未指定時はJavaScriptファイルのあるディレクトリとなります。
インストールが完了すると、以降scope配下のファイルリクエストがあったときに、このスクリプトが呼び出されるようになります。このため、よほどのことがなければアプリケーションのルートディレクトリ、すなわち/を指定することになります。
なお、ここでsw.jsがあるディレクトリより上位のディレクトリを指定すると例外が発生します*2。
これは「Service Worker、はじめの一歩 | 第1回 Service Workerとは | CodeGrid」によると、「Service-Worker-Allowedヘッダ」というヘッダがないと変更できない動作のため、sw.jsは素直にルートディレクトリに置くのが無難です。
キャッシュ対象となるファイルのリストを作成する
次に、キャッシュしたいファイルの一覧を作成します。キャッシュ対象は自分の作成したファイルだけでなく、CDNなどを参照しているJQueryやBootstrapなどのファイルも対象です。
この辺については面倒なので、わたしは次のようなファイルを列挙してJavaScriptファイルを生成するPythonスクリプトを書きました。ファイルの変更があったときはこれを使用する ということで。
from pathlib import Path import json def files(rootdir: Path, path: Path) -> list[str]: l = [] for f in path.glob("*"): if str(f.name).startswith("."): continue if f.is_file() and f.suffix == "": continue if f.is_file() and f.suffix in [".pptx", ".py", ".json", ".md"]: continue if f.is_dir() and f.name in ["tools"]: continue if f.is_file() or not f.name in ["res", "src"]: fs = "/{}{}".format(str(f.relative_to(rootdir)).replace('\\', '/'), '/' if f.is_dir() else '') print(fs) l.append(fs) if f.is_dir(): l.extend(files(rootdir, f)) return l root = Path(__file__).parent.parent data = json.dumps(files(root, root), indent=2) with (root / "src" / "files.js").open("w") as f: f.write(f""" const CB_ALL_FILES = {data} """) print("Done.")
そして、sw.jsの冒頭に以下のように書きます。
importScripts("./src/files.js") const CB_ALL_FILES_URLS = CB_ALL_FILES.concat( "https://cdn.honokak.osaka/honoka/4.3.1/css/bootstrap.min.css", "https://code.jquery.com/jquery-3.5.1.slim.min.js", "https://cdn.honokak.osaka/honoka/4.3.1/js/bootstrap.bundle.min.js" );
これでファイルリストの作成は完了です。*3。
なお、詳細は不明ですが、このような記述と、JavaScript読み込み時のintegrity
やcrossorigin
の指定は競合を起こす場合があります*4。
ServiceWorkerのライフサイクルに対応するイベントハンドラを記述する
sw.jsに各種イベントハンドラを書いていきます。ServiceWorkerのライフサイクルはGoogleの記事などを見ればおおよそわかります。
とりあえず最低限キャッシュを動作させるには、install
とfetch
だけ書けば良いっぽい(ただ適宜キャッシュを捨てるなどの処理におそらくactivate
も必要)。
キャッシュが必要なファイルの一覧作成
install
でキャッシュ必要なURLのリスト(https://からはじまるアドレスかサーバルートからの絶対パス)を指定します。
self.addEventListener('fetch', (event) => { console.log('service worker fetch ... ' + event.request); event.respondWith( caches.match(event.request).then((cacheResponse) => { return cacheResponse || fetch(event.request).then((response) => { return caches.open('v1').then((cache) => { cache.put(event.request, response.clone()); return response; }); }); }) ); });
上記のopen()メソッドの引数に使っている「v1」はキャッシュの呼称なので、本来であれば定数として持っておいた方が良さそうです。定数が変わればキャッシュを参照しなくなります。
ただしこの時点ではキャッシュが必要なURLのリストは指定できてもキャッシュは行なわれていない状態です。実際のキャッシュはfetch
で行ないます。
fetchする
次に実際にファイルをダウンロードし、必要ならキャッシュに追加します。
self.addEventListener('install', (event) => { event.waitUntil( caches.open('v1').then((cache) => { return cache.addAll( CB_ALL_FILES ); }) ); });
これでキャッシュは完了です。
動作確認する
動作確認には、Edgeの開発者モードがとても便利です。Applicationタブでほとんどの情報を参照することができます。
ただしくServiceWorkerがインストールされていれば、「ServiceWorkers」項目にsw.js
の名前が、「キャッシュストレージ」の項目にキャッシュされているファイルの一覧が表示されています(キャッシュストレージの項目に入っているはずのファイルがなかった場合は、files.jsの中身を確認します)。
オフライン時の挙動を確認する
オフライン時の挙動を確認するときは、画面上部のオフラインチェックボックスをチェックします。するとその間はインターネットに接続していない扱いになります。
ServiceWorkerを削除してやりなおす
キャッシュが古くて更新したいときなど、ServiceWorkerを削除するときは、「登録解除」ラベルをクリックします(実運用の時は、ちゃんとアンインストール処理を設けましょう)。
ただしそのまま再読込するとページがうまく読めないときがあるので、一度ブラウザタブを閉じてから開き直すと良いです。
(この時点での)まとめ
とりあえず落とし穴がところどころにあるのが注意が必要ですが、PWAのキャッシュストレージ機能を実装しました。実際使うにはキャッシュを適宜更新したり、期限切れキャッシュを削除したりする必要がありますが、ひとまず一つ目の峠は越えたようです。
参考文献
- プログレッシブウェブアプリ (PWA) | MDN
- [Javascript] Service Worker 初心者入門、概要から使い方まで │ Web備忘録
- Service Worker Offline Cache Techniques - Qiita
- FetchEvent.respondWith() - Web APIs | MDN
- Service Worker Offline Cache Techniques - Qiita
- ExpressとServiceWorkerを利用してPush通知を送る - Qiita
- ServiceWorkerとCache APIを使ってオフラインでも動くWebアプリを作る - Qiita
- サービスワーカー API - Web API | MDN
*1:執筆時点ではfeature-pwa_support_n32というブランチで作業中です
*2:Failed to register a ServiceWorker for scope ('指定しようとしたURL') with script ('sw.jsのファイルURL'): The path of the provided scope ('指定したディレクトリ') is not under the max scope allowed ('sw.jsのあるディレクトリ'). Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.
*3:なお、ディレクトリも必要であれば指定が必要です。たとえば多くのサーバーでは「/src/」がリクエストされた場合「/src/index.html」が返されますが、ServiceWorker側では相手のサーバが挙動をするサーバかどうかが判断できないからです。なお、今回は「/src/」に直接アクセスする可能性がないので記述していません
*4:実際に指定したファイルを読み込もうとすると「The FetchEvent for "https://code.jquery.com/jquery-3.5.1.slim.min.js" resulted in a network error response: an "opaque" response was used for a request whose type is not no-cors」というエラーが出ました。Githubで検索をしてみるとそれでも読めているソースコードが散見されるのでなにか手順が抜けている気もします