Vue CLI 3 で PWA チュートリアル（Service Workers / Add to Home Screen / Push Notifications）

**オリジナル記事**
https://blog.n11sh1.com/vue-pwa-tutorial-ja/

# PWAとは？
**PWA（Progressive Web App）** の一部に以下のような機能があります。

- Service Workers
- Add to Home Screen
- Push Notifications

これにより Webアプリ として開発しても Nativeアプリ 同様のUXをユーザーに提供できます。
ここらへんを **Vue** を使って試していきます。

> *Ref. [Progressive Web Apps | Web | Google Developers](https://developers.google.com/web/progressive-web-apps/)
Ref. [はじめてのプログレッシブ ウェブアプリ - Google Codelabs](https://codelabs.developers.google.com/codelabs/your-first-pwapp-ja/#0)*

# Vue CLI 3 + PWA

デモ
https://vue-pwa-tutorial.firebaseapp.com

ソースコード
https://github.com/n11sh1/vue-pwa-tutorial

## 環境構築手順
### 1. Vue CLI のインストール
> *Ref. [Installation | Vue CLI 3](https://cli.vuejs.org/guide/installation.html)*

```terminal
$ npm install -g @vue/cli
```

### 2. 新規プロジェクト作成
```terminal
$ vue create vue-pwa-tutorial
```

### 3. マニュアルモードを選択
```terminal
Vue CLI v3.3.0
? Please pick a preset: 
  default (babel, eslint) 
❯ Manually select features 
```

### 4. PWAを選択
```terminal
Vue CLI v3.3.0
? Please pick a preset: Manually select features
? Check the features needed for your project: 
❯◉ Babel
 ◉ TypeScript
 ◉ Progressive Web App (PWA) Support
 ◉ Router
 ◉ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
```
※TypeScript, Router, Vuex の選択はお好みで


### 5. あとは適当に選択
```terminal
Vue CLI v3.3.0
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, PWA, Router, Vuex, Linter
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript for auto-detected polyfills? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: TSLint
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? Yes
? Save preset as: 
```

### 6. プロジェクト作成中…
```terminal
Vue CLI v3.3.0
✨  Creating project in /Users/zyyx-kubo/WVVUProjects/chatty/vue-pwa-tutorial.
🗃  Initializing git repository...
⚙  Installing CLI plugins. This might take a while...
```

### 7. サーバー起動
```terminal
$ cd vue-pwa-tutorial/
$ npm run serve
```

### 8. 開発環境にアクセス
PC
<img width="1437" alt="vue-pwa-tutorial.png" src="https://qiita-image-store.s3.amazonaws.com/0/121942/00c0517f-b312-0d20-7eea-a7a333221625.png">

SP
![ダウンロード1.png](https://qiita-image-store.s3.amazonaws.com/0/121942/041a1aba-7180-bb4c-ea9e-17e96e029113.png)

このままでは単なるWebサイトなので、PWAらしくしていきます！


# PWA の機能を試す
## Service Workers
`npm run serve` で起動した場合、以下のように **Service Workers** は動きません。
Developer Tool > Application > Service Workers
![vue-pwa-tutorial_と_Web_Server_for_Chrome.png](https://qiita-image-store.s3.amazonaws.com/0/121942/fb058542-ef9d-3b6a-21d4-6f6c7de8ba08.png)

**Service Workers** の挙動確認には `npm run build` でビルドする必要があります。
*[Web Server for Chrome](https://chrome.google.com/webstore/detail/web-server-for-chrome/ofhbbkphhbklhfoeikjpcbhemlocgigb)* を使ってビルドされた `./dist` ディレクトリを公開すると、以下のように **Service Workers** が動いていることが確認できます。（Service Workers を使用しない画面の修正などは `npm run serve` で問題なく動作確認できます）

```terminal
$ npm run build
 DONE  Build complete. The dist directory is ready to be deployed.
```

![Web_Server_for_Chrome.png](https://qiita-image-store.s3.amazonaws.com/0/121942/a7f5e5dd-540a-594c-e509-4b205d5b56dc.png)

![vue-pwa-tutorial2.png](https://qiita-image-store.s3.amazonaws.com/0/121942/6629e523-b9a5-a738-e82b-dbfcde96c921.png)
![vue-pwa-tutorial3.png](https://qiita-image-store.s3.amazonaws.com/0/121942/0bd4094c-acdb-04ed-20cd-d90715b001d4.png)
静的コンテンツ（*.html, *.css, *.js, *.png, *.txt）が precache されています。

Developer Tool > Application > Service Workers > Update on reload にチェックを付ければ、リロードの度にキャッシュを更新してくれます。（Cache Storage から対象のファイルを削除でも可）

## Add to Home Screen
### Web App Manifest
「ホーム画面に追加」のダイアログを表示するには、JSON形式の **Web App Manifest** ファイルが必要です。
Vue CLI で PWA を選択するとデフォルトで作成されるので、そちらをそのまま使用します。

```json:./public/manifest.json
{
  "name": "vue-pwa-tutorial",
  "short_name": "vue-pwa-tutorial",
  "icons": [
    {
      "src": "./img/icons/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "./img/icons/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": "./index.html",
  "display": "standalone",
  "background_color": "#000000",
  "theme_color": "#4DBA87"
}
```

### 開発環境
PC
Developer Tool > Application > Manifest > Add to homescreen
![vue-pwa-tutorial4.png](https://qiita-image-store.s3.amazonaws.com/0/121942/b869d64b-c886-2b08-3c5c-f3c7165773dd.png)

[chrome://apps/](chrome://apps/)
![アプリ.png](https://qiita-image-store.s3.amazonaws.com/0/121942/75015500-be3a-d329-e3c7-4e06fadfa24e.png)

SPはこのままだと見えないので、サーバーにデプロイします。

### デモ環境（Firebase Hosting）
*[Firebase](https://firebase.google.com/?hl=ja)* を使う理由としては、次に **Push Notifications** を試すので、そのための布石でもあります。

> *Ref. [Deployment Firebase | Vue CLI 3](https://cli.vuejs.org/guide/deployment.html#firebase)*

#### 1. 新規プロジェクト作成
まずは *[Firebase console](https://console.firebase.google.com/u/0/)* にGoogleアカウントでサインインして、プロジェクトを作成します。
![screencapture-console-firebase-google-u-0-2019-01-18-21_39_00.png](https://qiita-image-store.s3.amazonaws.com/0/121942/e4a84214-6dd5-49d9-829e-bdbe9fcd0044.png)

#### 2. デプロイ
デプロイはコマンドで実行するので *[Firebase CLI](https://github.com/firebase/firebase-tools)* をインストール

```terminal
$ npm install -g firebase-tools
```

ログインする

```terminal
$ firebase login
```

プロジェクトルートで初期化

```terminal
$ firebase init hosting

? Select a default Firebase project for this directory: (Use arrow keys)
  [don't setup a default project] 
❯ vue-pwa-tutorial (vue-pwa-tutorial) 
  [create a new project] 

? What do you want to use as your public directory? (public) dist

? Configure as a single-page app (rewrite all urls to /index.html)? (y/N) y

? File dist/index.html already exists. Overwrite? (y/N) N
```

デプロイ

```terminal
$ firebase deploy --only hosting
```

#### 3. 動作確認
SP
![ダウンロード2.png](https://qiita-image-store.s3.amazonaws.com/0/121942/9e88ed8d-f33d-19ea-c319-8aa831f793e6.png)
![ダウンロード3.png](https://qiita-image-store.s3.amazonaws.com/0/121942/69bf2483-a186-c6b9-b1a4-502e03a396ec.png)
![ダウンロード5.png](https://qiita-image-store.s3.amazonaws.com/0/121942/f9615f4d-8344-42b3-b63a-23281e248272.png)
![ダウンロード4.png](https://qiita-image-store.s3.amazonaws.com/0/121942/4f7e351c-d2e2-a42b-6860-23ad4dd12253.png)


これで Nativeアプリ 同様にホーム画面にアイコンが追加されました。（アプリストアからインストールする手間も減ります）

静的コンテンツがキャッシュされているので、動作も早いです。（APIなどもキャッシュできるようですが、今回は割愛）

## Push Notifications
プッシュ通知には *[Firebase Cloud Messaging(FCM)](https://firebase.google.com/docs/cloud-messaging/?authuser=0)* を使用します。

> *Ref. [JavaScript Firebase Cloud Messaging クライアント アプリを設定する | Firebase](https://firebase.google.com/docs/cloud-messaging/js/client?authuser=0)
Ref. [JavaScript クライアントでメッセージを受信する | Firebase](https://firebase.google.com/docs/cloud-messaging/js/receive?authuser=0)
Ref. [Firebase Cloud Messaging Quickstart | GitHub
](https://github.com/firebase/quickstart-js/tree/master/messaging)*

### 1. FCM の Web Push 証明書 を取得
![vue-pwa-tutorial_–_設定_–_Firebase_console.png](https://qiita-image-store.s3.amazonaws.com/0/121942/0ac015c4-a663-4f94-0639-d4cde0e359ee.png)

### 2. Web App Manifest に gcm_sender_id を追加
```javascript:./public/manifest.json
{
  ...,
  "gcm_sender_id": "103953800507"
}
```
### 3. Firebase SDK のインストール
```terminal
$ npm install --save firebase
```

### 4. Firebaseの設定情報を取得
Firebase console > プロジェクトの設定 > 全般 > アプリ > Web
![vue-pwa-tutorial_–_設定_–_Firebase_console2.png](https://qiita-image-store.s3.amazonaws.com/0/121942/092c2556-aacc-a274-e608-23a62bc7d67f.png)


### 5. 通知の受信許可とトークン取得

src/main.ts に以下のコードを追加

```typescript:src/main.ts
import * as firebase from "firebase";

var config = {
  apiKey: "xxxxxxxxxxxx",
  authDomain: "xxx.firebaseapp.com",
  databaseURL: "https://xxx.firebaseio.com",
  projectId: "xxx",
  storageBucket: "xxx.appspot.com",
  messagingSenderId: "xxxxxxxxxxxx"
}; // 4をコピペ
firebase.initializeApp(config);

const messaging = firebase.messaging();

messaging.usePublicVapidKey("xxxxxxx"); // 1で取得した鍵ペア

// 通知の受信許可
messaging.requestPermission().then(() => {
  console.log('Notification permission granted.');

  // トークン取得
  messaging.getToken().then((token) => {
    console.log(token)
  })
}).catch((err) => {
  console.log('Unable to get permission to notify.', err);
});
```

### 6. FCM 用の Service Worker を追加
FCM では `firebase-messaging-sw.js` で Firebase メッセージング Service Worker を定義する必要があります。
（あるいは、useServiceWorker を使って既存の Service Worker を指定できます。こちらは割愛）

```javascript:./public/firebase-messaging-sw.js
// [START initialize_firebase_in_sw]
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here, other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/5.5.6/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/5.5.6/firebase-messaging.js');
// Initialize the Firebase app in the service worker by passing in the messagingSenderId.
firebase.initializeApp({
  'messagingSenderId': 'xxxxxxxxx' // 4の messagingSenderId をコピペ
});

// Retrieve an instance of Firebase Messaging so that it can handle background messages.
const messaging = firebase.messaging();
// [END initialize_firebase_in_sw]
```

Push Notification は Service Workerが動いている必要があるので `npm run build` して Web Server for Chrome で確認します。
![http___127_0_0_1_8887_が次の許可を求めています_と_vue-pwa-tutorial.png](https://qiita-image-store.s3.amazonaws.com/0/121942/45e63a77-d635-3d2f-c615-2fdfc39fd8f0.png)
![vue-pwa-tutorial5.png](https://qiita-image-store.s3.amazonaws.com/0/121942/100f8657-3305-9e50-3983-cf13e027667c.png)

### 7. 動作確認
<img width="1439" alt="vue-pwa-tutorial_–_設定_–_Firebase_console6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121942/e928e329-1725-f6fa-5dca-608539fa26b9.png">

以下の curl でメッセージ受信テスト

```terminal
$ curl -X POST -H "Authorization: key=${↑のサーバーキー}" -H "Content-Type: application/json" -d '{
  "to": "${5のトークン}",
  "notification": {
  	"title": "FCM Message",
    "body": "This is an FCM Message",
    "icon": "./img/icons/android-chrome-192x192.png"
  }
}' https://fcm.googleapis.com/fcm/send
```

PC
<img width="1432" alt="Banners_and_Alerts_と_n11sh1_vue-pwa-tutorial__Vue_CLI_3___PWA_Plugin.png" src="https://qiita-image-store.s3.amazonaws.com/0/121942/32e9c7b1-d8fc-870b-8909-655e80ebb568.png">
通知が届きました！
ページがフォアグラウンド（フォーカスされている場合）は、メッセージは届きません。
<img width="1427" alt="vue-pwa-tutorial6.png" src="https://qiita-image-store.s3.amazonaws.com/0/121942/07ea14d0-d60f-409b-9b34-c70ce100d63f.png">

SP
SPも同様に通知を許可して curl を実行するとプッシュ通知が届きます。（アプリアイコンにも通知ドットが付きます）
![ダウンロード6.png](https://qiita-image-store.s3.amazonaws.com/0/121942/58ad8b89-2378-08c0-6fde-e19ae54677b6.png)
![ダウンロード7.png](https://qiita-image-store.s3.amazonaws.com/0/121942/f2d11337-5c30-4c15-8fc1-a3a4106bab1d.png)
![ダウンロード8.png](https://qiita-image-store.s3.amazonaws.com/0/121942/c3fa2a21-3bd7-3e00-00a5-3d0fa503ec90.png)

# まとめ
**PWA** を使えば Webアプリでも Nativeアプリ に近い UX が提供できます。

今回紹介できていないこともあるので、また機会があれば書きます。

- Service Workers
    - Cache Control
    - Offline Page
- Push Notifications
    - Token Refresh
    - Firebase Cloud Messaging

# さいごに
個人ブログにオリジナル（日本語＆英語）をUPしています。
この記事が良いと感じた方は、そちらにもリアクション頂けると嬉しいです:relaxed:
https://blog.n11sh1.com