Next.js with Netlify でService Workerを使う


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

追記2018.8.1
使用プラグインを変更しました
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に適宜追加する感じです。

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.jsprecache-manifest.[hash].jsいうファイルが生成されていることがわかります。

package.json

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

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

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というコンポーネントを作成します。

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で使いましょう。

_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のスコアもなかなかいい感じになりました。

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