JavaScript
vue.js
ServiceWorker
PWA

はじめに

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

1. PWAとは

PWA(Progressive Web Application)とは、一言で言えば「ネイティブアプリのような使い勝手を実現したWebアプリ」です。
Googleのコードラボを一通りやると全体像がつかめるかと思います。

PWAでできること

ホームスクリーンへの追加(Add to homescreen)

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

詳細はこちら

キャッシュ(Service Worker Caching)

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

プッシュ通知(Push Notification)

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

バックグラウンド同期

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

PWAを作るための条件

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

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

Service Workerのブラウザ実装状況を見ると、まだまだと言った感じがします。
しかしながら、iOSのSafariでサポートされるという情報もあり、展望は開けていると個人的には考えています。

2. 環境整備

PWAでできることを大まかに理解したところで、開発できる環境を整えていきます。

2.1 vue-pwa-boilerplate

vue-pwa-boilerplateは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

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

2.2 Web Server for Chrome

vue-pwa-voilerplateでは、開発ビルド(npm run dev)ではService Workerが登録されません。
build/service-worker-dev.jsbuild/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 devnpm run startで起動するNode.jsの開発サーバではService Workerが動かないという点です。
Service Workerの挙動を確認するにあたってはWeb Server for Chromeを導入します。
dist/以下にビルドされた諸々のファイルが吐き出されるので、パスを指定して開きます。
スクリーンショット 2017-12-08 9.20.45.png

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

スクリーンショット 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が入っています。
webpackのプラグインなので、設定ファイル上で次のように記述されています。

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をキャッシュする設定が書かれています。

このまま使っても良いのですが、今回はworkbox-webpack-pluginを使っていきたいと思います。

npm uninstall sw-precache-webpack-plugin
npm install --save-dev workbox-webpack-plugin
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のライフサイクルに関わる部分(skipWaitingclientsClaim)に関しては本エントリでは省略しますが、押さえておくべきところなので別エントリで書こうと思います。

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

スクリーンショット 2017-12-08 10.26.18.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から呼び出すことを想定します。

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によると、キャッシュ戦略には次のようなものがあります。

  • 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

「追加」ボタンをクリックすると名前をつけるためのダイアログが表示され、OKをクリックするとChromeのアプリ欄に追加されます。(chrome://apps/で確認可)
スクリーンショット 2017-12-08 11.25.17.png
スクリーンショット 2017-12-08 11.25.53.png

マニフェストの詳細に関してはこちらを見ると良いです。

4. パフォーマンスの計測

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

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

結果はこんな感じになりました。
スクリーンショット 2017-12-08 11.37.47.png

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

まとめ

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