俺的PWAの振り返り

# はじめに

PWAに手を出したものの、手を出してからほったらかして1年近く。
ちょっと復習しておこうと思います。
理解が充分でないところはこれから深めていきます。
もし間違っていることがあればどんどん突っ込んでくださいｗ

拙作アプリの「[LTタイマー](https://taka-hama.sakura.ne.jp/pwa/lttimer/)」を題材に進めていこうかと思います。
LTタイマーを作ったのが1年以上前なのでもしかしたらやり方が古いかもしれないので、気づいた時点でアップデートしていきたい。
参考サイトのソースをベースとして、少し手を加えています。

# 参考
[PWA： ServiceWorkerを使って、キャッシュをコントロールする(オフラインハンドリング)](https://qiita.com/OMOIKANESAN/items/13a3dde525e33eb608ae)
[MDN ウェブアプリマニフェスト](https://developer.mozilla.org/ja/docs/Web/Manifest)
[僕の考えた最強のService Workerキャッシュ戦略で爆速サービスを作った](https://qiita.com/tiwu_official/items/47e8a7c3e6f2d57816d7)

# PWAとは
PWAとはProgressive Web Appsの略です。
ネイティブアプリのような感じのWebアプリを作成できます。
あくまでネイティブアプリのような感じなので、ネイティブアプリにはできるけどPWAではできないこともあります。

PWAでアプリを作成・動作させるためには、PWAに対応したWebブラウザが必要になります。
PWAに対応していないブラウザでは、ただのWebサイトとして扱われるので心配はいりません。
また、localhostまたはHTTPSでないと、PWAは利用できません。
GitHubを使うと、GitHub PageでHTTPSで公開できるので、Webサーバーを自前で用意しなくてもPWAを試すことができます。
(PWAの勉強会で知った。アウトプットすると予想外のインプットがあるのでアウトプット大事！）

PWA自体は、ウェブアプリマニフェスト(manifest.json) + ServiceWorker + Cache API の組み合わせで成り立っています。

## ServiceWorker
Service Workerはブラウザにインストールされ、バックグランドで常駐します。
まだやったことはありませんが、Service Workerでプッシュ通知を扱うことができるとのこと。
あと、ServiceWorkerはDOMにアクセスできません。

## manifest.json
manifest.jsonはPWAの設定をJSON形式で記載したものです。
アプリの名前、表示方法、アイコンなどなどが設定できます。

## Cache API
Cache APIをつかって、ローカルストレージに読み込んだコンテンツを保存したり、読み込んだりします。
ここではローカルストレージをメインで紹介していますが、実はローカルストレージのほかにSession Strage、IndexedDB、WebSQLなども扱えるとのことです、この辺も、調べないとね。

# LTタイマーの構成
[GitHub](https://github.com/project-dev/lttimer_pwa)を見てもらうとわかりますが、こんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/50900f63-5142-e437-c608-a0a1e9dc2dd3.png)

ちなみに、GitHubの内容は古い場合がありますので注意してください。

manifest.jsonがマニフェスト
sw.jsがサービスワーカーの実装になります。

# 処理の概要
まずはServiceWorkerの登録処理になります。
登録処理はindex.htmlでおこなっています

```javascript:index.html
if ('serviceWorker' in navigator) {
	navigator.serviceWorker.register('./sw.js')
	.then(function(registration){
		console.log('Service Worker install success.');

		registration.onupdatefound = function() {
			// 更新があると呼び出される
			console.log('Update : ServiceWorker');
			registration.update();
		}
	})
	.catch((error) => {
		// registration failed
		console.log('register faild ： ', error);
	});
}
```

まず、ServiceWorkerに対応しているかどうかのチェックが必要になります。
それを最初のif文で行っています。
次に、ServiceWorkerで動かすJavaScriptファイルを登録します。

```javascript:index.html
	navigator.serviceWorker.register('./sw.js')
```

ServiceWorkerに変更があった場合の対応として、registration.onupdatefoundメソッドを実装しています。
この中で、update()メソッドを呼び出します。
※気になることが出てきたので、調べて更新する予定

```javascript:index.html
	registration.onupdatefound = function() {
		// 更新があると呼び出される
		console.log('Update : ServiceWorker');
		registration.update();
	}
```

sw.jsの内容です。
まず、ServiceWorkerではファイルのキャッシュを行います。
キャッシュするファイルの一覧を配列で宣言しています。
CACHE_NAMEとVERSIONを結合してるのは、ServiceWorkerの更新が発生した際に、キャッシュの更新を行うことを目的としているためです。その辺は、後で説明します。

```javascript:sw.js
var CACHE_NAME = 'lttimer';
var VERSION="1.0.1"
var CACHE_FILE = [
	 './index.html'
	,'./css/DSEG7Classic-Regular.woff'
	,'./css/PixelMplus10-Regular.woff'
	,'./js/jquery-1.9.1.min.js'
	,'./sound/Zihou01-mp3/Zihou01-1.mp3'
	,'./sound/Zihou01-mp3/Zihou01-1.ogg'
	,'./sound/silent.mp3'
	,'./sound/silent.ogg'
];
const CACHE_KEYS = [
  CACHE_NAME + VERSION
];
```


次にServiceWorkerのインストールイベントでキャッシュへの登録処理を行います。
installイベントは1回しか呼び出されません。

```javascript:sw.js
self.addEventListener('install', function(e) {
	e.waitUntil(
		caches.open(CACHE_NAME.then(function(cache) {
			return cache.addAll(CACHE_FILE);
		})
	);
});
```

installイベントが終わるとactivateイベントが呼び出されます。
acticateの中では、キャッシュのキーと一致しないキャッシュの削除処理をおこなっています。

```javascript:sw.js
self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(keys => {
            return Promise.all(
                keys.filter(key => {
                    return !CACHE_KEYS.includes(key);
                }).map(key => {
                    // 不要なキャッシュを削除
                    if(key.indexOf(CACHE_NAME) == 0){
                        console.log("ServiceWorker : " + key+ " remove");
                        return caches.delete(key);
                    }else{
                        console.log("ServiceWorker : " + key+ "no remove");
                        return true;
                    }
                })
            );
        })
    );
});
```

次にfetchイベントでリクエストを処理します。
ここでリクエストを元にキャッシュから取り出したり、キャッシュにないファイルを登録します。
場合によっては古いキャッシュを削除し、新しいファイルをキャッシュします。
(ここもほぼコピペであまり理解していない・・・^^;)

fetchイベントの中はオンライン、オフラインで処理を分けます。
オンラインオフラインの切り分けは navigator.onLine　でできるとのこと。

```javascript:sw.js
self.addEventListener('fetch', function(event) {
  //ブラウザが回線に接続しているかをboolで返してくれる
  var online = navigator.onLine;
  if(online){
    //オンラインのときの制御
  }else{
    //オフラインのときの制御
  }
});
```

オンラインの処理はこんな感じ
まだ、処理を完全に理解しきっていないのですが、たぶんこのコメント通りでいいはず。

```javascript:sw.js
    event.respondWith(
        caches.match(event.request).then(
            function (response) {
                if (response) {
                    // キャッシュを返す
                    return response;
                }
                return fetch(event.request).then(function(response){
                    // キャッシュにないので追加
                    // Responseはストリームなのでキャッシュなので複製
                    cloneResponse = response.clone();
                    if(!response || response.status != 200){
                        //正常に取得できなかったときにハンドリングしてもよい
                        console.log("ServiceWorker : request faild " + response.status);
                    }else{
                        //現行のキャッシュに追加
                        caches.open(CACHE_NAME + VERSION).then(function(cache){
                            cache.put(event.request, cloneResponse).then(function(){
                                //正常にキャッシュ追加できたときの処理(必要であれば)
                                console.log("casshed");
                            });
                        });
                    }
                    return response;
                }).catch(function(error) {
                    //デバッグ用
                    return console.log(error);
                });
            }
        )
    );
```
event.respondWith()の中で、matchメソッドでキャッシュの検索結果を処理します。
一致すればそれを返却し、一致しなければ、キャッシュに登録します。

オフラインの時はこんな感じ。

```javascript:sw.js
    event.respondWith(
        caches.match(event.request).then(
            function(response) {
                // キャッシュがあったのでそのレスポンスを返す
                if (response) {
                    return response;
                }
                //オフラインでキャッシュもなかったパターン
                return caches.match("offline.html").then(function(responseNodata){
                    //適当な変数にオフラインのときに渡すリソースを入れて返却
                    //今回はoffline.htmlを返しています
                    return responseNodata;
                });
            }
        )
    );
```

offline.htmlを返却するようにしているけど、offline.htmlを用意していない・・・ｗ
基本的にはオンラインの時と同じでmatchの結果で処理を行っています。

ここまでで最低限と思われる処理になります。

# manifest

manifestファイルには、アプリケーションのアイコンや、名称などの情報を設定します。
設定項目が多くあるので、MDNの[ウェブアプリマニフェスト](https://developer.mozilla.org/ja/docs/Web/Manifest)のページを参考にするとよいと思います。

```json:manifest.json:
{
	"name": "LT Timer",
	"orientation": "landscape",
	"display": "standalone",
	"start_url": "./",
	"short_name": "LT Timer",
	"description": "LT Timer",
	"background_color": "#000020",
	"theme_color": "#000020",
	"icons": [
			{
				"src": "./img/icon_48.png",
				"type": "image/png",
				"sizes": "48x48"
			},
		{
			"src": "./img/icon_96.png",
			"type": "image/png",
			"sizes": "96x96"
		},
		{
			"src": "./img/icon_192.png",
			"type": "image/png",
			"sizes": "192x192"
		}
	]
}
```

とりあえず、使っている設定の概要を

|設定名|概要|
|---|---|
|name|アプリケーション名|
|orientation|アプリケーションの向き|
|display|standalone|
|start_url|アプリケーションの開始URL|
|short_name|アプリケーションの短縮名|
|description|アプリケーションの説明|
|background_color|背景色|
|theme_color|アプリケーションのテーマ色|
|icons|アプリケーションのアイコン|


# デバッグ

デバッグはChromeのデベロッパーツールを使うのが良いと思います。
SafariとかFirefoxとかは使わないのでよくわからないけど。

デベロッパーツールをですが、通常のWebの開発で利用する機能は当然ですが、PWAに関するところで重要なのが以下の2点。

- 保存したコンテンツを削除
- Service Workerの登録解除
- オンライン・オフラインの切り替え

これができないとデバッグが大変。というか無理でしょう。
上記の内容は、デベロッパーツールのApplicationタブで確認できます。

また、console.logでログを出力ができます。
というか、デベロッパーツールでApplicationタブのサービスワーカーを見るとエラーがカウントされているけど、確認ができません(自分だけ？)
そのため、ログに出さないとサービスワーカーでエラーが発生しても、エラーがでいるかの把握が困難になります。

# 最後に
この内容はYoutubeのチャンネルや日本Androidの会 浜松支部の2月の定例会でも扱いたいなぁと思っています。