kqito's 技術ブログ

技術やプログラミングについて思ったことを呟くブログ

create-react-appで生成されるservice-worker周りについてのメモ

はじめに

こんにちは。Webフロントエンドを専攻しているkqitoです。 今回はcreate-react-appで自動で生成されるservice-worker周りについてメモします。

create-react-appする

create-react-appのバージョンは以下の通りです。

$ create-react-app -V
> 3.3.0

このような形で生成します

create-react-app test-service-worker --template typescript

今回はtypescriptでservice-workerのregister, unregisterの部分を読んでいきます。

そういえば

create-react-app repo --typescript

みたいなオプションはdeprecatedされていました。

service-worker.tsをみてみる

exportされている関数

exportされているのはunregisterregisterの2つだけです。順を追ってコードを見てみます。

unregister

export function unregister() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready
      .then(registration => {
        registration.unregister();
      })
      .catch(error => {
        console.error(error.message);
      });
  }
}

早期returnなどはせず、navigatorグローバル変数にserviceWorkerがあるなら(ブラウザ対応してるなら)unregisterしてる単純な関数ですね。

こちらはcreate-react-app/src/index/tsxにてデフォルトで呼び出されている関数です。

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

デフォルトはunregisterしてるけど、registerするときには「隠れた罠(pitfalls)」があるので注意してね的な内容ですね。

基本的に開発時にprecacheする必要はないですし、productionデプロイ時に利用するだけで良さそう。

ということは何も編集していない限りは上記のunregisterが呼び出されていると...

register

以下がregister関数の全体です。

export function register(config?: Config) {
  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
    // The URL constructor is available in all browsers that support SW.
    const publicUrl = new URL(
      process.env.PUBLIC_URL,
      window.location.href
    );
    if (publicUrl.origin !== window.location.origin) {
      // Our service worker won't work if PUBLIC_URL is on a different origin
      // from what our page is served on. This might happen if a CDN is used to
      // serve assets; see https://github.com/facebook/create-react-app/issues/2374
      return;
    }

    window.addEventListener('load', () => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

      if (isLocalhost) {
        // This is running on localhost. Let's check if a service worker still exists or not.
        checkValidServiceWorker(swUrl, config);

        // Add some additional logging to localhost, pointing developers to the
        // service worker/PWA documentation.
        navigator.serviceWorker.ready.then(() => {
          console.log(
            'This web app is being served cache-first by a service ' +
              'worker. To learn more, visit https://bit.ly/CRA-PWA'
          );
        });
      } else {
        // Is not localhost. Just register service worker
        registerValidSW(swUrl, config);
      }
    });
  }
}

ifがいくつかネストしていますが、やっていることはproductionかつserviceWorkerがブラウザ対応しているときにloadイベントを登録している感じですね。

const isLocalhost = Boolean(
  window.location.hostname === 'localhost' ||
    // [::1] is the IPv6 localhost address.
    window.location.hostname === '[::1]' ||
    // 127.0.0.0/8 are considered localhost for IPv4.
    window.location.hostname.match(
      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
    )
);

上記を満たすホストだった場合にはservice-workerが読み込めるかを確認 + service-workerを登録するcheckValidServiceWorker関数を実行したりしています。

service-workerでやっていること

yarn buildなどで生成されるservice-worker.jsは以下のようなファイルです。このファイルが上記のservice-worker.tsで登録されています。

importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");

importScripts(
  "/precache-manifest.1e65759576907aec8ccc3e22bff345f4.js"
);

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

workbox.core.clientsClaim();

/**
 * The workboxSW.precacheAndRoute() method efficiently caches and responds to
 * requests for URLs in the manifest.
 * See https://goo.gl/S9QRab
 */
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), {
  
  blacklist: [/^\/_/,/\/[^\/?]+\.[^\/]+$/],
});

これらはworkboxによって提供されているservice-workerです。

workboxについてはこちらの記事が理解しやすかったです

qiita.com

yarn ejectしてwebpackの設定を見ればわかるのですが、workbox-webpack-pluginというライブラリを利用してworkboxに関する設定ファイルを生成しているため生成されるわけですね。

      new ManifestPlugin({
        fileName: 'asset-manifest.json',
        publicPath: paths.publicUrlOrPath,
        generate: (seed, files, entrypoints) => {
          const manifestFiles = files.reduce((manifest, file) => {
            manifest[file.name] = file.path;
            return manifest;
          }, seed);
          const entrypointFiles = entrypoints.main.filter(
            fileName => !fileName.endsWith('.map')
          );

          return {
            files: manifestFiles,
            entrypoints: entrypointFiles,
          };
        },
      }),

ざっくりまとめるとこんなことをしています。

  • Precacheするファイルの指定(runtime cacheとかはしてない)
  • Navigation requestsされるprecacheされているファイルの指定

こんな感じでしょうか。

ちなみに最新バージョンのworkboxではregisterNavigationRouteはdeprecatedされている感じでした

github.com

まとめ

ざっくりと生成されるファイル等を読んでみました。

Cache Storageはパフォーマンスをかなり向上する物ですが、キャッシュ削除のタイミングなどちゃんと図らないと意図せぬ内容が表示されたりすると思うので気をつけながら利用したいところです。

以上です。