PWA(Progressive Web Apps)と戯れてみる

はじめに
-----

Livesense Advent Calendar「学」の7日目です。「学」から連想できるものであれば、なんでもよしということで何を題材としようかと迷いましたが、せっかくなので「業務上あまり関わりがない分野の技術を学ぶ」ということにしました。

普段はAWSやGCPなどのクラウドやコンテナ周りを触ることが多く、インフラ関連がほとんどのため、今回はフロントエンド周りで熱いと噂のPWAを触ってみたいと思います。

PWAに関しては、Qiitaでもまだ投稿が少なく30件程度でした。特に目新しいことをしておらず、何番煎じかわかりませんが、なにか参考になることがあれば幸いです。また、的外れなことを書いてたらご指摘いただけると助かります。

PWA(Progressive Web Apps)とは
-----

Googleの記事を借用すると、以下の特徴を持つようです。
https://developers.google.com/web/fundamentals/codelabs/your-first-pwapp/?hl=ja

> ウェブとアプリの両方の利点を兼ね備えたアプリです。ブラウザのタブで表示してすぐに利用することができ、インストールの必要はありません。使い続けてユーザーとの関係性が構築されていくにつれ、より強力なアプリとなります。不安定なネットワークでも迅速に起動し、関連性の高いプッシュ通知を送信することができます。また、ホーム画面にアイコンを表示することができ、トップレベルの全画面表示で読み込むことができます。

| 特徴            | 説明                  |
| -------------- | -------------------------------------------------------------------------------- |
| 段階的            | プログレッシブ・エンハンスメントを基本理念としたアプリであるため、 ブラウザに関係なく、すべてのユーザーに利用してもらえます。                  |
| レスポンシブ         | パソコンでもモバイルでもタブレットでも、次世代の端末でも、 あらゆるフォームファクタに適合します。                                |
| ネットワーク接続に依存しない | **Service Worker の活用により、オフラインでも、 ネットワーク環境が良くない場所でも動作**します。                           |
| アプリ感覚          | App Shell モデルに基づいて作られているため、アプリ感覚で操作できます                                          |
| 常に最新           | Service Worker の更新プロセスにより、常に最新の状態に保たれます。                                         |
| 安全             | 覗き見やコンテンツの改ざんを防ぐため、**HTTPS 経由**で配信されます。                                              |
| 発見しやすい         | W3C のマニフェストとService Worker の登録スコープにより、 **「アプリケーション」として認識されつつ、検索エンジンからも発見**することができます。 |
| 再エンゲージメント可能    | **プッシュ通知**のような機能を通じで容易に 再エンゲージメントを促すことができます。                                         |
| インストール可能       | ユーザーが気に入れば**アプリのリンクをホーム画面に残して** おくことができ、アプリストアで探し回る必要はありません。                         |
| リンク可能          | URL を使って簡単に共有でき、複雑なインストールの必要はありません。                                              |

重要そうな部分を太字にしています(元記事では太字ではありません)

現状でいうと、PWAはあまり本格的に採用されていないように思えます。
おそらく、SafariのレンダリングエンジンであるWebkitがService Workerにまだ対応していないためでしょう(PWAはService Workerに依存しています)

ただ、8月3日にWebkitが「In Development」になったのを機に、流れが変わっていくかもしれません。

https://caniuse.com/#feat=serviceworkers
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155801/b36b51e6-7f73-63e4-20a4-be8d677ea887.png)

ところでService Workerって？
-----

こちらのGoogleの記事がとてもわかりやすいです。
https://developers.google.com/web/fundamentals/primers/service-workers/?hl=ja

> Service Worker はブラウザが Web ページとは別にバックグラウンドで実行するスクリプトで、Web ページやユーザのインタラクションを必要としない機能を Web にもたらします。すでに現在、プッシュ通知やバックグラウンド同期が提供されています。さらに将来は定期的な同期、ジオフェンシングなども導入されるでしょう。

PWAの実装例
-----

2017年4月に[TwitterがPWAを採用](https://blog.twitter.com/official/ja_jp/topics/product/2017/twitter-lite_.html)して話題になりました。
 https://mobile.twitter.com/ にスマートフォンからアクセスして、「ホーム画面に追加」をするとPWAとしてインストールできます。

戯れてみる
-----

以下の流れで確認していきます。今回はGCP+Firebaseを使いますが、Github Pageでも動作確認可能だと思います。

1. GCPのプロジェクト作成
2. firebaseの設定諸々
3. コーディング
4. デプロイ
5. 実機確認
6. トークン取得
7. プッシュ通知送信

端末は、Android 7.0のZenfone3で確認しています。ご了承ください。

### 1. GCPのプロジェクト作成

Firebaseを使うにあたって、あらかじめGCP(Google Cloud Platform)でプロジェクトを作成しておく必要があります。
https://console.cloud.google.com/home/

### 2. firebaseの設定諸々

1で作成したGCPのプロジェクトとFirebaseのプロジェクトを紐付けます。
https://console.firebase.google.com/?hl=ja

![Screen Shot 2017-12-07 at 6.00.50.png](https://qiita-image-store.s3.amazonaws.com/0/155801/04fe6355-5c36-e93d-14b7-65f1447175a1.png)

`firebase-tools`をnpmインストールして、初期設定を行います。`firebase init`によって、`.firebaserc`と`firebase.json`が生成されます。
https://firebase.google.com/docs/functions/get-started?hl=ja

```
$ npm install -g firebase-tools
$ firebase login
$ firebase init
$ mkdir app
```

以下の`"public": "app"`はディレクトリ名と一致している必要があります。

```json:firebase.json
{
    "hosting": {
      "public": "app",
      "ignore": [
        "firebase.json",
        "**/.*"
      ]
    }
}
```

### 3. コーディング

- app
  - images
     - app-icon-144.png
  - index.html
  - main.js
  - manifest.json
  - sw.js
- .firebaserc
- firebase.json

#### index.html

`<link rel="manifest" href="./manifest.json">`でWeb App Manifestを参照しています。
https://developer.mozilla.org/ja/docs/Web/Manifest

> Web App Manifest は、シンプルなドキュメントでアプリケーションの（名前や著者、アイコン、説明のような）情報を提供します。主な目的は、プログレッシブ Web アプリ を作ることにあります

`script`タグにおいて、見慣れない`"/__/"`という呼び出し方をしてますが、これはFirebase Hostingを使っている時の便利記法のようです。最初よくわからず詰まりました。
https://firebase.google.com/docs/web/setup?hl=ja

2つ目のハマりポイントとして、Firebase Hostingの場合は、公式ドキュメントでよく見かける`firebase.initializeApp(config);`も不要です。呼び出すと、`Firebase: Firebase App named '[DEFAULT]' already exists (app/duplicate-app).`というエラーにぶつかります。

```html:index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="manifest" href="./manifest.json">
    <title>PWA Sample</title>
</head>
<body>
    <button onclick="requestPermission()">Request Permission</button>
    <script src="/__/firebase/4.6.2/firebase-app.js"></script>
    <script src="/__/firebase/4.6.2/firebase-messaging.js"></script>
    <script src="/__/firebase/init.js"></script>
    <script src="./main.js"></script>
</body>
</html>
```

#### manifest.json

各設定項目についてはこちらに記載があります。
https://developers.google.com/web/fundamentals/web-app-manifest/?hl=ja

`gcm_sender_id`はFirebase用の固定値です。
https://firebase.google.com/docs/cloud-messaging/js/client?hl=ja

```json:manifest.json
{  
    "name":"PWA Sample",
    "short_name":"PWA",
    "start_url":"./",
    "orientation":"any",
    "display":"standalone",
    "background_color":"#fff",
    "gcm_sender_id":"103953800507",
    "icons":[  
       {  
          "src":"images/app-icon-144.png",
          "sizes":"144x144",
          "type":"image/png"
       }
    ]
 }
```

#### main.js

Service Workerへの登録や、ユーザに通知権限を求める処理を記載しています。`requestPermission()`は`index.html`におけるbuttonの`onclick`で呼び出されています。

```js:main.js

const messaging = firebase.messaging();

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('./sw.js').then(registration => {    
    console.log(registration);
    messaging.useServiceWorker(registration);
  }).catch(error => {
    console.error(error);
  });
}

window.addEventListener('online', e => {
    console.log('online');
}, false);

window.addEventListener('offline', e => {
    console.log('offline');
}, false);

// アプリがフォアグラウンドにある場合にプッシュ通知が届いた場合にログ出力
// https://firebase.google.com/docs/cloud-messaging/js/receive?hl=ja
messaging.onMessage(payload => {
    console.log(payload);
});

// ボタン押下のタイミングでユーザに通知権限を求める
function requestPermission() {
    messaging.requestPermission().then(() => {
        messaging.getToken().then(token => {
            console.log(token);
        }).catch(error => {
            console.error(error);
        });
    }).catch(error => {
      console.error(error)
    });
}
```

#### sw.js

Service Workerの本体です。

`install`、`activate`、`fetch`などのイベントが出てきますが、これらライフサイクルについてもGoogleの記事がわかりやすいです。このQiita記事では完全にスルーしている、Service Workerのスコープの話や更新についても解説してくれています。

https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle?hl=ja

```js:sw.js

importScripts('/__/firebase/4.6.2/firebase-app.js');
importScripts('/__/firebase/4.6.2/firebase-messaging.js');
importScripts('/__/firebase/init.js');

const cacheName = 'v1';
const messaging = firebase.messaging();

self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(cacheName).then(cache => {
            return cache.addAll([
                './',
                './main.js'
            ]).then(() => {
                self.skipWaiting();
            });
        })
    );
});

self.addEventListener('activate', event => {
    console.log('activate');  
});

self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request).then(response => {
            if (response) {
                return response;
            }
            return fetch(event.request);
        })
    );
});

// アプリがバックグラウンドにある場合にプッシュ通知が届いた場合にログ出力
// https://firebase.google.com/docs/cloud-messaging/js/receive?hl=ja
messaging.setBackgroundMessageHandler(payload => {
    console.log(payload);
    const title = 'Background Message Title';
    const options = {
      body: 'Background Message body.'
    };
  
    return self.registration.showNotification(title, options);
});
```

### 4. デプロイ

```
$ firebase deploy
$ firebase open hosting:site
```

### 5. 実機確認

下から、PWAのインストールバナーが表示されています。
自動で表示されるインストールバナーは条件があるのでご注意ください。これらの条件をスキップするには、chrome://flags/#bypass-app-banner-engagement-checks を有効にします。

https://developers.google.com/web/fundamentals/app-install-banners/?hl=ja
> 2 回以上のアクセスがあり、そのアクセスに 5 分以上の間隔がある。

![Screenshot_20171207-081402.jpg](https://qiita-image-store.s3.amazonaws.com/0/155801/157aa64a-ccc9-32af-dfa5-866ab32b2666.jpeg)

「追加」を押下するとホーム画面にアイコンが追加されます。この画像や名前は`manifest.json`で指定したものです。

![Screenshot_20171207-081416.jpg](https://qiita-image-store.s3.amazonaws.com/0/155801/8c7742be-8878-fbd1-a1f7-dc62d829f9b2.jpeg)

個人的に驚いたのですが、PWAとしてインストールしたアプリは、ネイティブアプリ同様、Androidのアプリ一覧に表示されました。通知のオン/オフもこの画面からできます。

![Screenshot_20171207-081437.jpg](https://qiita-image-store.s3.amazonaws.com/0/155801/a8bc96b2-6a00-b080-2216-43b844635644.jpeg)


### 6. トークン取得

トークンは現状consoleに流すようにしています。

```js:main.js
...
        messaging.getToken().then(token => {
            console.log(token);
        }).catch(error => {
            console.error(error);
        });
...
```

AndroidとPCをUSBで繋いで、リモートデバッグしましょう。
https://developers.google.com/web/tools/chrome-devtools/remote-debugging/?hl=ja

まずはAndroid端末を「デバッグモード」にする必要があります。

- 設定=>端末情報=>ビルド番号=>連続7回タップ
- 設定=>開発者向けオプション=>USBデバッグ=>オン

USBを接続したら、Chromeの設定を行います。
![Screen Shot 2017-12-07 at 6.49.10.png](https://qiita-image-store.s3.amazonaws.com/0/155801/48eb77d7-789d-32ec-c65d-a318e1169e47.png)

「Enter URL」の部分にfirebaseのURLを入力してInspectすれば接続できます。
![Screen_Shot_2017-12-07_at_6_50_59.jpg](https://qiita-image-store.s3.amazonaws.com/0/155801/cf20ba0b-62cb-a348-1e4f-b21b9b01c5a9.jpeg)

この状態で、「Request Permission」ボタンを押下すると、Chromeコンソール上にトークンが表示されます。

### 7. プッシュ通知送信

```
$ curl -X POST \
       --header "Authorization: key=<Server Key>" \
       --header "Content-Type: application/json" \ 
       -d '{"to":"<Token>","notification":{"body":"Hello"},"priority":10}' \
       https://fcm.googleapis.com/fcm/send
```

- `<Token>`の部分はログに出力されたトークンを指定してください。
- `<Server Key>`の部分はFirebaseの管理画面において、「プロジェクト設定」→「クラウドメッセージング」のページで確認できます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155801/30f0233a-070b-453f-e5b3-2ea569ffbe5b.png)

![Screen_Shot_2017-12-07_at_5_45_05.jpg](https://qiita-image-store.s3.amazonaws.com/0/155801/ae2448de-a5ef-9f05-3523-42cb1a9876f3.jpeg)

上記を実行すると、以下のようにプッシュ通知が届きました。

![Screenshot_20171207-053104.jpg](https://qiita-image-store.s3.amazonaws.com/0/155801/fa16bb32-8221-db86-9904-48efdb4cfcb6.jpeg)

まとめ
-----

駆け足でしたが、PWAのインストールからプッシュ通知まで挙動を確認しました。
前述したとおり、SafariでService Workerがまだ使えないため、PWAが浸透するまでに時間がかかると思いますが、個人的には大きな可能性を感じています。

この先もPWAの動向についてはウォッチしていこうと思います。