# 20200204 PWA続き ###### tags: `firebase` `web` # 関連資料 1. [Android2019 as Github repository](https://github.com/TakashiSasaki/Android2019/blob/master/README.md) 1. [hello-kbc-sasaki as Glitch project](https://glitch.com/~hello-kbc-sasaki) 1. [はじめてのプログレッシブウェブアプリ](https://developers.google.com/web/fundamentals/codelabs/your-first-pwapp?hl=ja) 1. [Service Worker の紹介](https://developers.google.com/web/fundamentals/primers/service-workers?hl=ja) 1. [サービスワーカー API](https://developer.mozilla.org/ja/docs/Web/API/Service_Worker_API) 1. [Service worker の使用](https://developer.mozilla.org/ja/docs/Web/API/Service_Worker_API/Using_Service_Workers) 1. [ウェブアプリへの Service Worker とオフラインの追加](https://developers.google.com/web/fundamentals/codelabs/offline) # すべてはマニフェストから ## HTMLファイルにおけるマニフェストへのリンク ブラウザがあるウェブサイトを訪れた時、それがPWA的であることをどのように認識するのだろうか。ブラウザはただ当該URLからHTTPリクエストによりHTMLドキュメントをダウンロードするだけである。そのHTML文書のヘッダでマニフェストが書かれているかがすべての始まりとなる。 ```htmlembedded= <!DOCTYPE html> <html lang="en"> <head> <title>hello-kbc-sasaki</title> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="/style.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.min.css" /> <link rel="manifest" href="/manifest.json" /> </head> ``` ## マニフェストの中身 マニフェストはJSONで記述される。 ```json= { "lang": "ja", "short_name": "hks", "name": "hello-kbc-sasaki", "icons": [ { "src": "https://cdn.glitch.com/project-avatar/8cd4fd9a-3eae-4063-abfb-5b60a878c846.png", "type": "image/png", "sizes": "200x200" } ], "start_url": "https://hello-kbc-sasaki.glitch.me/", "display": "standalone", "theme_color": "#2196F3", "background_color": "#2196F3", "orientation": "portrait" } ``` ![](https://i.imgur.com/RqMLYin.png) ## マニフェストの確認 マニフェストが認識されていることは、F12で開くChrome Dev Toolsで確認できる。 ![](https://i.imgur.com/xKzq97t.png) ## マニフェストの効果 マニフェストにより、ブラウザがウェブサイトをウェブアプリとして認識できるようになる。そのため例えば https://hello-kbc-sasaki.glitch.me/ にアクセスするとウェブアプリをインストールするためのUIが表示されるようになる。スマートフォンではホーム画面にショートカットアイコンを置くことができるようになる。 ![](https://i.imgur.com/CGuzpzc.png) # JavaScriptワーカーとサービスワーカー サービスワーカーはJavaScript[ワーカー](https://www.html5rocks.com/en/tutorials/workers/basics/)の一種で、JavaScriptにマルチスレッドをもたらす。ウェブページのDOMにアクセスできる従来のメインスレッドとは独立の実行コンテキストを持つ。 >### ワーカーで使用できる機能 >ウェブワーカーの動作はマルチスレッドになるので、アクセスできるのは次の JavaScript 機能のサブセットのみとなります: > >* navigator オブジェクト >* location オブジェクト(読み取り専用) >* XMLHttpRequest >* setTimeout()/clearTimeout() と setInterval()/clearInterval() >* アプリケーション キャッシュ >* importScripts() メソッドを使った外部スクリプトのインポート >* 他のウェブ ワーカーの生成 >### ワーカーで使用できない機能 >* DOM(非スレッドセーフ) >* window オブジェクト >* document オブジェクト >* parent オブジェクト > >[JavaScript の並行処理という問題](https://www.html5rocks.com/ja/tutorials/workers/basics/) メインスレッドでのグローバルオブジェクトは`window`であるが、ワーカースレッドでのグローバルオブジェクトは`self`である。 >Service Worker はブラウザが Web ページとは別にバックグラウンドで実行するスクリプトで、Web ページやユーザーのインタラクションを必要としない機能を Web にもたらします。 既に現在、プッシュ通知やバックグラウンド同期が提供されています。 さらに将来は定期的な同期、ジオフェンシングなども導入されるでしょう。 このチュートリアルで説明する機能は、ネットワーク リクエストへの介入や処理機能と、レスポンスのキャッシュをプログラムから操作できる機能です。 > [Service Worker の紹介](https://developers.google.com/web/fundamentals/primers/service-workers?hl=ja) * [サービスワーカー API](https://developer.mozilla.org/ja/docs/Web/API/Service_Worker_API) * [Service worker の使用](https://developer.mozilla.org/ja/docs/Web/API/Service_Worker_API/Using_Service_Workers) * [ウェブアプリへの Service Worker とオフラインの追加](https://developers.google.com/web/fundamentals/codelabs/offline) # サービスワーカーの生成 最初はメインスレッドしかないので、メインスレッドからサービスワーカーを生成する。ワーカースレッドでは`install`イベントが発生する。この時点ではワーカースレッドは待機状態であり、続くイベントは発生しない。待機状態のワーカースレッドは`self.skipWaiting()`によりアクティブ状態に移行する。 閲覧のウェブページで動くJavaScript環境におけるグローバルオブジェクトは`window`である。Nagivatorオブジェクトは`window.nagivator`でアクセスできる。さらにサービスワーカーは`window.navigator.serviceWorker`でアクセスできる。すべてのブラウザにおいてサービスワーカーを使えるわけではないので、`serviceWorker`の存在を確認する必要があるのだが、ここでは省略する。 ```javascript= window.addEventListener("load", function() { window.navigator.serviceWorker.ready.then(registration => { console.log("Service worker is ready."); }); window.navigator.serviceWorker.register("/sw.js"); }); ``` サービスワーカーに対して`/sw.js`という相対URLで示されるJavaScriptコードを実行せよ、と指示している。あとはこの`/sw.js`に何を書くかということである。 サービスワーカーが動いていることはDev Toolsで確認できる。 ![](https://i.imgur.com/I0eVRad.png) # サービスワーカーの初期化 ワーカースレッドではそれが登録された直後に`install` イベントが発生するので、これをとらえて初期化を行う。非同期的な処理を受け付ける準備が整うまで一旦待機状態になるのは、ワーカースレッドが扱うデータに不整合が生じるのを防ぐためである。待機状態のワーカースレッドは`self.skipWaiting()`によりアクティブ状態に移行する。 ## `install` イベントに対する処理 ```javascript= self.addEventListener("install", event => { console.log("'install' event is fired."); event.waitUntil( caches .open(CACHENAME) .then(function(cache) { //cache は指定したキャッシュ名に対応するCacheオブジェクト return cache.addAll([ "/manifest.json", "/script.js", "/style.css", "/index.html", "/sw.js", "https://cdn.glitch.com/project-avatar/8cd4fd9a-3eae-4063-abfb-5b60a878c846.png", "https://cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.min.css", "https://www.gstatic.com/firebasejs/7.6.2/firebase-app.js", "https://www.gstatic.com/firebasejs/7.6.2/firebase-auth.js", "https://www.gstatic.com/firebasejs/7.6.2/firebase-database.js" ]); }) .then(() => self.skipWaiting()) .catch(e => { console.log(JSON.stringify(e)); }) ); }); ``` このコードでは`hello-kbc-sasaki-cache-20200204.0`という名前のキャッシュを生成し、事前に読み込んでいる。この処理はPromiseで処理され、最後の`then`で実行される`skipWaiting`により待機状態のワーカースレッドをアクティブ状態に移行している。 ## `waitUntil` とは何か 単純なイベントディスパッチャはイベントの発生に対してあらかじめ登録されたイベントハンドラを実行するだけのものである。そのイベントハンドラ内でエラーが発生したか否かは考慮されない。たとえば`click`イベントで起動された関数の中で起きたエラーをイベントディスパッチャが知る術はない。 これに対して`install`イベントは`Event`の子クラスである[`ExtendableEvent`](https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent)のインスタンスであり、イベントディスパッチャがイベントハンドラにおける処理の終了を待つための`waitUntil`メソッドをもつ。 # `fetch` イベントに対する処理 ```javascript= self.addEventListener("fetch", event => { // abandon non-GET requests if (event.request.method !== "GET") return; let url = event.request.url; event.respondWith( caches.open(CACHENAME).then( //cache は指定したキャッシュ名に対応するChcheオブジェクト cache => { return cache.match(event.request).then(response => { if (response) { //キャッシュからの取り出しに成功した console.log("cache hit for " + url); return response; } // キャッシュからの取り出しに失敗した console.log("cache miss for " + url); return ( // キャッシュに無いのでfetchするしかない fetch(event.request, { mode: 'no-cors' }) .then(newreq => { console.log("network fetch: " + url); if (newreq.ok) cache.put(event.request, newreq.clone()); return newreq; }) // キャッシュにもないしfetchもできなかった .catch(() => { //オフライン用のレスポンスを生成して返す offlineResponse(url); }) ); }); } ) ); }); function offlineResponse(url) { //URLを見てオフライン用の画像やテキストを生成して返す。 //ここではどんなURLに対してもただのプレーンテキストを返している。 console.log(url); return new Response("offline", { headers: { "Content-Type": "text/plain", "Cache-Control": "no-store" } }); } //offlineResponse ``` ## fetch APIとセキュリティ Fetch APIはリクエストの`mode`として`no-cors`を指定することでCORS未対応やCORSの制限を越えてWebサーバからコンテンツを取得できる。ただしResponseオブジェクトでラップすることによりウェブページ側のコードから従来の手段ではコンテンツにアクセスできないという制限をかけている。`cache.addAll`ではSAP(Same Origin Policy)の壁を越えられないが、`self.fetch`では越えられる可能性が有るので、その段階でキャッシュに追加することができるかもしれないが未確認。 [ServiceWorkerはSameOriginを超えられるのか](https://azu.github.io/slide/pixelgrid/serviceworker-cors.html) サービスワーカーからコンテンツが供給されていることはDev Toolsで確認できる。 ![](https://i.imgur.com/T9WuYK2.png) # キャッシュの調整 自分のウェブアプリを構成するために必要なURLはすべてキャッシュする。他のウェブサイトのURLで指定されるリソースをキャッシュに含めることもできるが、これが成功するか否かは自分のウェブアプリの[オリジン](https://developer.mozilla.org/ja/docs/Glossary/Origin)と外部のウェブサーバの[CORS](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)の設定に依存する。 ![](https://i.imgur.com/yb7Xpbo.png) たとえば`mini-default.css`は`cdnjs.cloudflare.com`というCDNサーバから供給されており、これはCORSをあらゆるオリジンに対して許可しているので、キャッシュに成功する。 ## キャッシュの確認 サービスワーカーの介入により何が供給されているかはChrome DevToolsで確認することができる。何度かリロードしてみて、ディスクキャッシュやメモリキャッシュからではなくサービスワーカーから供給されていることを確認する。 ![](https://i.imgur.com/nMoYrEX.png) ## CORS違反でキャッシュできない場合 Firebase JavaScript Libraryは`www.gstatic.com` から供給されており、こちらはキャッシュに失敗する。 ![](https://i.imgur.com/uCJqpnr.png) 確実にキャッシュするためには全てを自分のオリジンにコピーするのが手っ取り早いが、ライセンスは常に適切に扱うこと。 ![](https://i.imgur.com/civ8Ged.png) ウェブアプリの動作に必要なすべてのリソースがサービスワーカーから供給されるようになると、オフラインでの動作が可能となる。 ![](https://i.imgur.com/hIMDziD.png) # 古いキャッシュの削除 ## `activate` イベントに対する処理 ```javascript= //古いキャッシュを破棄する処理 self.addEventListener("activate", event => { console.log("'activate' event is fired."); event.waitUntil( caches .keys() .then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if ([CACHENAME].indexOf(cacheName) < 0) return caches.delete(cacheName); }) ); }) .catch(function(e) { console.log(e); }) ); }); ```