Vue.jsで始めるPWA

# はじめに
先月、フロントエンドエンジニア界隈では[リニューアルした日経電子版が高速すぎてヤバイ件](https://note.mu/konpyu/n/n9fd5b81f6305)に注目が集まりましたね。
今自分で注目しているVue.jsと、PWAでSPAをつくるためのことはじめを記事にしようと思います。

# 1. PWAとは
PWA（Progressive Web Application）とは、一言で言えば「ネイティブアプリのような使い勝手を実現したWebアプリ」です。
[Googleのコードラボ](https://codelabs.developers.google.com/codelabs/your-first-pwapp-ja/#0)を一通りやると全体像がつかめるかと思います。

## PWAでできること
### ホームスクリーンへの追加（Add to homescreen）
PWAで作られたWebアプリケーションにアクセスした際に、ホーム画面への追加を促すことができます。
条件は下記です。
- 必要な情報が記載されたマニフェストファイルが存在する
- サイトにService Workerが登録されている
- HTTPS経由で配信されている
- 2回以上のアクセスがあり、そのアクセスに5分以上の間隔がある

詳細は[こちら](https://developers.google.com/web/fundamentals/app-install-banners/?hl=ja)。


### キャッシュ（Service Worker Caching）
Webアプリケーションの骨格となるApp Shellの`pre-cache`やWebアプリケーションの使用中に取得するリソースの`runtime-cache`を利用することができます。
表示速度を高速化したり、オフラインのユーザ体験を向上させたりすることができます。


### プッシュ通知（Push Notification）
プッシュ通知と言えばネイティブアプリのイメージがありましたが、Webアプリでもプッシュ通知を送ることができます。
サービスエンゲージメントを高めていくうえで上手く使用すると効果があります。

### バックグラウンド同期
オフラインでのリクエストをためておき、オンラインになったタイミングで処理を進めることができる機能です。
例えばチャットサービスを使っていて地下鉄などで電波が悪くなることがあります。
そんなときにオフライン状態でメッセージを送信すると、電波状況が悪い区間を抜けてオンラインになったタイミングで同期することができます。


## PWAを作るための条件
PWAを作るためには下記の条件を満たしている必要があります。

− HTTPSのサイトであること
- ブラウザがService Workerに対応していること

Service Workerの[ブラウザ実装状況](https://developer.mozilla.org/ja/docs/Web/API/ServiceWorker_API/Using_Service_Workers#ブラウザ実装状況)を見ると、まだまだと言った感じがします。
しかしながら、[iOSのSafariでサポートされる](https://www.suzukikenichi.com/blog/ios-is-likely-to-support-service-worker/)という情報もあり、展望は開けていると個人的には考えています。


# 2.　環境整備
PWAでできることを大まかに理解したところで、開発できる環境を整えていきます。
## 2.1 vue-pwa-boilerplate
[vue-pwa-boilerplate](https://github.com/vuejs-templates/pwa)はVue.jsでPWAを開発できる雛形です。
`vue-cli`を使って簡単にプロジェクトを作成することができます。

```
$ npm install -g vue-cli
$ vue init pwa my-project
$ cd my-project
$ npm install
$ npm run dev
```

`npm run dev`で開発サーバを立ち上げるとブラウザに画面が表示されます。
![スクリーンショット 2017-12-08 9.52.30.png](https://qiita-image-store.s3.amazonaws.com/0/29971/9ba5388c-c167-ac28-cebe-3f90f347fd65.png "スクリーンショット 2017-12-08 9.52.30.png")

これでVue.jsでSPAが開発できる状態になりました。一方で、PWAという意味ではもう一手間必要です。


## 2.2 Web Server for Chrome
vue-pwa-voilerplateでは、開発ビルド（`npm run dev`）ではService Workerが登録されません。
`build/service-worker-dev.js`と`build/service-worker-prod.js`を見比べてみてください。
`npm run build`でService Workerが登録される状態でビルドされます。


```
⠙ building for production...Total precache size is about 136 kB for 5 resources.
Hash: 2990e7f381c49b2088ee
Version: webpack 3.10.0
Time: 5389ms
                                              Asset       Size  Chunks             Chunk Names
      static/img/icons/apple-touch-icon-152x152.png    4.05 kB          [emitted]
              static/js/app.7b4c3edaaf02253673f6.js      12 kB       0  [emitted]  app
         static/js/manifest.7ac8a796b3963c4ae8b1.js    1.49 kB       2  [emitted]  manifest
static/css/app.1d063bc0cd301699760e884e1e4c3379.css  528 bytes       0  [emitted]  app
          static/js/app.7b4c3edaaf02253673f6.js.map    42.5 kB       0  [emitted]  app
       static/js/vendor.68998a222dcb7b47ea9a.js.map     973 kB       1  [emitted]  vendor
     static/js/manifest.7ac8a796b3963c4ae8b1.js.map    14.2 kB       2  [emitted]  manifest
                                         index.html     2.5 kB          [emitted]
        static/img/icons/android-chrome-192x192.png    9.42 kB          [emitted]
      static/img/icons/apple-touch-icon-120x120.png    3.37 kB          [emitted]
        static/img/icons/android-chrome-512x512.png    29.8 kB          [emitted]
           static/js/vendor.68998a222dcb7b47ea9a.js     120 kB       1  [emitted]  vendor
      static/img/icons/apple-touch-icon-180x180.png    4.68 kB          [emitted]
        static/img/icons/apple-touch-icon-60x60.png    1.49 kB          [emitted]
        static/img/icons/apple-touch-icon-76x76.png    1.82 kB          [emitted]
              static/img/icons/apple-touch-icon.png    4.68 kB          [emitted]
                 static/img/icons/favicon-16x16.png  799 bytes          [emitted]
                 static/img/icons/favicon-32x32.png    1.27 kB          [emitted]
                       static/img/icons/favicon.ico    15.1 kB          [emitted]
    static/img/icons/msapplication-icon-144x144.png    1.17 kB          [emitted]
                static/img/icons/mstile-150x150.png    4.28 kB          [emitted]
             static/img/icons/safari-pinned-tab.svg    10.6 kB          [emitted]
                               static/manifest.json  436 bytes          [emitted]

  Build complete.

  Tip: built files are meant to be served over an HTTP server.
  Opening index.html over file:// won't work.
```

ここでもう一つ問題となるのは、`npm run dev`や`npm run start`で起動するNode.jsの開発サーバではService Workerが動かないという点です。
Service Workerの挙動を確認するにあたっては[Web Server for Chrome](https://chrome.google.com/webstore/detail/web-server-for-chrome/ofhbbkphhbklhfoeikjpcbhemlocgigb)を導入します。
`dist/`以下にビルドされた諸々のファイルが吐き出されるので、パスを指定して開きます。
![スクリーンショット 2017-12-08 9.20.45.png](https://qiita-image-store.s3.amazonaws.com/0/29971/d96511dd-2bc2-c127-bae0-e3550e0964ed.png "スクリーンショット 2017-12-08 9.20.45.png")

`127.0.0.1:8887`にアクセスすると先ほどと同じ画面が表示されます。
ここで、Chromeの開発者ツールを開いて`Application > Service Workers`タブを開いてみましょう。

![スクリーンショット 2017-12-08 9.25.28.png](https://qiita-image-store.s3.amazonaws.com/0/29971/57cc7fbb-7c0d-fc9d-6707-d09403bd65e9.png "スクリーンショット 2017-12-08 9.25.28.png")

Service Workerが稼動しているのがわかります。

画面を実装するときは`npm run dev`、Service Workerの挙動を確認したい場合はWeb Server for Chromeと言った具合に使い分けると良いと思います。


# 3. Vue.jsのSPAをPWA化する
今の段階では、ほとんどただのSPAなので、PWAらしくしていきます。
今回やるのは
- Service Workerを使ったキャッシング
- ホームスクリーンへの追加
です。


## 3.1 キャッシュによるオフライン体験の向上
### workbox-webpack-pluginの導入とpre cache
vue-pwa-boilerplateにはキャッシュするためのプラグインとして[sw-precache-webpack-plugin](https://github.com/goldhand/sw-precache-webpack-plugin)が入っています。
webpackのプラグインなので、設定ファイル上で次のように記述されています。

```javascript:webpack.prod.conf.js
    // service worker caching
    new SWPrecacheWebpackPlugin({
      cacheId: 'my-pwa',
      filename: 'service-worker.js',
      staticFileGlobs: ['dist/**/*.{js,html,css}'],
      minify: true,
      stripPrefix: 'dist/'
    })
```

いわゆる、[AppShell](https://developers.google.com/web/fundamentals/architecture/app-shell?hl=ja)をキャッシュする設定が書かれています。

このまま使っても良いのですが、今回は[workbox-webpack-plugin](https://www.npmjs.com/package/workbox-webpack-plugin)を使っていきたいと思います。

```
npm uninstall sw-precache-webpack-plugin
npm install --save-dev workbox-webpack-plugin
```


```diff:webpack.prod.conf.js
- const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin')
+ const workboxPlugin = require('workbox-webpack-plugin') 

// 中略

-    new SWPrecacheWebpackPlugin({
-      cacheId: 'my-pwa',
-      filename: 'service-worker.js',
-      staticFileGlobs: ['dist/**/*.{js,html,css}'],
-      minify: true,
-      stripPrefix: 'dist/'
-    })

+    new workboxPlugin({
+      cacheId: 'my-pwa',
+      globDirectory: config.build.assetsRoot,
+      globPatterns: ['**/*.{html,js,css}'],
+      swDest: path.join(config.build.assetsRoot, 'service-worker.js'),
+      skipWaiting: false,
+      clientsClaim: true
+    })

```


Service Workerのライフサイクルに関わる部分（`skipWaiting`、`clientsClaim`）に関しては本エントリでは省略しますが、押さえておくべきところなので別エントリで書こうと思います。

改めて`npm run build`して`127.0.0.1:8887`にアクセスします。
この時点だと、`sw-precache-webpack-plugin`のときに登録されたService Workerが生きていて、キャッシュも残っているので一度クリアして再読み込みしてみます。

![スクリーンショット 2017-12-08 10.26.18.png](https://qiita-image-store.s3.amazonaws.com/0/29971/ef52a9b7-6625-3c8a-3aca-2c76c22f34c0.png "スクリーンショット 2017-12-08 10.26.18.png")

![スクリーンショット 2017-12-08 10.26.31.png](https://qiita-image-store.s3.amazonaws.com/0/29971/fb3807f0-08bd-1429-9a06-b35091bf69da.png "スクリーンショット 2017-12-08 10.26.31.png")

AppShellがキャッシュされていました！


### runtime-caching
Workboxではpre-cachingだけでなく、runtime-cachingも設定することができます。runtime-cachingは例えばAPIの呼び出し結果をキャッシュしておき、同じAPIを呼び出した際にサーバにリクエストを投げずに、キャッシュから結果を取得させることができます。

`http://api.myservice.com/something`といったようなエンドポイントのAPIを作ったとして、それをSPAから呼び出すことを想定します。

```diff:webpack.prod.conf.js
    new workboxPlugin({
      cacheId: 'my-pwa',
      globDirectory: config.build.assetsRoot,
      globPatterns: ['**/*.{html,js,css}'],
      swDest: path.join(config.build.assetsRoot, 'service-worker.js'),
      skipWaiting: false,
-     clientsClaim: true
+     clientsClaim: true,
+     runtimeCaching: [
+       {
+         // APIのキャッシュ
+         urlPattern: /.*api.*/,
+         handler: 'networkFirst',
+         options: {
+           cacheName: 'api',
+           cacheExpiration: {
+             maxAgeSeconds: 60 * 60 * 24
+           }
+         }
+       }
+     ]
    })
```

ここで設定したパラメータは下記のとおりです。

- urlPattern: キャッシュするURLのパターン
- handler: キャッシュ戦略
- options.cacheName: キャッシュの名前
- options.maxAgeSeconds: キャッシュの生存期間（秒）

### キャッシュ戦略と生存期間
[Comprehensive caching strategies](https://developers.google.com/web/tools/workbox/#comprehensive_caching_strategies)によると、キャッシュ戦略には次のようなものがあります。

- Cache only
- Cache first, falling back to network
- Cache, with network update
- Network only
- Network first, falling back to cache

今回は、`networkFirst`としてみました。
これはオフライン時にネットワークからリソースが取れなかったときにキャッシュに格納してあるリソースを使用する（本当は常に最新の情報をネットワークから取得したいけど）ことを意図しています。また、キャッシュの生存期間は秒単位の時間を指定します。

更新性の低いリソースであれば戦略を`cacheFirst`として生存期間を長くするなど、
キャッシュ戦略や生存期間は、取得するリソースの特性に応じて適切に設定すると良いでしょう。


## ホームスクリーンへの追加
### Add to Home Screen
ホームスクリーンへの追加を実装するには、`project/static/manifest.json`を設定します。
vue-pwa-boilerplateを導入した段階で既に追加されるようになっています。

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

```

PCで開発しながら確認するためには、`Application > Manifest`タブ内にある`Add to homescreen`リンクをクリックします。
すると、ブラウザの上部にメッセージが出てきます。


![スクリーンショット 2017-12-08 11.24.52.png](https://qiita-image-store.s3.amazonaws.com/0/29971/980b6083-de0c-e65b-a738-de4ec3990b3d.png "スクリーンショット 2017-12-08 11.24.52.png")

「追加」ボタンをクリックすると名前をつけるためのダイアログが表示され、OKをクリックするとChromeのアプリ欄に追加されます。（`chrome://apps/`で確認可）
![スクリーンショット 2017-12-08 11.25.17.png](https://qiita-image-store.s3.amazonaws.com/0/29971/fa3ee164-5516-761a-e803-3516d854438f.png "スクリーンショット 2017-12-08 11.25.17.png")
![スクリーンショット 2017-12-08 11.25.53.png](https://qiita-image-store.s3.amazonaws.com/0/29971/8fe9f74d-ed62-0e69-688e-3c3572a18640.png "スクリーンショット 2017-12-08 11.25.53.png")

マニフェストの詳細に関しては[こちら](https://developers.google.com/web/fundamentals/web-app-manifest/?hl=ja)を見ると良いです。


# 4. パフォーマンスの計測
PWAを作るときに役立つ「Audit」という機能がChromeの開発者ツールに含まれています。
Auditはパフォーマンスやベストプラクティスを診断してくれるスグレモノで、結果を見ながらアプリケーションの品質を高めていくことができます。
`Application > Audit`タブを開くと「Perform an audit...」ボタンがあるので、クリックしてみます。

診断対象には下記が含まれます。
- Progressive Web App
- Performance
- Accessibility
- Best Practice

結果はこんな感じになりました。
![スクリーンショット 2017-12-08 11.37.47.png](https://qiita-image-store.s3.amazonaws.com/0/29971/298f6a5c-2359-3506-a954-6b7ebc89ff52.png "スクリーンショット 2017-12-08 11.37.47.png")

ほぼ何もしてないので良いスコアが出てます。
診断対象ごとに詳細な情報も表示されるので、フィードバックに従って修正していけばアプリケーションのクオリティを上げていくことができます。


# まとめ
本エントリでは
- PWAでできること
- PWAの開発環境の導入
- SPAのPWA化
- パフォーマンスの計測
といった、開発するにあたって必要なことを一通り説明しました。
Vue.jsのアドベントカレンダーですがあまりVue.js要素が無くてすみません。
Vue.jsで始めよう！PWA！