Angular Service Workerを導入する


## はじめに

この記事は [Angular Advent Calendar 2017](https://qiita.com/advent-calendar/2017/angular) 17日目の記事です。

この記事では、Angular v5でリリースされた@angular/service-workerを、既存のAngularプロジェクトに導入していきます。

## Angularプロジェクト

https://github.com/Ismaestro/angular5-example-app

（´-`）.｡oO(自分で用意する時間がなかったので、このいい感じのサンプルアプリを使って進めていきます)

Service Workerを登録する前の状態で、Lighthouseで解析しておきましょう。

```
$ npm i
$ ng build --prod
$ http-server dist
```

![スクリーンショット 2017-12-18 00.58.17.png](https://qiita-image-store.s3.amazonaws.com/0/19111/fe35c4db-8b79-579d-6cae-03262fbbdd6d.png "スクリーンショット 2017-12-18 00.58.17.png")
![スクリーンショット 2017-12-18 01.05.30.png](https://qiita-image-store.s3.amazonaws.com/0/19111/35b04fc4-6ebe-ef4e-a10d-73631ff1c6dc.png "スクリーンショット 2017-12-18 01.05.30.png")


## Angular Service Workerの導入

日本語の記事だと、[laco](https://qiita.com/lacolaco)さんの [Angular CLI 1.5によるAngular Service Workerクイックスタート – lacolaco-blog – Medium](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)がとても素晴らしく、この記事でも大いに参考にしています。

```
$ npm i @angular/service-worker
$ ng set apps.0.serviceWorker=true
```

これで .angular-cli.json に "serviceWorker": true の設定が書き足されます。

次に、Angular Service Workerは次の2つのファイルを使用します。

* ngsw-worker.js: Angular Service Workerの本体
* ngsw.json: ngsw-worker.jsが使う設定ファイル


ngsw-worker.jsファイルは、node_modulesの中からコピーします。Angular CLIのassets機能を使うことで、ビルドのたびに `node_modules/@angular/service-worker/ngsw-worker.js` ファイルが、出力先のルートディレクトリにコピーされます。

```.angular-cli.json
  "assets": [
    "assets",
    "favicon.ico",
    "sitemap.xml",
    "googled41787c6aae2151b.html",
    "CNAME",
    {
      "input": "../node_modules/@angular/service-worker",
      "glob": "ngsw-worker.js",
      "output": "."
    }
  ],
```

ngsw.jsonファイルはAngular Service Workerが提供する ngsw-config コマンドを使って生成します。設定元となるファイル src/ngsw-config.json を新たに作成します。

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

package.jsonに新しいビルドスクリプトを登録します。

```package.json
  "scripts": {
    "build:ngsw": "ng build -prod && node_modules/.bin/ngsw-config dist src/ngsw-config.json"
  },
```

最後に、ビルドに含まれるようになったngsw-worker.jsファイルをService Workerとして登録するためにServiceWorkerModuleをアプリケーションに導入します。app.module.tsファイルを開き、次のように編集します。

```app.module.ts
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
// Import NGSW module
import {ServiceWorkerModule} from '@angular/service-worker';

import {APP_CONFIG, AppConfig} from './config/app.config';

import {AppRoutingModule} from './app-routing.module';
import {SharedModule} from './shared/modules/shared.module';
import {CoreModule} from './core/core.module';

import {AppComponent} from './app.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from '@angular/common/http';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
import {HttpLoaderFactory} from './app.translate.factory';
import {HeroTopComponent} from './heroes/hero-top/hero-top.component';
import {ProgressBarService} from './core/progress-bar.service';
import {ProgressInterceptor} from './shared/interceptors/progress.interceptor';
import {TimingInterceptor} from './shared/interceptors/timing.interceptor';

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    HttpClientModule,
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [HttpClient]
      }
    }),
    SharedModule.forRoot(),
    CoreModule,
    AppRoutingModule,
    // Register ngsw-worker.js as SW
    ServiceWorkerModule.register('/ngsw-worker.js'),
  ],
  declarations: [
    AppComponent,
    HeroTopComponent
  ],
  providers: [
    {provide: APP_CONFIG, useValue: AppConfig},
    {provide: HTTP_INTERCEPTORS, useClass: ProgressInterceptor, multi: true, deps: [ProgressBarService]},
    {provide: HTTP_INTERCEPTORS, useClass: TimingInterceptor, multi: true}
  ],
  bootstrap: [AppComponent]
})

export class AppModule {
}
```

それではこの状態でアプリケーションを実行してみましょう。

```
$ npm run build:ngsw
$ http-server dist
```

![スクリーンショット 2017-12-18 01.34.15.png](https://qiita-image-store.s3.amazonaws.com/0/19111/658cc194-14d2-9464-71f5-9fd338de799f.png "スクリーンショット 2017-12-18 01.34.15.png")


![スクリーンショット 2017-12-18 01.34.38.png](https://qiita-image-store.s3.amazonaws.com/0/19111/f92c0414-72c2-7355-64d5-545b5993fe42.png "スクリーンショット 2017-12-18 01.34.38.png")


DevToolsやLighthouseで確認すると、Service Workerが登録されていることがわかると思います。
またngsw-config.jsonで記述したキャッシュ設定により、外部APIからデータを取得する箇所以外のオフライン対応も完了しています。実際に試してみましょう。

![スクリーンショット 2017-12-18 01.57.42.png](https://qiita-image-store.s3.amazonaws.com/0/19111/026d21b4-33f6-b37e-e83e-c09db56012df.png "スクリーンショット 2017-12-18 01.57.42.png")

![スクリーンショット 2017-12-18 01.57.55.png](https://qiita-image-store.s3.amazonaws.com/0/19111/375502ca-16f9-53e5-8447-b27f9aa9a7c9.png "スクリーンショット 2017-12-18 01.57.55.png")

「Offline」にチェックしてからリロード・ページ遷移を行うと、外側のApplication Shellは維持されていますが、Heroの情報は表示されずローディング状態が続きます。

オフラインアクセスに備えて、Heroの情報もキャッシュに入れておきましょう。


## Runtime caching

このために、先ほど使ったassetGroupsとは別のdataGroupsを使用します。

* assetGroups
	* app [shell]のバージョンを追跡していて、グループのうち1つ以上のリソースが更新された場合、利用可能な新しいバージョンのアプリがあるとみなし、対応する更新フローを開始する
* dataGroups
	* appのバージョンとは独立していて、独自のキャッシュポリシーを使ってキャッシュを行う。APIレスポンスを処理するのに適切


ngsw-config.jsonにdataGroupsの設定を加えて、以下のように編集します。

```ngsw-config.json
{
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html"
        ],
        "versionedFiles": [
          "/*.bundle.css",
          "/*.bundle.js",
          "/*.chunk.js"
        ]
      }
    }
  ],
  "dataGroups": [
    {
      "name": "api-freshness",
      "urls": [
        "/",
        "/heroes"
      ],
      "cacheConfig": {
        "strategy": "freshness",
        "maxSize": 100,
        "maxAge": "3d",
        "timeout": "10s"
      }
    }
  ]
}
```

Service Workerとキャッシュをすべて破棄した状態で、新たにビルドしたアプリケーションを実行してみましょう。
![Untitled.gif](https://qiita-image-store.s3.amazonaws.com/0/19111/99b54b0b-f8c6-4db7-70b7-56f3486166b7.gif "Untitled.gif")


オフラインになった状態でも、Heroの情報がキャッシュから無事表示されるようになりました！

## 参考にしたもの
* [Angular CLI 1.5によるAngular Service Workerクイックスタート – lacolaco-blog – Medium](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)
* [Automatic Progressive Web Apps using the Angular Mobile Toolkit by Maxim Salnikov](http://slides.com/webmax/pwa-ng-conf/)
* [A new Angular Service Worker — creating automatic progressive web apps. Part 1: theory](https://medium.com/progressive-web-apps/a-new-angular-service-worker-creating-automatic-progressive-web-apps-part-1-theory-37d7d7647cc7)
* [A new Angular Service Worker — creating automatic progressive web apps. Part 2: practice](https://medium.com/@webmaxru/a-new-angular-service-worker-creating-automatic-progressive-web-apps-part-2-practice-3221471269a1)
* https://angular.io/guide/service-worker-intro


## おわりに

ngsw-config.jsonの設定に関しては、[公式ドキュメント](https://angular.io/guide/service-worker-configref)に詳しく載っているのでそちらも読んでみてください。