3分クッキング ServiceWorkerで阿部寛さんを超える

(この記事は3分で読めます)

ServiceWorkerは、ブラウザネットワークのプロキシであり、ブラウザの通信をインターセプトすることが主な目的です。

ServiceWorkerは、Push通知のためだけではありません。昨年秋に日経新聞[^0]やdev.toで話題になったServiceWorkerは、**リソースキャッシュ**の用途です。

既存のAngularアプリケーションにこのリソースキャッシュを導入します。

# TL;DL

- ServiceWorkerのリソースキャッシュにフォーカスする。
- 既存のAngularアプリケーションを4ステップで調理する。
- 阿部寛さんのホームページ[^1]と実食勝負する。
- 味の秘訣を解説する

# レシピ

* Angular-CLIで作成されたプロジェクト ×1
* Angular`5.0.0~` ×1
* Angular-CLI`1.6.0~`[^2]　×1
* chrome`49〜` ×1

# (~00:30)

ServiceWorkerをインストールします。

```bash
$ yarn add @angular/service-worker
$ ng set apps.0.serviceWorker=true
```

# (~01:30)

`src/`直下に`ngsw-config.json`の名前で以下のファイルを作ります。コピペすればオーケーです。

```json
{
  "index": "/index.html",
  "assetGroups": [{
    "name": "app",
    "installMode": "prefetch",
    "resources": {
      "files": [
        "/favicon.ico",
        "/index.html"
      ],
      "versionedFiles": [
        "/*.bundle.css",
        "/*.bundle.js",
        "/*.chunk.js"
      ]
    }
  }, {
    "name": "assets",
    "installMode": "lazy",
    "updateMode": "prefetch",
    "resources": {
      "files": [
        "/assets/**"
      ]
    }
  }]
}
```

# (~02:00)

`app.module.ts`に以下を追加します。プロダクション環境だけの起動設定です。

```ts:app.module.ts
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from 'environments/environment';

@NgModule({
  imports: [
    ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production}),
  ],
```

# (~03:00)

ビルドします。

```bash
ng build --prod
```

これで出来上がりです！

# 実食

さて、実食です。リアルワールドの体験を重視し、chromeブラウザで通常通りに立ち上げて、食します。speed index[^3]ではなく、loadイベント発火を指標にします。

アプリを起動しましょう。

<img width="713" alt="スクリーンショット 2018-01-07 20.32.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/194428/f54e31b4-b0c7-8e74-6c39-e7bbd81fa4f2.png">

あれ、作ったばかりなのにもう冷めてますね。あなたのアプリケーションはもうServiceWorkerで動いています。キャッシュが温まっているので、そちらで召し上がりましょう。リロードします。


<img width="713" alt="スクリーンショット 2018-01-07 20.33.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/194428/e098042b-241c-3dbd-e794-ffc1a1846355.png">

絶妙です！

> ああ なんてことだ 食べ始めているのに さらに腹がへっていくかのようだ


口直しのために、ネットワークをオフラインにして、再度立ち上げてみましょう。

<img width="713" alt="スクリーンショット 2018-01-07 20.33.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/194428/6bbec58b-f72f-1468-ac19-6bd3ebc2aeb8.png">

> うおォン、俺はまるで人間火力発電所だ


さて、対戦相手の阿部寛さんのホームページも食しましょう。

<img width="713" alt="スクリーンショット 2018-01-07 17.41.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/194428/3e4b831f-e572-77ba-e29a-e5db4f49f35b.png">

Oh...熟成された味の凄みですね。参りました:bow:

# 種明かし

さて、なぜ、こんなに簡単にリソースキャッシュができるのかという点を解説しておきます。

一言で言えば、 **CLIビルドのハッシュ値管理にServiceWorkerのリソースキャッシュを統合している**からということになります。

Angular-CLIのビルドでは、JS/CSSらだけでなく、そのイニシエーターとなる`index.html`からワンパックしていることはご存知だと思います。`index.html`ではbundle.jsやstyle.cssらのハッシュ値込みのファイル名を織り込んでいます。

prodビルドの成果物例
<img width="311" alt="スクリーンショット 2018-01-07 21.01.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/194428/2928a645-8f1d-3fb5-4938-902e4a080dcd.png">

AngularのServieWorkerでは、このビルド成果物を丸ごと一つのバージョンとして取り扱っています。CLIビルドの成果物であるハッシュ付きのファイル名に、対応するハッシュテーブルを作成します。そのため、ServiceWorkeの内部管理では、ファイル名にハッシュが振られていないhtmlやfavicon.icoもハッシュ値を持っています。

```json:ngsw.json
  "hashTable": {
    "/0.7075f57397a56dxxxxxx.chunk.js": "3fd21bc081da85b2c45ace5d2ebbae8a25d3d0ff",
    "/inline.1b416a2e995969xxxxxx.bundle.js": "e2c614d5436663cc3ac31daa62da2199a5c5d4c9",
    "/main.b5d767c695c55exxxxxx.bundle.js": "76722272d07dfe9a2f1660bcb2276903145cb285",
    "/polyfills.e72c343776e23cxxxxxx.bundle.js": "9982da2c3d26503834c98a77966b30e9770b5ff7",
    "/styles.b2aa3d2df2c64exxxxxx.bundle.css": "89ad19ffab4015aa9f037c990a8c3cd3ef9a0d6a",
    "/favicon.ico": "f104e54df31b07f35ce4cb16ce1855f1d81c47cb",
    "/index.html": "0d41a7001a0a32de8baa3e21088d363b89183e71",
    "/manifest.json": "7f4fb3ea794dd79b6a3b9d916222a83da2286048",
    "/assets/images/top.jpg": "8b00026f1fd228b3e21b6c61d5a59d2442aeddcf"
  }
```

この情報を元にして、端末側のServiceWorkerはバージョン管理を行なっています。

<img width="701" alt="スクリーンショット 2018-01-07 21.57.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/194428/bf421406-24a2-d2e6-a214-5df141b260db.png">

このバージョンが変わると、@angular/service-workerが提供している`ngsw-worker.js`が、アップデートサイクル[^4]を始めます。このアップデートはバックグラウンドで行われ、ブラウザのタブではversion1が実行され続けています。

つまり、ServiceWorkerのキャッシュには、複数バージョンが並存しているということです。[^5]

<img width="777" alt="スクリーンショット 2018-01-07 22.15.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/194428/2cc78622-bf6f-a514-9b06-3162acc64978.png">

この2つのServiceWorkerCacheとは、**`ng build`の成果物とイコール**です。distフォルダにあるものがそのまま一つのバージョンとしてServiceWorkerのキャッシュに載ります。（ng build成果物以外のキャッシュについては別記事[^6]でまとめました。） 単体のリソースファイルだけを更新されると、ある通信APIのインターフェースが変わってるのにコンポーネントファイルは更新されずクライアントでクラッシュするというような事態が起こりますが、アプリバージョンとしてセットで管理されているので安心です。

以上の仕組みになるので、フレームワークのユーザーであるアプリケーション開発者にとっては、一度上記の4ステップを行うと、以降は**`ng build`を実行するだけでリソースファイルのキャッシュを実現できる**わけです。後は、ngsw-worker.jsがキャッシングはもちろん、**遅延ロード**や**異常状態**などなどもろもろを織り込んで振る舞います。

これは、Angular-CLIのビルドが、htmlファイルまでも対象にしてるからこそ、できる芸当だなあと思います。

# @angular/service-workerはデフォルトで入れていいのではないか

上記のように、AngularのServiceWorkerは、CLIのビルドを流用して、リソースファイルのキャッシュを行なっている仕組みになっています。もちろん、ServiceWorkerModuleの中身を見ると、ServiceWorkerの環境チェックを行い、ブラウザの`navigator`に`serviceWorker`が無ければ何もしません。そのため、AngularのServiceWorkerは、**とりあえず入れておけばいいのではないか**と思います。 

ただし、リソースのバージョン整合性が崩れた後に、クライアント端末を復帰させるためには強制更新などが必要になるかもしれないことには注意です。

- デプロイを段階的に行なっていたり、途中でコケても、端末でServiceWorkerが動いているので、勝手に端末から中途半端なリソースを取得しに来てしまいます。
- 通信が切れても復帰の仕組みがありますが、特にモバイル端末では何が起るかわかりません。
- オリジンサーバーと端末の間に、リバースプロキシなどでキャッシュレイヤーを作っているとキャッシュが崩れる可能性が出ます。
- 他サービス（例えばFirebase）などのServiceWorkerも併用しようとすると上手く動きません。[^6]

ServiceWorkerは、端末側の話になるためサポートも大変です。それらのコストが負荷になるサービスでは慎重になった方がいいかもしれません。

とはいえ、端末の整合性が崩れたり、キャッシュの内容に異変を検知すると、AngularServiceWorkerは、セーフモード移行やフェールセーフなどの防衛機構を搭載しています。仕組みからして手っ取り早い復旧方法は、サーバ側の`ngsw.json`を更新することです。すなわちハッシュ値を変えた`ngsw-config.json`をビルドすることです。また、`ngsw.json`をサーバから削除すれば、端末のServiceWorkerを順次止めることが可能です。詳細は、Angular公式ドキュメント（現在翻訳中の日本語版）の[プロダクションにおけるServiceWorker](https://angular.jp/guide/service-worker-devops)を参照ください。

# 更なるServiceWorker

ServiceWorkerのユースケースは、push通知だけでなく、リソースファイルのキャッシュだけでなく、まだまだ考えられます。
MDNでは以下のユースケースが挙げられています。[^7]

ServiceWorkerの想定ケース

* バックグラウンド同期
* 他のoriginからのリソース要求に対する応答
* 地図計算・ジャイロ計算のように計算する事が高コストなアップデートを集中的に受信すること。またそのように複数ページを１組のデータとして扱える事
* 開発目的のためcoffescript、less、CJS/AMDモジュールなどのクライアントサイドコンパイルや依存解決
* バックグラウンドサービス（XHRなど）のフック
* URLパターンに基づくテンプレートカスタマイズ
* 近い将来ユーザーが必要とするようなリソース（例：フォトアルバムアプリの新しい写真）のpre-fetching

ServiceWorkerをベースにしたAPI仕様

* **[Web Background Synchronization](https://wicg.github.io/BackgroundSync/spec/)** サイトにユーザーが存在しないときでもService workerを起動する事ができ、キャッシュをアップロードする。
* **[Push API](https://w3c.github.io/push-api/)** 新しいコンテンツが入手可能になった事をユーザーに対してメッセージとしてService workerから通知する。
* 特定の日付・時刻に対する反応（未策定[^8]）
* 特定のジオフェンスへ入った事を検知する（未策定[^8]）

MDNに無いケース

* PWAで挙げられるAppShell。ホームスクリーン追加もServiceWokerが必要です。

現状だと、通信のプロキシ用途にフォーカスしており、バックグラウンドでどこまで動かせてくれるのかよくわかりませんが、まずは手始めにUpdateサービス、Pushサービスと入り、ノウハウを貯めておけばいろいろ面白いことができそうです。なお、ServiceWorkerもv2のスペックづくりが進行中で、GitHubのISSUEトラッカー[^9]で状況を確認できます。1/7現在は51%完了の模様です。

以上です。３分で読めるはウソでしたし、阿部寛さんも超えられませんでしたが、３分で調理はできたので免じてください:bow:

# 他の参考資料

**[Angular公式ドキュメントの日本語版「ServiceWorker」](https://angular.jp/guide/service-worker-intro)**（注：~~現在翻訳中ですが、全てPullRequestは起こしているため順次反映されていきます。~~マージされました。）
**[Angular CLI 1.5によるAngular Service Workerクイックスタート](https://medium.com/lacolaco-blog/angular-cli-1-5%E3%81%AB%E3%82%88%E3%82%8Bangular-service-worker%E3%82%AF%E3%82%A4%E3%83%83%E3%82%AF%E3%82%B9%E3%82%BF%E3%83%BC%E3%83%88-bd90db99fe13)**（注:CLI1.6では更に簡単になっています。）
**[Angular Service Workerを導入する](https://qiita.com/massa142/items/339929ae56de88c8f451)**（注:CLI1.6では更に簡単になっています。）


[^0]: https://speakerdeck.com/sisidovski/nikkei-high-performance-pwa
[^1]: http://abehiroshi.la.coocan.jp/
[^2]: 最後の参考資料に挙げているように、一つ前のv1.5とも設定方法が変わります。どんどん簡単になっていってます。
[^3]: https://developers.google.com/web/tools/lighthouse/audits/speed-index
[^4]: ServiceWorkerそのもののアップデートサイクルとは厳密には異なります。リソースキャッシュの整合性を担保するために、Angularがさらにもう一層のアプリバージョン管理を行なっています。https://angular.jp/guide/service-worker-devops#%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3
[^5]: 新しいバージョンのインストールに成功すると、新しいブラウザタブで開かれた場合、最新バージョンのアプリケーションが提供されます。既存のバージョンはクライアントがゼロになれば破棄されます。https://angular.jp/guide/service-worker-devops#%E3%82%BF%E3%83%96%E9%96%93%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3
[^6]: AngularServiceWorkerの注意点 https://qiita.com/studioTeaTwo/items/74219721ca51f6ffeabd#angularserviceworker%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9
[^7]: https://developer.mozilla.org/ja/docs/Web/API/ServiceWorker_API
[^8]: MDN上では記述ありませんが、W3Cのスペック策定状況は確認できます。https://www.w3.org/2018/01/web-roadmaps/mobile/
[^9]: https://github.com/w3c/ServiceWorker/milestone/3