Service Worker の ES モジュール

importScripts() に代わる最新の方法です。

背景

ES モジュールは、以前からデベロッパーの間で人気があります。その他の多くのメリットに加えて、共有コードを一度リリースすれば、ブラウザや Node.js などの代替ランタイムで実行できる、汎用的なモジュール形式を利用できるというメリットもあります。最新のすべてのブラウザは ES モジュールをサポートしていますが、コードを実行できる場所がすべてサポートされているわけではありません。具体的には、ブラウザの Service Worker 内の ES モジュールのインポートのサポートが広がってきています。

この記事では、一般的なブラウザでの Service Worker における ES モジュールのサポートの現在の状態、回避すべき問題点、下位互換性のある Service Worker コードを配布するためのベスト プラクティスについて詳しく説明します。

使用例

Service Worker 内の ES モジュールに最適なユースケースは、ES モジュールをサポートする他のランタイムと共有される最新のライブラリまたは構成コードを読み込む場合です。

ES モジュールより前はこの方法でコードを共有しようとしていた場合、不要なボイラープレートを含む古い「ユニバーサル」モジュール形式(UMD など)を使用し、グローバルに公開される変数を変更するコードを記述する必要がありました。

ES モジュールを介してインポートされたスクリプトは、内容が変更され、importScripts()動作と一致する場合に、Service Worker の更新フローをトリガーできます。

現在の制限事項

静的インポートのみ

ES モジュールは、import ... from '...' 構文を使用して静的にインポートするか、import() メソッドを使用して動的にインポートできます。現在、Service Worker の内部では静的構文のみがサポートされています。

この制限は、importScripts() の使用に適用される同様の制限に似ています。importScripts() への動的呼び出しは Service Worker 内では機能しません。すべての importScripts() 呼び出しは本質的に同期なので、Service Worker が install フェーズを完了する前に完了する必要があります。この制限により、インストール時に Service Worker の実装に必要なすべての JavaScript コードをブラウザが認識し、暗黙的にキャッシュできるようになります。

最終的にはこの制限が解除され、動的 ES モジュールのインポートが許可される可能性があります。現時点では、Service Worker の内部で静的構文のみを使用していることを確認してください。

他のワーカーへの対応

「専用」ワーカーでの ES モジュールnew Worker('...', {type: 'module'}) で構築されたもの)のサポートが広く行われており、Chrome と Edge バージョン 80 以降、および最新バージョンの Safari でサポートされています。静的と動的の両方の ES モジュールのインポートが専用ワーカーでサポートされています。

Chrome と Edge では、バージョン 83 以降、共有ワーカーの ES モジュールがサポートされていますが、現時点では他のブラウザはサポートされていません。

地図のインポート未対応

マップをインポートすると、ランタイム環境でモジュール指定子を書き換えることができます。たとえば、ES モジュールを読み込む優先 CDN の URL を先頭に追加できます。

Chrome と Edge バージョン 89 以降ではインポート マップがサポートされていますが、現時点では Service Worker で使用できません

ブラウザ サポート

Service Worker の ES モジュールは、Chrome と Edge バージョン 91 以降でサポートされます。

Safari は Technology Preview 122 リリースでサポートを追加しました。今後、この機能は Safari の安定版でリリースされる予定です。

コード例

これは、ウェブアプリの window コンテキストで共有 ES モジュールを使用し、同じ ES モジュールを使用する Service Worker を登録する基本的な例です。

// Inside config.js:
export const cacheName = 'my-cache';
// Inside your web app:
<script type="module">
  import {cacheName} from './config.js';
  // Do something with cacheName.

  await navigator.serviceWorker.register('es-module-sw.js', {
    type: 'module',
  });
</script>
// Inside es-module-sw.js:
import {cacheName} from './config.js';

self.addEventListener('install', (event) => {
  event.waitUntil((async () => {
    const cache = await caches.open(cacheName);
    // ...
  })());
});

下位互換性

上記の例は、すべてのブラウザが Service Worker の ES モジュールをサポートしている場合は正常に機能しますが、この記事の執筆時点ではそうではありません。

サポートが組み込まれていないブラウザに対応するために、ES モジュール互換バンドラを介して Service Worker スクリプトを実行し、すべてのモジュール コードをインラインで含み、古いブラウザで動作する Service Worker を作成できます。または、インポートしようとしているモジュールがすでに IIFE または UMD 形式でバンドルされている場合は、importScripts() を使用してインポートできます。

Service Worker の 2 つのバージョン(ES モジュールを使用するバージョンと使用しないバージョン)が利用可能になったら、現在のブラウザがサポートする内容を検出し、対応する Service Worker スクリプトを登録する必要があります。サポートを検出するためのベスト プラクティスは現時点で流動的ですが、推奨事項については、この GitHub の問題の議論に従うことができます。

_Photo by Vlado Paunovic(出典: Unsplash