30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Service Worker のインポートを理解する

Service Worker では、importScriptsを使うことでスクリプトをインポートできる。
ES Modules (以下、ESM)を使うこともできるが、ブラウザによってはまだ対応していない。Chrome ではバージョン 91 から利用できるようになった。
この記事では、これらの機能を使って Service Worker でスクリプトをインポートする方法を見ていく。

動作確認には Next.js と Google Chrome を使っている。それぞれのバージョンは以下の通り。

  • Next.js 10.2.0
  • Google Chrome 91.0.4472.77

ユーザーが初めてページにアクセスした状況を再現するために、シークレットウィンドウでページを開くようにしている。

Module Service Worker と Classic Service Worker

Service Worker は、Module Service Worker と Classic Service Worker に分類できる。
全ての Service Worker は必ずどちらかになる。

どちらであるかは、Service Worker の登録時に決まる。
registerで登録する際にtypeの値をmoduleにすると、Module Service Worker になる。

// Module Service Worker になる
navigator.serviceWorker.register('/sw.js', {type: 'module'})

typeclassicにした場合、あるいは設定を省略した場合は、Classic Service Worker になる。

// Classic Service Worker になる
navigator.serviceWorker.register('/sw.js', {type: 'classic'})

// Classic Service Worker になる
navigator.serviceWorker.register('/sw.js')

そしてどちらの Service Worker であるかで、利用できるインポート方法が変わる。
Module Service Worker では ESM を、Classic Service Worker ではimportScriptsを、利用できる。
利用できない方のインポートを使おうとすると、エラーになる。

ES Modules の使い方

まずは、Module Service Worker による ESM の使い方を見ていく。

以下の内容のpublic/sw.jsを用意する。

import {x} from './module.js';

console.log(x);

public/module.jsも用意する。

export const x = 1;

そして最後に、pages/index.jsを以下の内容にする。

import {useEffect} from 'react';

export default function Home() {
  useEffect(() => {
    navigator.serviceWorker.register('/sw.js', {type: 'module'});
  }, []);

  return (
    <>
      <p>Hello Service Worker!</p>
    </>
  );
}

この状態で$ yarn devを実行しhttp://localhost:3000/にアクセスする。
そうするとログに1が表示されるので、Service Worker の登録、そしてその Service Worker によるmodule.jsの読み込みが上手くいっていることが分かる。

Dynamic Import は使えず、使おうとするとエラーになる。

// TypeError: import() is disallowed on ServiceWorkerGlobalScope by the HTML specification.
import('./module.js').then((res) => {
  console.log(res.x);
});

importScripts の使い方

次に、importScriptsの使い方を見ていく。

ESM と異なり、「エクスポートした値をインポートする」というような挙動にはならない。
読み込んだスクリプトを実行し、名前空間が共有される。
HTML ファイルのなかでscriptタグを使ったときと同じような使用感になる。

そのため、public/module.jsの内容を以下のようにしてそれをimportScriptsで読み込んだ場合、Service Worker 側でfoobarを使えるようになる。

const foo = 'foo';

function bar() {
  return 'bar';
}

public/sw.jsを書き換えて、確認してみる。

importScripts('./module.js');

console.log(foo);
console.log(bar);
console.log(bar());

最後にpages/index.jsを書き換えて、Classic Service Worker として登録されるようにする。

navigator.serviceWorker.register('/sw.js');

この状態でhttp://localhost:3000/にアクセスすると以下のログが流れ、module.jsと名前空間が共有されていること分かる。

foo
ƒ bar() {
  return 'bar';
}
bar

名前空間が共有されているので、同じ名前の変数を定義してしまうと、エラーになる。
そのため以下のようにするとエラーになる。

// public/module.js
const x = 1;
// public/sw.js
importScripts('./module.js');
const x = 9;

ここまでの例ではトップレベルでimportScriptsを実行していたが、installイベントのなかで実行することもできる。
そのため以下の内容にすると、ページを表示してから約3秒後にThis is module.jsがログに流れ、そこからさらに約3秒後に、fooがログに流れる。

// public/module.js
const foo = 'foo';

console.log('This is module.js');
// public/sw.js
function sleep(ms) {
  const startTime = performance.now();
  while (performance.now() - startTime < ms);
}

self.addEventListener('install', (e) => {
  e.waitUntil(
    new Promise((resolve) => {
      sleep(3000);
      importScripts('./module.js');
      sleep(3000);
      console.log(foo);
      resolve();
    })
  );
});

installイベントよりも後にimportScriptsを実行するとエラーになる。
そのため、以下のコードはエラーになる。

self.addEventListener('activate', (e) => {
  e.waitUntil(
    new Promise((resolve) => {
      importScripts('./module.js');
      resolve();
    })
  );
});

installイベントよりも後にimportScriptsでスクリプトを実行したい場合は、installイベント、あるいはトップレベルで、そのスクリプトを実行しておく必要がある。
以下のようにするとエラーにはならず、ページ表示時にThis is module.jsというログが 2 回流れる。

// public/module.js
console.log('This is module.js');
// public/sw.js
importScripts('./module.js');

self.addEventListener('activate', (e) => {
  e.waitUntil(
    new Promise((resolve) => {
      importScripts('./module.js');
      resolve();
    })
  );
});

ただ、public/module.jsのなかで変数を定義していた場合、その変数の定義を 2 回実行することになりpublic/module.jsでエラーが発生するので、注意する。

Classic Service Worker のなかで Dynamic Import を使うことはできない。

// TypeError: import() is disallowed on ServiceWorkerGlobalScope by the HTML specification.
import('./module.js').then((res) => {
  console.log(res.x);
});

一方で、typemoduleを設定していないscript要素や Web Worker では、Dynamic Import を使える。
Service Worker だけ挙動が異なっているため注意する。

script要素での Dynamic Import については以下を参照。

numb86-tech.hatenablog.com

Web Worker での Dynamic Import については以下を参照。

numb86-tech.hatenablog.com

更新確認とインポート

更新確認の際の挙動は、ESM でもimportScriptsでも同じ。挙動が変わることはない。
updateViaCacheの設定によって挙動が変化し、更新があった場合は Service Worker スクリプト自体が改めて読み込まれる。

更新確認やupdateViaCacheの詳細は以下の記事に書いた。

numb86-tech.hatenablog.com

参考資料