Next.js with Netlify でService Workerを使う

通常Next.jsでService Workerを使う場合は、`server.js`などを自前で作成します。
しかしNetlifyは静的なコンテンツ配信を行うサーバーのため上の手法は選択できません。逆に言えば静的なコンテンツとしてディレクトリに存在していれば実行できるようになります。そのための手法をここでは記載します。

|追記2018.8.1|
|:---|
|使用プラグインを変更しました<br>sw-precache-webpack-plugin -> workbox-webpack-plugin|

## パッケージのインストール
### workbox
webpackを通して動的にバンドルされたファイルをキャッシュしてくれる便利プラグインが存在します。
いくつかあるのですが、以下の記事を参考に今回は`workbox`を採用しました。

> Workbox で Service Woker のキャッシュを導入してみた話し、Webpack を添えて | Nagisaのすゝめ
> https://blog.nagisa-inc.jp/archives/1132

今回はworkboxをwebpackで使うため`workbox-webpack-plugin`というプラグインをインストールします。

```
$ yarn add workbox-webpack-plugin -D
```

## next.config.js
next.jsのwebpackをoverrideするため、`next.config.js`をルートに作成します。

```
$ touch next.config.js
```

`next.config.js`は以下のように書きます。
`runtimeCaching`に適宜追加する感じです。

```js:next.config.js
const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {
    webpack: config => {
        config.plugins.push(
            new WorkboxPlugin.GenerateSW({
                cacheId: 'workbox',
                swDest: 'service-worker.js',
                skipWaiting: true,
                clientsClaim: false,
                runtimeCaching: [
                    {
                        urlPattern: '/',
                        handler: 'networkFirst',
                        options: {
                            cacheName: 'page',
                            expiration: {
                                maxAgeSeconds: 60 * 60 * 24
                            }
                        }
                    },
                    {
                        urlPattern: /\/api\/.+/,
                        handler: 'networkFirst',
                        options: {
                            cacheName: 'api',
                            expiration: {
                                maxAgeSeconds: 60 * 60 * 24
                            }
                        }
                    },
                    {
                        urlPattern: /\.(png|svg|woff|ttf|eot)/,
                        handler: 'cacheFirst',
                        options: {
                            cacheName: 'assets',
                            expiration: {
                                maxAgeSeconds: 60 * 60 * 24 * 14
                            }
                        }
                    }
                ]
            })
        );

        return config;
    }
};
```

この状態で、`next build`を実行すると生成される`.next`ディレクトリの直下に`service-worker.js`と`precache-manifest.[hash].js`いうファイルが生成されていることがわかります。

## package.json
Netlifyで使用するのは`next export`コマンドを通して生成された`out`ディレクトリが該当します。
よって`next build`によって生成される`.next`ディレクトリ内は合致しません。
そこで`service-worker.js`と`precache-manifest.[hash].js`を`out`ディレクトリ内にコピーするタスクを`package.json`に記載します。そしてこのコピーするタスクを`next export`時に実行するようにします。

具体的には以下のようになります。

```json:package.json
"scripts": {
    "storybook": "start-storybook -s ./ -p 6006",
    "dev": "next",
    "build": "next build",
    "start": "next start",
    "export": "npm run build && next export && npm run copySW",
    "copySW": "cp .next/service-worker.js out/service-worker.js && cp .next/precache* out/"
},
```

こうすることでNetlifyで使用する`out`ディレクトリに`service-worker.js`たちがコピーされ、使える状態になります。

## service-worker.jsの実行
コピーされた`service-worker.js`がコンポーネントで実行されるようにしましょう。
コンポーネントがマウントされたタイミングが望ましいため、`componentDidMount`で実行することが適切です。

まずは`OfflineSupport.js`というコンポーネントを作成します。

```jsx:OfflineSupport.js
import React, { PureComponent } from 'react';

class OfflineSupport extends PureComponent {
    componentDidMount() {
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker
                .register('./service-worker.js')
                .then(() => {
                    console.log('service worker registration successful');
                })
                .catch(err => {
                    console.warn(
                        'service worker registration failed',
                        err.message
                    );
                });
        }
    }

    render() {
        return null;
    }
}

export default OfflineSupport;
```

これを`_app.js`で使いましょう。

```jsx:_app.js
import App, { Container } from 'next/app';
import React from 'react';
import OfflineSupport from '../src/components/OfflineSupport';

class RootApp extends App {
    render() {
        return (
            <Container>
                <YourComponents />
                <OfflineSupport />
            </Container>
        );
    }
}

export default RootApp;
```

## おわり
ここまでの設定をすることでserver.jsなどを自前で作成しなくともNetlifyをService Workerを使うことは可能です。
Lighthouseのスコアもなかなかいい感じになりました。

![スクリーンショット 2018-08-02 0.03.47.png](https://qiita-image-store.s3.amazonaws.com/0/80473/13fd2e85-0ef1-561f-87b2-3f9e96999369.png)

ぜひ機会があれば試してみてください！