🎯

PWAマスター講座 第1部:はじめに

2024/12/02に公開

こんにちは、開発者の皆さん!
PWA(Progressive Web App)の深い理解を目指した記事シリーズへようこそ!

「PWA」という言葉が生まれたのは8年前ですが、実際にPWAに取り組んだWeb開発者はまだそれほど多くありません。このシリーズでは、PWA開発に関する情報を分かりやすく、網羅的に提供していきます。

今回は、基礎編です。この入門記事では、PWAのキーコンセプト、可能性と限界、そして実践的な例を通して理解を深めます。PWA開発には独特の知識が必要なので、しっかりとした理論的な土台を築くことが重要です。スムーズな学習のためには、クライアントとサーバー間のやり取りを理解しておくことが必要です。

コーヒーを淹れて、リラックスして、一緒に始めましょう!

img

基本事項

PWAとは何か

インターネットユーザーは、Web開発といえばウェブサイトの制作だと考えるでしょう。しかし、現代のWeb開発者は「ウェブサイトではなく、Webアプリケーション」と言うでしょう。

違いは何でしょうか?

「Webアプリケーション」という言葉は、Web開発者の焦点を、視覚的な表現(マークアップ)からアプリケーションの機能(インタラクティブ性)へと移行することを示しています。言い換えれば、現代のウェブサイトは、単なるページの集まりではなく、多くの機能を持つ複雑なアプリケーションになりつつあります。

現代のWebアプリケーションは、ネイティブアプリケーションの機能に非常に近づいています。プッシュ通知、ファイルシステムへのアクセス、ジオロケーションデータ、デバイスの位置情報など、多くのWeb APIが既に利用可能です。これらのAPIを適切に活用することで、ネイティブのモバイルアプリやデスクトップアプリに極めて近いWebアプリケーションを実現できます。このようなWebアプリケーションをPWAと呼びます。

PWA(Progressive Web Application)は、スタンドアロンアプリケーションとしてインストールできる特別な種類のWebアプリケーションです。これは、ブラウザのキャッシュなど、様々なWeb APIを活用することで実現されます。

img

PWAの特徴

PWAを他のWebアプリケーションと区別する機能は何でしょうか?

まず、ブラウザから直接インストールできることです。
Webアプリケーションはスタンドアロンアプリケーションとしてインストールされ、隔離されたブラウザ環境で実行されます。ネイティブアプリと同様に、PWAには独自のデータストア(キャッシュ)があります。

第二に、オフライン機能です。
多くのWebアプリケーションは、以前アクセスしたページであっても、ネットワーク接続がないとメインページを表示できません。PWAのキャッシュ機構は、ユーザーが既にアクセスしたページを動的にキャッシュするだけでなく、アプリケーションの正常な動作に必要なリソースを事前にキャッシュすることも可能です。

オフラインモードのサポートの程度は、アプリケーションによって異なります。一部のアプリケーションでは、ネットワーク接続がない場合でもユーザーがPOSTリクエストを送信できますが、他のアプリケーションでは、ユーザーが手動で保存しない限りページが表示されません。

開発者は、以下のような機能も頻繁に実装します。

  • プッシュ通知
  • カウント付きの通知アイコン
  • 大量のファイルのバックグラウンドでのダウンロード

多くの場合、PWAは、一般的なWebアプリケーションよりも優れたUI/UXを提供し、ネイティブアプリケーションと共通する特徴です。

アプリケーション開発におけるPWAの位置づけ

PWAという用語がGoogleによって2016年に提案されたにもかかわらず、2024年現在、広く普及しているとは言えません。高速で使いやすいネイティブアプリが存在するため、PWAの技術を学ぶ必要がないように思えるかもしれません。具体的な利点と欠点を検討する前に、PWA導入の実際の事例をいくつか紹介したいと思います。きっと、これらの企業はご存じだと思います。

  • オンラインビデオエディターClipchampは、PWA開発によって月間インストール数を97%増加させ、パフォーマンスを2.3倍向上させました。PWAを利用したユーザーの保持率は、従来のユーザーよりも9%高かったとのことです。
  • Starbucksは、PWAによってオフライン機能を提供し、ユーザーの関心を高めました。その結果、日々のアクティブユーザー数が2倍に増加し、注文の多様化も23%向上しました。
  • Rakuten 24は、モバイル開発のリソースが限られていたため、PWAを採用しました。最も重要な新機能は、静的リソースのキャッシュ機能と、ホーム画面へのアプリ追加機能でした。PWAユーザーは、同サイトへのアクセス頻度が310%、購入頻度が150%増加しました。ただし、現在では、そのWebアプリケーションをインストールできるようにはなっていないようです。情報のソースはこちらをご覧ください。

PWAの利点と欠点

多くの大企業はPWAの開発に投資することでポジティブな効果を実感していますが、すべてのウェブアプリケーションがPWAであるべきというわけではありません。PWAの利点と欠点を見ていきましょう。

利点 欠点
🌐様々なOSを持つ多種多様な端末がブラウザを使用します 🔍PWAで利用できる機能は、具体的なブラウザによって異なります
📈ウェブアプリケーションを開発すれば、モバイルアプリも一緒に手に入ります ⚙️ ネイティブアプリの方がパフォーマンスが高いです
🚶‍♂️ユーザーはウェブサイトの使用体験が気に入ればPWAをインストールできます。
マーケットプレイスを利用する必要はありません
🤔ウェブアプリケーションのインストール可能性は、一般のユーザーにはまだよく理解されていません。マーケットプレイスは他のユーザーの役立つ評価を提供します
🔄多数のAPI(例:BackgroundSyncAPI)により、オフラインで動作する非常にインタラクティブなアプリを開発できます 🏗️この種のアプリケーションの開発は、より洗練された複雑なアーキテクチャを必要とします。開発の初期段階からこれを考慮することが望ましいです
🧩現代のWebAPIは、さまざまなデバイスで最も一般的に使用される機能をすべてサポートしています 🔍これらのAPIの使用可能性は、ブラウザとそのバージョンによって異なります

ほとんどのPWAの利点には明確な欠点が存在するため、常にPWAの作成を目指すべきだとは一概には言えません。PWAを選択することで時間とコストを節約できる場合もありますが、その決定が特定のユーザーにとって問題の原因となることもあります。PWAを使用するかどうかの決定は、特定のプロジェクトの要件に基づいて行うべきです。

考察のため
  1. アーキテクチャへの影響は、タスクの複雑さに直接依存します。例えば、ウェブサイトをインストール可能にすることは、既存のアーキテクチャを変更することなく、いつでも可能です。オフラインでのアプリケーションの最小限の動作の実装も、大きな労力を必要としません。一方、オフラインでデータを変更する機能は、これらの変更をローカルに記録し、接続が回復したときに実行する必要があることを意味します。これははるかに複雑で、多くの極端なケースを考慮する必要があります。

    • ユーザーセッションが終了しており、ネットワークが復旧した場合はどうすればよいでしょうか?
    • オフラインの変更をの情報どのくらい保存する必要がありますか?
    • オンラインで行われた変更とローカルで行われた変更の競合をどのように解決する必要がありますか?
    • ネットワークが復旧しても、バックグラウンドでデータ送信を試みた場合に失敗した場合、どうすればよいでしょうか?
      これらの質問には、唯一の正解はありません。これらの機能を含めるかどうかを決定する際には、時間コストを考慮する必要があります。
  2. ユーザーは、特定のPWAが何ができるのかを理解する必要があります。必要なリソースをダウンロードしている場合は、ユーザーに少なくともそのことを伝える必要があります(理想的には、ダウンロードの進行状況を表示します)。ネットワークの問題は、誰にでも起こり得ます。ユーザーがキャッシュされた古いデータを表示した場合、誤った結論に至る可能性があります(例:タスクページに新しいタスクがない)。このような場合は、表示されている情報が古い可能性があることをユーザーに通知することが適切です。

  3. PWAの略語には、非常に重要な原則が隠されています。「Progressive」は、Progressive Enhancementも意味し、PWAの機能は、ブラウザがこれらの機能をサポートしている場合、またはユーザーが許可している場合に、基本的な機能を補完する必要があることを意味します。PWAの独自の機能に焦点を当てるべきではありません。ユーザーは、PWAのインストールを意図的に選択しない可能性があり、特定のブラウザでPWA機能がサポートされていない可能性もあります。PWAはWebアプリケーションの中核ではなく、Webアプリケーションに追加される「ボーナス」であることを覚えておく必要があります。

Service Workerについて少し

基本事項

Service Workerは、PWAを実現するための重要な技術です。Service Workerは、Webアプリケーションの初回起動時に自動的にインストールされます。本質的に、Service WorkerはWebアプリケーションとサーバーの間のプロキシとして機能します。

Service Workerは、クライアントからのfetchリクエストをインターセプトできるため、インターネット接続が不安定な場合に、開発者がロジックを定義できます。キャッシュ機能(ダウンロードしたリソースをローカルに保存する機能)と組み合わせることで、接続が全くできない場合でも、リクエストに応答できます。

img

Service WorkerをWeb Workerと混同しないように注意してください。Web Workerはより一般的な概念であり、Service Workerも含まれています。これは、別々のスレッドで動作する多くのワーカーを指します。クライアントとのやり取りは、イベント駆動アーキテクチャによって行われ、これはWebアプリケーションのコードでイベントを購読し、Service Worker側(およびその逆)でこれらのイベントを公開することを意味します。これにより、SWはメインプロセスを負担することなく機能できます。

MDNによると、Service Workerはすべての最新のブラウザでサポートされています。これは、caniuse.comのデータと一致しており、Service Workerは97.6%のデバイスでサポートされています。

キャッシュ

初期化

リソースをキャッシュするには、複数のキャッシュを一度に保存する caches オブジェクトを使用します。1つのドメインには1つのキャッシュストアが対応します。1つのドメインで複数のサービスを展開する場合は、この点を考慮する必要があります。キャッシュを開くには、非同期メソッド open(cacheName) を使用します。

caches.open("cache-v1").then(cache => {
    // キャッシュ関連のコード
});

詳細については、MDN Web Docs を参照してください。

追加

キャッシュに追加するには、add(url) メソッドを使用します。
たとえば、アプリケーションのルートページを次のようにキャッシュに追加できます。

caches.open("cache-v1").then(cache => {
  // ルートページをキャッシュに保存
  cache.add("/");
});

Service Workerは、指定されたアドレスのリソースを取得しようとします。メソッドの呼び出しは、undefined で解決するPromiseを返します。リクエストが正常に完了した場合にのみ、レスポンスはキャッシュに保存されます。
詳細については、MDN Web Docs を参照してください。

一度に複数のリソースをキャッシュに追加することもできます。これには、addAll(requests) メソッドを使用します。

caches.open("cache-v1").then(cache => {
  // インデックスページと静的なリソースをキャッシュに保存
  cache.addAll([
    "/",
    "/index.html",
    "/style.css",
    "/app.js"
  ]);
});

add() と同様に、すべてのリソースが取得された場合にのみ、キャッシュが更新されます。メソッドの呼び出しは、undefined で解決するPromiseを返します。
詳細については、MDN Web Docs を参照してください。

書き換え

既存のキャッシュを置き換えたり、リソースを手動でキャッシュに追加するには、put(url, response) メソッドを使用します。Response オブジェクトを手動で作成できるため、ネットワーク接続は必須ではありません。これにより、リクエスト時に生成されたデータを返すことができます。

caches.open("cache-v1").then(cache => {
  // 手動生成されたデータをキャッシュに追加
  cache.put("/api/todo", new Response(JSON.stringify([{ id: 1, title: "hello" }, { id: 2, title: "world" }])));
});

メソッドの呼び出しは、undefined で解決する Promise を返します。
詳細については、MDN Web Docs を参照してください。

取得

キャッシュから以前保存したリソースを取得するには、match(request, options)matchAll(request, options) のメソッドを使用します。引数については、MDN Web Docs を参照してください。

caches.open("cache-v1").then(async (cache) => {
  // 以前保存されたルートページを取得する
  const response = await cache.match("/");
});

match() メソッドの呼び出しは、Promise を返します。リクエストされたリソースがキャッシュにある場合は、response で解決します。リソースが見つからない場合は、undefined で解決します。
詳細については、MDN Web Docs を参照してください。

また、matchAll() メソッドは、特定の条件に一致するキャッシュのリソースの配列を返します。
たとえば、

caches.open("cache-v1").then(async (cache) => {
  // 例えば、"/"と"/?query=lorem-ipsum"はキャッシュに存在すれば、下記のリクエストで取得できます
  const responses = await cache.matchAll("/", { ignoreSearch: true });
});

詳細については、MDN Web Docs を参照してください。

SWのライフサイクル

インストール

Service Workerが動作を開始する前に、アプリケーションのメインコードで登録する必要があります。Service Workerがブラウザでサポートされていることを確認してから登録するのが良い習慣です。

// その他の初期化コード
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/my-service-worker.js");
}

ワーカーのスコープ(可視範囲)は、登録時に定義されます。スコープとは、Service Workerが発信するfetchリクエストを認識するURLの集合です。デフォルトでは、Service Workerのスコープは、そのURLに対応します。たとえば、www.example.com/resources/my-service-worker.jsにあるService Workerは、www.example.com/resourcesのスコープに対応します。この場合、Service Workerはwww.example.com/pagesのURLからのfetchリクエストを完全に無視します。

登録時に、スコープを明示的に指定することもできます。

navigator.serviceWorker.register("/my-service-worker.js", { scope: "./" });

引数に指定されたスコープは、デフォルトで定義されるスコープよりも広くなることはできません。上記の例に戻ると、下記のようにスコープを変更することはできません。

navigator.serviceWorker.register("/resources/my-service-worker.js", { scope: "/" });

/ のスコープは、Service Workerの元のURL /resources よりも広いためです。

このパラメーターは、Service Workerのスコープを狭める必要がある場合にのみ使用してください。Service Workerの登録に関するその他の例と詳細については、MDN Web Docs を参照してください。

もう1つの重要な点は、1つのスコープに対して1つのService Workerしか存在できないことです。これは、1つのWebページで同時に1つのService Workerしかアクティブにできないことを意味します。

登録プロセスは3つのステップで実行されます。

  1. Service Workerのスクリプトをダウンロードする。
  2. ダウンロードしたスクリプトをパースする。
  3. インストールを実行する。
    開発者は、インストールのロジックを拡張できます。
    たとえば、この段階で、多くの場合、静的リソースのキャッシュを追加します。

これらのいずれかのステップでエラーが発生すると、Service Workerはインストールされません。インストール時の問題を解決する際には、原因に注意する必要があります。

img
SWダウンロード時のエラー例

インストール時にリソースをキャッシュするには、次のように記述できます。

self.addEventListener('install', event => {
  // インストール中のキャッシュ化の例
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/resources/static/logo.svg'))
  );
});

Service Workerのインストールが成功すると、待機状態になります。この時点で、他のページを制御するアクティブなService Workerがない場合、このステップはスキップされます。待機状態の詳細については、SWのライフサイクル:更新 を参照してください。

実践:インストールとキャッシュ

Service Workerのインストール時のキャッシュ動作を視覚的に確認できるウェブページ を用意しました。開発者ツールを使用して、SWの状態やキャッシュの内容を確認する方法に関する完全な手順が含まれています。この記事で開発者ツールをより詳細に理解したい人のために、Chrome DevToolsを使ったPWA開発で役立つ主要なツールを以下に示します。(デバッガーなど、その他の開発者ツールと併用できます)

必要なすべての開発ツールは、「アプリケーション」タブにあります。それらには以下が含まれます。

  • Manifest - manifest.json の内容を示します。これは、WebアプリケーションをPWAとしてインストールできるようにする機能の他に、さまざまなデバイスのショートカット用のアイコン、背景色、テーマの色、表示方法などを指定できます。プロパティのサポートはブラウザによって異なるため、使用前にMDN Web Docs で確認することをお勧めします。

    DevToolsのManifestタブ
  • Service Workers - Service Workerの状態を示します。ソースのリンクをクリックすると、Service Workerのソースコードに移動できます。また、現在のService Workerバージョンがインストールされた日時も表示されます。ここで、「更新」をクリックしてSWを手動で更新したり、「登録解除」をクリックしてアクティブなSWを削除したりできます。

    DevToolsのSWタブ
  • Cache Storage - キャッシュの内容(Cache API)に関する情報を表示します。ここで、個々のキャッシュリソースの内容を確認したり、手動で削除したりすることもできます。

    DevToolsのCache Storageタブ

PWAのデバッグについてもっと知りたいですか?Googleには、詳細な情報を含む素晴らしい記事 があります。

アクティベーション

インストールが完了すると、最初のバージョンのService Workerは動作の準備ができていますが、すぐにリクエストをインターセプトするわけではありません。インストールが完了した後でも、リクエストが送信された場合、デフォルトではService Workerはアクティブになりません。これは一貫性の原則に基づいています。Service Workerなしでページが開かれた場合、Service Workerなしで機能する必要があります。これにより、ページが予測可能な方法で機能することを保証できます。
SWをアクティブ化する方法は2つあります。

  • ページをリロードします。リロードすると、SWは最初から有効になります。
  • clients.claim() - アクティブでないすべてのクライアント(分かりやすく言うと、Webアプリケーションが開かれているタブ)でSWのアクティブ化を強制します。ページをリロードする必要がないため、アプリケーションによっては便利です。
    一般的にPWAはProgressive Enhancementを意味し、つまり、PWAの機能は追加的なものであり、Webアプリケーションの正常な動作に必須ではありません。clients.claim()を使用する場合は、WebアプリケーションがSWを使用せずに正常に機能することを確認する必要があります。

activate イベント自体は、アクティブなSWが変更されたときに発生します。これは、SWの最初のインストール時と更新時の両方で重要です。

self.addEventListener('activate', event => {
  // 新しいSWをすぐ有効にする
  self.clients.claim();
});

clients.claim() は、SWの最初のインストール時のみ実行されることに注意してください。更新時には、このコマンドは無視されます。

fetchのインターセプト

デフォルトでは、Service Workerはfetchリクエストをインターセプトしません。
これを開始するには、fetch イベントに登録する必要があります。

self.addEventListener("fetch", (event) => {
  // 適切なfetch処理を記述
});

説明を簡単にするために、Webアプリケーション全体が静的リソースで構成されていると仮定します。そのため、リソースを一度キャッシュすれば、それが変更されないことが保証されます(ただし、実際の開発でははるかに複雑です)。

  1. まず、キャッシュにすでに保存されているリクエストのレスポンスを調べます。
  2. リソースがキャッシュにある場合は、サーバーへのリクエストを行わずにすぐにそれを返します。(1-2-3)
  3. リソースがキャッシュにない場合は、サーバーにリクエストを行い、エラーが発生しない場合にレスポンスをキャッシュして返します。(1-2-4-5-3)

この戦略はCache Firstと呼ばれます。

img
CacheFirst戦略の図

実際には、この戦略は次のように実装されます。

self.addEventListener("fetch", (event) => {
  async function cacheFirst(url) {
    const cacheStorage = await caches.open("cache-v1");
    // キャッシュからのレスポンスを取得
    const cachedResponse = await cacheStorage.match(url);

    if (cachedResponse) {
      // キャッシュにあれば、そのまま返却
      return cachedResponse;
    } else {
      // キャッシュになければ、まずフェッチする
      const response = await fetch(url);
      // 取得したレスポンスをキャッシュに保存
      await cacheStorage.put(url, response);
      // そしてクライアントに返す
      return response;
    }
  }

  const { url } = event.request;
  event.respondWith(cacheFirst(url));
});

他にも戦略があります。それらについては、このシリーズの次の記事で説明します。

実践:fetchのインターセプト

このウェブページ では、実際にキャッシュとリクエストのインターセプトを確認できます。このデモでは、上記のコードと同様にCache First戦略が実装されています。キャッシュとネットワークへのリクエストの違いを視覚的に示すために、Service Worker側で2秒の遅延が設定されています。

開発者ツールを「ネットワーク」タブで開いて、有効なURLを入力し、「fetch」をクリックします。3秒以内に、取得したレスポンスのJSONが表示されます。もう一度「fetch」をクリックすると、すぐにレスポンスが返ってきます。ネットワーク接続が全くない場合でも、キャッシュにあるためレスポンスが返ってきます。


クライアントからのフェッチとSWからのフェッチ

これは、1つのリクエストを送信した後のネットワークのタイムラインです。歯車のアイコンが付いた記録は、Service Workerからのfetchリクエストです。最初の歯車のない記録は、クライアントからのリクエストです。

キャッシュからデータが即座に返される場合は、ネットワークに表示されるのはクライアントからのリクエストのみです。そのため、SWがfetchをインターセプトした場合、実際にはソースからのリクエストはSW内で処理されることを理解することが大事です。

更新

インストール時の動作は、更新されたSWの場合、少し異なります。
更新プロセスは、以下の場合に開始されます。

  • SWのスコープにあるページに移動した場合
  • メインコードからpush または sync メソッドが呼び出された場合(24時間以内に更新がない場合)
  • .register() が呼び出された場合、ただし、SWのURLが変更されている場合のみ。それでも、これはアンチパターンです。Service WorkerのURLを変更しないようにしてください
  • メインコードから手動で更新する場合:
navigator.serviceWorker.register('/my-service-worker.js').then(reg => reg.update());

register メソッドはSWの登録結果を返し、それを介して手動で更新を呼び出すことができます。また、たとえば、アクティブなSWに関する情報 (reg.active) も含まれています。

SWが更新されたと見なされるのは、ブラウザで現在動作しているものとバイト単位で異なる場合です。これは、SW内でインポートされているスクリプトにも適用されます。

新しいバージョンのService Workerの初期化は、動作中のバージョンと並行して開始されます。新しいバージョンでもinstallイベントが発生します。新しいバージョンの初期化中にエラーが発生した場合(以前説明した手順のいずれかで)、新しいバージョンは破棄され、古いバージョンが動作を続けます。

新しいSWのインストールが正常に完了すると、待機状態になります。この状態は、現在のアクティブなSWが少なくとも1つのクライアントを制御している限り続きます。クライアントとは、このSWを使用するWebページまたはWeb Workerのことです。この状態は、開発者ツール内のService Workerタブで確認できます。また、この状態を手動で変更することもできます。

self.skipWaiting() コマンドで待機状態をスキップできます。この関数を呼び出すと、古いバージョンのSWがオフになり、新しいバージョンがインストールされるとすぐにアクティブになります。

このトピックを学習していたとき、私は最初に、このメソッドとclients.claim()の違いがわかりませんでした。主な違いは次のとおりです。

self.skipWaiting() clients.claim()
有効化イベント前にのみ呼び出すことができます(例えば、install) activateイベントに呼び出されます
新しいSWの各インストールで呼び出すことができます 最初のインストール時だけ呼び出されます。スコープ内のページの制御をすぐに取得するために使用されます

これからは、いくつかの結論が導き出されます。

  • SWの更新時にactivateイベントは、マイグレーション操作(古いキャッシュのクリアなど)に最適なイベントです。古いSWがページを制御しなくなり、新しいSWが正常にインストールされたことを保証します。
  • self.skipWaiting() を使用すると、ページが1つのバージョンのSWで開かれたにもかかわらず、別のバージョンで動作し続ける可能性があります。SWの実装によっては、ページが以前の(すでに古いキャッシュから取得され、削除された)コードに基づいて動作している場合、新しいバージョンのSWと正しく動作しない問題が発生する可能性があります。
対策の考え

先ほど述べたバージョン管理の問題を回避する方法について話し合ってみましょう。この問題は、一貫性が守られていない結果です。ページが最初から新しいバージョンのService Workerを使用すれば、問題は発生しません。これは、ページをコードでリロードすることで実現できます。location.reload()。しかし、新しいバージョンのSWがインストールされた後にすぐにページをリロードすることは、UXの観点からあまり適切ではありません。ユーザーは、突然のページのリロードにイライラする可能性があります。さらに悪いことに、その間に情報を入力していた場合、情報が失われるリスクがあります。一見、このアプローチはメリットよりもデメリットが多いように思えます。

しかし、それを大幅に向上させる方法があります。

  1. 新しいバージョンのSWがインストールされ、古いバージョンの実行が終了するのを待っている場合、新しいバージョンが動作する準備ができたことをユーザーに通知します。
  2. ユーザーは、古いバージョンで必要な操作をすべて完了します。
  3. 通知ウィンドウで、ユーザーは「更新」をクリックします。
  4. ページがリロードされ、新しいバージョンのSWで動作する準備が整います。

これにより、バージョン管理の問題を回避するだけでなく、ユーザーにSWの変更を丁寧に通知することもできます。ユーザーは適切なタイミングで更新を選択し、実行できます。これは、PWAの機能を実装すると、UXの新しい課題が発生するケースの1つです。多くのSWの操作はユーザーの目から隠されているため、ユーザーに裏側で発生しているすべての操作を適切かつ十分に通知する必要があります。

クライアントとの連携

SWのコードは別スレッドで実行されるため、メインスレッドのコードに直接アクセスできません。それらをつなぐには、postMessage(message, options) メソッドを使用します。

// アクティブなクライアントのリストを取得
const clients = await self.clients.matchAll();
// 各クライアントにメッセージを送信
clients.forEach(client => client.postMessage("SWからこんにちは!"));

メインスレッドのコードでは、次のようにメッセージイベントリスナーを追加できます。

if ("serviceWorker" in navigator) {
  // SWを登録
  navigator.serviceWorker.register("/my-service-worker.js");
  // 登録されたSWにメッセージイベントリスナーを追加
  navigator.serviceWorker.addEventListener("message", event => console.log(event.data));
}

メインスレッドからSWにメッセージを送信するには、次のように記述できます。

if ("serviceWorker" in navigator) {
  // アクティブなSWにメッセージを送信
  navigator.serviceWorker.controller.postMessage("クライアントからこんにちは!");
}

Service Worker側でのリスナーは、同様に記述できます。

self.addEventListener("message", event => console.log(event.data));

スレッド間でデータを転送する際には、structured clone アルゴリズムが使用されます。この重要な結果として、postMessage はすべてのデータ型をサポートするわけではありません。
たとえば、関数やErrorオブジェクトのインスタンスを渡すことはできません。
したがって、転送できるのは、すべてのシリアライズ可能なオブジェクトです。

実践:クライアントとの連携

このページでは、クライアントからSWに送信されるリソースのキャッシュを操作できます。前のデモと同様に、fetchリクエストはCacheFirst戦略を使用しています。この例では、無効なURLであっても、レスポンスをキャッシュに保存できます。この場合、fetchを使用してもエラーは発生せず、結果がキャッシュから直接取得されます。開発者ツールを使用して、これを確認してください。リソースを手動でキャッシュに追加し、そのリソースのアドレスでfetchを実行してみてください。次に、キャッシュからそのリソースを削除し、もう一度fetchを実行してみてください。この場合、SWがネットワークリクエストを送信していることがわかります。

PWA開発ツール

おめでとうございます!Service Workerの理論的な基礎を習得し、キャッシュ処理、fetchリクエストのインターセプト、ワーカーとクライアントとの連携を実際に確認できました。今後の記事で扱うより複雑なシナリオの実装の基礎を築きました。一度にすべての情報を覚えるのは難しいでしょう。必要に応じて、この記事を何度か読み返して、コードを書いて知識を確認してください。

Service Worker APIは、受信リクエストの処理方法を開発者が完全に制御できるようにします。そのため、多くの開発者はこのAPIの使用に難しさを感じています。一般的なワークフローを実装するには、かなりの量のコードを書く必要があります。このような制御レベルはライブラリの作成には適していますが、実際の開発ではあまり良い結果を示しません。

現在、PWA開発を簡素化するいくつかのライブラリがありますが、それらのすべては、Googleによって開発されたWorkbox を基盤としています。このライブラリの主要な動作原理を理解することで、Workboxに基づいて開発されたあらゆるライブラリを問題なく使用できます。PWAの設定を細かい部分まで行う必要のない開発者は、Workbox CLIの使用を便利に感じるでしょう。最大限の自由度が必要な開発者も失望することはありません。Workboxを使用すると、実際に説明したコードは10行にも満たなくなります。

興味がありますか?次の記事では、Workboxを詳しく見ていきます。

その間、一息ついて、新しい情報を整理してください。次の記事でお会いしましょう!


こちらもご覧ください

  • PWA Builder - Microsoftのプロジェクトです。使いやすく、UIにはWebコンポーネントが使用されていますが、React、Vue、Angularなどのフレームワークを使用することはできません。2019年以降、大きなアップデートはありません。
  • Progressier - PWAを設定できるノーコードツールです。有望に見えますが、完全に有料で、詳細な情報を得るのは難しいです。
Sun* Developers

Discussion