WebpackとWorkboxでらくらくオフライン対応

UI開発者 加藤

昨今の世界的な情勢を受けて、GoogleはWebサイトが常に利用可能であるためのガイダンスを公開しました。「Availability, reliability, resilience, and stability」のセクションでは主にファイルの配信について書かれており、サイトへのトラフィックが増えることによって起こりうる問題に備えるための手法を見ることができます。そのうちの1つがService Workerを利用してオフライン対応をすることです。オフライン対応をするには必要なファイルを適切にキャッシュすることが求められますが、Googleはその手段の1つとしてWorkboxの利用を推奨しています。

Workboxとは

WorkboxとはService Workerを手軽に扱うためのツールキットです。BackgroundSyncやGoogle Analytics用のモジュールが多数用意されています。今回使用するのはその内のWorkbox Precachingです。

Service Workerは少しのミスでページ全体が見られなくなってしまうなど、Webサイト上で動的に実行されるようなJavaScript機能とは影響範囲に大きな差があります。そのため、ファイルをキャッシュし、オフライン対応をするなどの単純なケースでは、自前でコードを用意するのではなくWorkboxを利用することでベストプラクティスにのっとり、ミスを減らすほうがよいこともあります。

今回はWebpackを使用したプロジェクトにWorkboxを導入してみます。WebpackではCSSや画像を含むページで使用するほとんどのリソースをいくつかのファイルにバンドルします。つまり、Webpackのビルドプロセス上で生成されたファイルだけをプリキャッシュするように設定すれば簡単にオフライン対応が実現できるということです。具体的にはwebpack.config.jsにWorkboxプロジェクトが公式に作成しているWorkbox webpack Pluginsをプラグインとして指定するだけです。

※この記事ではworkbox-webpack-plugin v5.1.3を使用しています。

Service Workerを新しく導入する場合

WebサイトにService Workerを新しく導入する場合はGenerateSWメソッドを使います。

// webpack.config.js

const {GenerateSW} = require('workbox-webpack-plugin');

const main = {
    plugins: [
        new GenerateSW({
            skipWaiting: true,
            clientsClaim: true
        })
    ]
};

module.exports = [main];

skipWaitingclientsClaimはworkbox-webpack-pluginのオプションで、trueを設定するとそれぞれinstallイベントでself.skipWaiting()が、activateイベントでself.clients.claim()が実行されるようになります。

ビルド後に生成されるファイルは「service-worker.js」と「workbox-{hash}.js」の2つです。どちらのファイルもwebpack.config.jsの設定によって内容が書き換えられます。実際にservice-workerとして機能するのは「workbox-{hash}.js」です。service-worker.jsはworkbox-{hash}.jsで定義されるメソッドを実行したり、キャッシュするファイルの一覧をworkbox-{hash}.jsに渡したりする役割を持ちます。(いずれもファイル名は設定で変更できます)

既存のService Workerに適用する場合

すでにService Workerがある場合はInjectManifestメソッドが使えます。既存のService Workerに以下のように記述します。

// service-worker.js

import {precacheAndRoute} from 'workbox-precaching';

precacheAndRoute(self.__WB_MANIFEST);

precacheAndRouteはworkbox-precachingの機能で、引数として指定されたファイルパスにマッチするファイルをすべてキャッシュします。またworkbox-webpack-pluginは既存のService Workerに含まれるself.__WB_MANIFESTをめがけてビルドプロセスに上がってきたファイルのパスを挿入します。結果としてビルドを実行するとWebpack対象ファイルをすべてキャッシュする機能を持ったService Workerができあがります。

webpack.config.jsには以下のように指定します。

// webpack.config.js

const {InjectManifest} = require('workbox-webpack-plugin');

const main = {
    plugins: [
        new InjectManifest({
            swSrc: './src/sw.js'
        })
    ]
};

module.exports = [main];

chunksexcludeChunksオプションなどを指定すればどのファイルをキャッシュするかを細かく設定することもできます。

HTMLファイル自体をキャッシュする

オフライン対応をするにはCSSやJavaScriptなどのサブリソースだけでなく、HTMLファイルもキャッシュする必要があります。この場合は事前にキャッシュするプリキャッシュではなく、ファイルがリクエストされた時に動的にキャッシュするランタイムキャッシュが活用できます。ランタイムキャッシュもwebpack.config.jsで設定できます。

// webpack.config.js

const {GenerateSW} = require('workbox-webpack-plugin');

const main = {
    plugins: [
        new GenerateSW({
            runtimeCaching: [
                {
                    urlPattern: 'index.html',
                    handler: 'NetworkFirst'
                }
            ]
        })
    ]
};

handlerには、Workbox内で定義されているキャッシュ戦略からいずれかを選択します。NetworkFirstの場合はサーバーにリクエストを行いますが、リクエストが失敗した場合はキャッシュされているリソースを参照します。 これでindex.htmlにリクエストがあった際にファイルがキャッシュされるようになります。ただしランタイムキャッシュの場合はService Workerがインストールされた後にリクエストが発生して初めてキャッシュされるため、index.htmlをキャッシュするには最低2回のリクエストを発生させる必要があります。

その他HtmlWebpackPluginなどを利用してHTMLファイル自体をビルドプロセスに含む方法もあります。EJSなどのテンプレートHTMLファイルを作成し、webpack.config.jsのプラグインに追加するだけです。

// webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const {GenerateSW} = require('workbox-webpack-plugin');

const main = {
    plugins: [
        new HtmlWebpackPlugin({
            inject: false,
        }),
        new GenerateSW()
    ]
};

この場合はindex.htmlもランタイムキャッシュではなくプリキャッシュされることになります。

Service Workerを登録する処理は別途どこかで実行する必要がありますが、Service Workerのコードは一切書くことなくオフライン対応を実現できます。WorkboxはPUSH通知を利用したい場合や、細かく挙動を管理したい場合には向いていないことが多いですが、静的サイトをとにかくスピーディーにオフラインでも閲覧できるようにしたい場合には活躍できるツールです。万が一に備えてサイト上の特に重要なページだけでもオフライン対応してみてはいかがでしょうか。