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'}) で構築されたもの)のサポートはより広範であり、バージョン 80 以降の Chrome と Edge、および Safari の最近のバージョンでサポートされています。専用ワーカーでは、静的と動的の両方の ES モジュール インポートがサポートされています。

Chrome と Edge は、バージョン 83 以降、共有ワーカーで ES モジュールをサポートしていますが、現時点でサポートを提供しているブラウザは他にありません。

インポート マップはサポートされていません

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

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

ブラウザ サポート

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

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() を使用してインポートできます。

ES モジュールを使用するバージョンと使用しないバージョンの 2 つの Service Worker を用意したら、現在のブラウザがサポートしているものを検出し、対応する Service Worker スクリプトを登録する必要があります。サポートを検出するためのベスト プラクティスは現在流動的ですが、GitHub の問題で推奨事項に関するディスカッションをフォローできます。