html5 ionic

ionic3(SPA)のServiceWorkerライブラリをsw-toolboxからWorkboxに切り替える

投稿日:

昨今は閲覧速度向上やオフラインでも使える利便性が求められ、Webアプリ開発とPWAはセットで考えられるようになってきましたね。

PWAは「ServiceWorker API」を使ってキャッシュする資源をコーディングしていきますが、ServiceWorkerのキャッシュは強力で、実際に開発してみるとコード変更が反映されない状況に陥ったりしてやきもきしたりします。

私も学習当初そうでしたがこの原因は「単純にServiceWorkerのことをよく理解していないだけ」だったりするので、そんな時は以下のPeteさんのハンズオンをやってみると底辺から理解が深まってスッキリすると思います。

とはいえお仕事でキャッシュの為のコーディングを毎回しているのは時間が勿体ないです。

ライブラリを使って楽をしたいところですが、既存のionic3プロジェクトに入っているServiceWorkerライブラリは「sw-toolbox」という今は推奨されていないものだったりします。

2019年現在はこちらの「Workbox」が主流です。

Workbox(とworkbox-cli)を使うと、予めキャッシュしておく資源(precache)、こういうケースはキャッシュから取り、こういうケースはネットワークから取るのような戦略(storategy)を、ほぼコーディング無しで設定出来るようになります。

ユーザエクスペリエンスが高まるServiceWorkerを少ない労力で導入できる為、ionic3(Cordova無しのSPA)をまだ使っているプロジェクトは勿論、非SPAなWebサイトでもWorkboxは導入検討の価値が有ります。

今回は「ionic3(SPA)でのServiceWorkerライブラリ移行」ケースにフォーカスして検証していきたいと思います。

Workboxへの切り替え手順

前回、History APIを使って戻るボタン対応したionic3プロジェクトでやってみます。

sw-toolkitをアンインストール

package.jsonを見ると3.6.0が入っていました。もう使わないのでnpm uninstallしておきます。

    "dependencies": {

        (snip)

        "sw-toolbox": "3.6.0",
C:\src\ionic\withHistoryAPI> npm uninstall sw-toolbox

service-worker.jsを編集してWorkboxを使うようにする

まだsw-toolbox用のコードが書かれている/src/service-worker.jsを、Workbox公式に従って変更します。

service-worker.js自体はindex.htmlからロードする記述があるのでコメントアウトされている場合は有効化するか、workboxで紹介されているコードでロードしておきます。

    <!-- ionic3のServiceWorker有効化テンプレート -->
    <!-- un-comment this code to enable service worker
   <script>
     if ('serviceWorker' in navigator) {
       navigator.serviceWorker.register('service-worker.js')
         .then(() => console.log('service worker installed'))
         .catch(err => console.error('Error', err));
     }
   </script>-->

   <script>
     if ('serviceWorker' in navigator) {
       window.addEventListener('load', () => {
         navigator.serviceWorker.register('/service-worker.js');
       });
     }
</script>

service-worker.jsを以下の内容で書き換えてみます。

// service-worker.js
console.log('Hello from service-worker.js');

importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');

if (workbox) {
    console.log(`Yay! Workbox is loaded`);
} else {
    console.log(`Boo! Workbox didn't load`);
    
workbox.routing.registerRoute(
    new RegExp('.*\.js'),
    workbox.strategies.networkFirst()
);

で、このファイルにキャッシュしたい資源や戦略を設定していく訳ですが・・・正規表現が使えるとはいえ、Webサイトの資源が多いとめんどいですね。

このServiceWorker定義を自動生成する「workbox-cli」がGoogle公式から公開されているのでそれを使ってもっと楽をします。

ServiceWorker定義をworkbox-cliで自動生成する

workbox-cliを入れてservice-worker.jsを自動生成していきます。

workbox-cliインストール

公式では-gでグローバルインストールしていますが、プロジェクト毎の開発時依存でも十分です。

C:\src\ionic\withHistoryAPI> npm install workbox-cli --save-dev

    (snip)

+ workbox-cli@4.3.1
added 193 packages from 125 contributors and audited 10775 packages in 61.917s
found 2 low severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details

C:\src\ionic\withHistoryAPI>workbox --version
4.3.1

workboxコマンドが使えるようになりました。

workbox-cliの設定

プロジェクトルートにworkbox.config.jsを作成して以下の内容を記述。

module.exports = {
    // キャッシュするファイル群のルートディレクトリ
    globDirectory: "www/",
    // キャッシュ対象ファイルの拡張子
    globPatterns: ["**/*.{html,png,svg,jpg,json,pem,js,ttf,woff,woff2,eot}"],
    // 除外(後で個別設定する為)
    globIgnores: ["**/main.js", "**/main.css", "**/vendor.js"],
    // ionicons対応(リクエスト時のURLパラメータを除外)
    ignoreURLParametersMatching: [/^v$/],
    // service-worker.js出力先
    swDest: "src/service-worker.js",

    // 個別に戦略(StaleWhileRevalidate)を設定するファイル
    runtimeCaching: [
        {
            urlPattern: "/build/main.js",
            handler: "StaleWhileRevalidate",
            options: {
                cacheableResponse: {
                    statuses: [0, 200]
                }
            }
        },
        {
            urlPattern: "/build/main.css",
            handler: "StaleWhileRevalidate",
            options: {
                cacheableResponse: {
                    statuses: [0, 200]
                }
            }
        },
        {
            urlPattern: "/build/vendor.js",
            handler: "StaleWhileRevalidate",
            options: {
                cacheableResponse: {
                    statuses: [0, 200]
                }
            }
        }
    ]
};

ionic3のwebpack結果は/wwwに吐かれるので配下資源をprecache対象にします。その中でもmain.js、main.css、vendor.jsはよく変更が入る為、「キャッシュ優先だけど同時にネットワークにも取りに行く」StaleWhileRevalidate戦略を設定しておきます。

(実際にはprecacheされた資源もサーバ資源が更新されれば更新されます。個別にStaleWhileRevalidateする必要はないかも知れません)

キャッシュ戦略の種類

キャッシュ戦略については以下のWorkboxページが図解付きで分かり易いです。

  • Cache Only : キャッシュからしか取らない
  • Cache First : キャッシュになかったらネットワークから取る
  • Network Only : ネットワークからしか取らない
  • Network First : ネットワークが繋がらなければキャッシュから取る
  • Stale While Revalidate : キャッシュから取るが、同時にネットワークにも取りに行き、更新されていればキャッシュに入れる。次回は最新がキャッシュから表示される。

service-worker.jsを自動生成してみる

workbox generateSW workbox.config.js

C:\src\ionic\withHistoryAPI> workbox generateSW workbox.config.js
Using configuration from C:\src\ionic\withHistoryAPI\workbox.config.js.
The service worker was written to src/service-worker.js
25 files will be precached, totalling 2.4 MB.

生成されたservice-worker.js。先ほど行ったworkbox.config.jsの設定どおり、

  • main.js、main.css、vendor.jsはStaleWhileRevalidate戦略
  • それ以外はprecache

するコーディングが成されたServiceWorker定義が出来上がりました。

/**
 * Welcome to your Workbox-powered service worker!
 *
 * You'll need to register this file in your web app and you should
 * disable HTTP caching for this file too.
 * See https://goo.gl/nhQhGp
 *
 * The rest of the code is auto-generated. Please don't update this file
 * directly; instead, make changes to your Workbox build configuration
 * and re-run your build process.
 * See https://goo.gl/2aRDsh
 */

importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

/**
 * The workboxSW.precacheAndRoute() method efficiently caches and responds to
 * requests for URLs in the manifest.
 * See https://goo.gl/S9QRab
 */
self.__precacheManifest = [
  {
    "url": "assets/fonts/ionicons.eot",
    "revision": "bdf1d30681cf87986c385eea78e8de9a"
  },
  {
    "url": "assets/fonts/ionicons.svg",
    "revision": "d9496a234c81179afbca6bf5959cc30a"
  },
  {
    "url": "assets/fonts/ionicons.ttf",
    "revision": "74c652671225d6ded874a648502e5f0a"
  },
  {
    "url": "assets/fonts/ionicons.woff",
    "revision": "81414686e99c00d2921e03dd53c0ab04"
  },
  {
    "url": "assets/fonts/ionicons.woff2",
    "revision": "311d81961c5880647fec7eaca1221b2a"
  },
  {
    "url": "assets/fonts/noto-sans-bold.ttf",
    "revision": "a165a42685795361b25593effb32fdb1"
  },
  {
    "url": "assets/fonts/noto-sans-bold.woff",
    "revision": "ce3d777f2c41cca1494021cfa3fcd72c"
  },
  {
    "url": "assets/fonts/noto-sans-regular.ttf",
    "revision": "2fd9c16b805724d590c0cff96da070a4"
  },
  {
    "url": "assets/fonts/noto-sans-regular.woff",
    "revision": "ce8ba1a4ff970db896192c41fc3c96b1"
  },
  {
    "url": "assets/fonts/roboto-bold.ttf",
    "revision": "1f4fd7e4df65487f07ba9148f7ca095d"
  },
  {
    "url": "assets/fonts/roboto-bold.woff",
    "revision": "43183beef21370d8a4b0d64152287eba"
  },
  {
    "url": "assets/fonts/roboto-bold.woff2",
    "revision": "28d80f43ae4cc35f19e1f1a6ab670f25"
  },
  {
    "url": "assets/fonts/roboto-light.ttf",
    "revision": "9ff15bd34ea83e4dd3f23c20c7f5090e"
  },
  {
    "url": "assets/fonts/roboto-light.woff",
    "revision": "7e2d32e7141050d758a38b4ec96390c0"
  },
  {
    "url": "assets/fonts/roboto-light.woff2",
    "revision": "a826ff848e9f52b1732fed7d154afa97"
  },
  {
    "url": "assets/fonts/roboto-medium.ttf",
    "revision": "a937e2cae14e68262a45aa91204c2fdf"
  },
  {
    "url": "assets/fonts/roboto-medium.woff",
    "revision": "0f3b7101a8adc1afe1fbe89775553c32"
  },
  {
    "url": "assets/fonts/roboto-medium.woff2",
    "revision": "b2c9c262e089411e20689ed393c00796"
  },
  {
    "url": "assets/fonts/roboto-regular.ttf",
    "revision": "07f8fb6acbabeb10d3fad9ab02d65e0b"
  },
  {
    "url": "assets/fonts/roboto-regular.woff",
    "revision": "f94d5e5102359961c44a1da1b58d37c9"
  },
  {
    "url": "assets/fonts/roboto-regular.woff2",
    "revision": "e6b9d54811307f98da62eae992ae05ba"
  },
  {
    "url": "assets/imgs/logo.png",
    "revision": "ff5d7c95e2809b0dd177a9768ba3b619"
  },
  {
    "url": "build/polyfills.js",
    "revision": "443c697fc904cd88a651d09cf5c2fe2b"
  },
  {
    "url": "index.html",
    "revision": "5b0901552c2613b21c23cd069ce0affc"
  },
  {
    "url": "manifest.json",
    "revision": "e46a62111076747fd325b7b2bce78d92"
  }
].concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest, {
  "ignoreURLParametersMatching": [/^v$/]
});

workbox.routing.registerRoute("build/main.js", new workbox.strategies.StaleWhileRevalidate({ plugins: [new workbox.cacheableResponse.Plugin({ statuses: [ 0, 200 ] })] }), 'GET');
workbox.routing.registerRoute("build/main.css", new workbox.strategies.StaleWhileRevalidate({ plugins: [new workbox.cacheableResponse.Plugin({ statuses: [ 0, 200 ] })] }), 'GET');
workbox.routing.registerRoute("build/vendor.js", new workbox.strategies.StaleWhileRevalidate({ plugins: [new workbox.cacheableResponse.Plugin({ statuses: [ 0, 200 ] })] }), 'GET');

冒頭のPeteさんのハンズオンではキャッシュする資源だけでなく、キャッシュするロジックも全部自分で書きましたが、Workboxを使うとノーコーディングです。楽ですね。

ブラウザにprecacheされた資源も、「revision」(ファイル内容のハッシュ値)が変わるとブラウザにダウンロードされ、更新されます。

service-worker.js更新をビルドサイクルに組み込む

プログラムを編集したらその変更がクライアントのブラウザに検知されるようにしておく必要があります。

リビジョンの更新によってそれが成されるので、ionic buildした際に自動でworkboxコマンドが走るようにしておきます。

webpackのworkboxプラグインを使うか、package.jsonでビルドスクリプトに組み込みます。今回は手間の要らない後者で行きます。

    "scripts": {
        "start": "ionic-app-scripts serve",
        "clean": "ionic-app-scripts clean",
        "build": "ionic-app-scripts build",
        "lint": "ionic-app-scripts lint",
        // ↓ 追加
        "ionic:build:after": "workbox generateSW workbox.config.js && cp src/service-worker.js www"
    },

ionic buildした後、wwwの資源を対象にsrc/service-worker.jsを作り、wwwにコピーするようにしました。

動作確認

ionic serveコマンドなりhttp-serverコマンドでHTTPサーバを起動してChrome(ver.78)でアクセスしてみます。

C:\src\ionic\withHistoryAPI> ionic serve

http://localhost:8100

F12でデベロッパーツールを表示。ServiceWorkerが認識されています。

続いてprecacheの確認。service-worker.jsで定義したリソースがキャッシュされています。precacheされたファイルはリビジョンが変わらない限りずっとブラウザキャッシュから表示します。ページ表示の高速化、ネットワーク節約、オフライン表示OKが期待できますね。

この時点ではまだStaleWhileRevalidate戦略でキャッシュするmain.js、main.css、vendor.jsは見えません。一度F5して再描画すると。ランタイムキャッシュに出てきます。この3ファイルはキャッシュ表示と同時にネットワークにもフェッチしにいきます。

Workboxによって適切にサーバ資源がブラウザにキャッシュされていることが分かりました。

プログラムを編集したらionic buildし直せばキャッシュ資源も更新されます。

留意点

Chromeの場合、サーバ側でservice-worker.jsが更新されていても、同じURLを開いている古いタブがある(古いServiceWorkerが動いている)とブラウザに更新されません。

再描画しても古いキャッシュが表示されてしまうときは、古いタブが残っていないか確認してみてください。

まとめ

  • Workboxの導入は簡単。ionic3だけでなく静的なWebでも有効。
  • キャッシュ戦略を知っておき、precacheが使えれば大体問題無し。

-html5, ionic
-,

執筆者:

関連記事

ionic-cli 5からcordovaを使ったハイブリッドアプリプロジェクトの作成方法が変わった

久しぶりにハイブリッドアプリを作ろう、ついでにionicとcordovaも最新にしよう。と作業を開始したら今までの手順と違って若干困惑した話です。 プロジェクト新規作成時にcordovaを使うか聞かれ …

IndexedDBにblob保存されたPDFファイルを外部アプリに頼らずにJavascriptで表示(後編)

目次1 はじめに2 ビューワ機能を提供してくれるnpmモジュールを選別する3 実装開始3.1 ng2-pdfjs-viewerを使えるようにする3.2 PDFビューワコンポーネントを作る3.3 Hom …

IndexedDBにストアしたオブジェクトのキー値を部分的に更新する

目次1 はじめに2 課題3 より良い方法4 まとめ5 補足 はじめに IndexedDBは「key : value」でレコードを保存するキーバリューストアです。 バリューには「単値」または「Javas …

Javascriptでコンパスを作ってAndroid、iPhoneが向いている方角を特定

普段は意識しなくても、AndroidもiPhoneも自然データをデジタルに変換するセンサーの塊です。 加速度センサー重力センサージャイロセンサー地磁気センサー気圧センサー照度センサー温度センサー位置セ …

History APIを使ってIonic(3以前のSPA)でブラウザの戻るボタンやAndroidバックキーを押すと前サイトに戻ってしまう件に対応する

Ionic2や3ではまだAngular Routerを採用していなかったので、ページ遷移をしてもブラウザ履歴が積まれず、Androidのバックキーやブラウザの戻るボタンを押すとサイトに入ってくる前のペ …

 

shingo.nakanishi
 

東京在勤、1977年生まれ、IT職歴2n年、生涯技術者として楽しく生きることを目指しています。デスマに負けず健康第一。