プログレッシブ ウェブアプリを段階的に強化する

最新のブラウザ向けに構築し、2003 年と同じように段階的に機能を強化

2003 年 3 月、Nick FinckSteve Champeon は、プログレッシブ エンハンスメントのコンセプトでウェブデザインの世界に衝撃を与えました。プログレッシブ エンハンスメントとは、コア ウェブページ コンテンツを最初に読み込むことを重視し、コンテンツの上に微妙な表現と技術的に厳密なレイヤを徐々に追加するというウェブデザイン戦略です。2003 年の段階的な機能強化とは、当時としては最新の CSS 機能、邪魔にならない JavaScript、さらには Scalable Vector Graphics のみを使用することでした。2020 年以降の段階的なエンハンスメントでは、最新のブラウザ機能を使用します。

漸進型の機能強化により、将来を見据えた包括的なウェブデザインを実現。Finck と Champeon の元のプレゼンテーションからのタイトルのスライド。
スライド: プログレッシブ エンハンスメントで将来を見据えたインクルーシブなウェブデザイン。 (ソース

最新の JavaScript

JavaScript といえば、ES 2015 の最新のコア JavaScript 機能がブラウザでサポートされているかどうかはとても便利です。新しい標準には、Promise、モジュール、クラス、テンプレート リテラル、アロー関数、letconst、デフォルト パラメータ、ジェネレータ、分解割り当て、レストとスプレッド、Map/SetWeakMap/WeakSet などが含まれます。すべてサポートされています

すべての主要ブラウザでのサポート状況を示す ES6 機能に関する CanIUse のサポート表。
ECMAScript 2015(ES6)ブラウザ サポートの表。(ソース

非同期関数は、ES 2017 の機能であり、私が個人的に気に入っているもので、すべての主要なブラウザで使用できますasync キーワードと await キーワードを使用すると、よりすっきりとしたスタイルで非同期の Promise ベースの動作を記述できるため、Promise チェーンを明示的に構成する必要がなくなります。

すべての主要ブラウザでの非同期関数のサポート状況を示す、CanIUse のサポート表。
非同期関数のブラウザ サポート表。(ソース

また、オプション チェーンnull 結合など、ごく最近追加された ES 2020 言語でも、すぐにサポートに達しました。コードサンプルは次のとおりです。 JavaScript のコア機能に関して言えば、草が今ほど環境に優しいことはなかったでしょう。

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
Windows XP の象徴的な緑の草の背景画像。
緑がかった部分は、JavaScript のコア機能を示しています。 (権限で使用する Microsoft 製品のスクリーンショット)

サンプルアプリ: Fugu Greetings

この記事では、Fugu GreetingsGitHub)というシンプルな PWA を使用します。このアプリの名前は Project Fugu 🐡? の一環です。Project Fugu 🐡? は、Android/iOS/デスクトップ アプリのすべての機能をウェブで利用できるようにする取り組みです。プロジェクトの詳細については、ランディング ページをご覧ください。

Fugu Greetings は、バーチャルなグリーティング カードを作成して大切な人に送ることができる描画アプリです。PWA の基本コンセプトを説明しています。信頼性が高く、完全にオフラインに対応しているため、ネットワークがない場合でも使用できます。また、デバイスのホーム画面にインストール可能で、スタンドアロン アプリケーションとしてオペレーティング システムとシームレスに統合されます。

PWA コミュニティのロゴに似たイラストを含む Fugu Greetings PWA。
Fugu Greetings サンプルアプリ。

プログレッシブ エンハンスメント

以上は以上です。次に、プログレッシブ エンハンスメントについて説明します。MDN ウェブ ドキュメント用語集では、このコンセプトを次のように定義しています。

プログレッシブ エンハンスメントとは、重要なコンテンツと機能のベースラインを可能な限り多くのユーザーに提供しつつ、必要なコードをすべて実行できる最新のブラウザを使用しているユーザーにのみ最善のエクスペリエンスを提供するという設計理念のことです。

通常、機能検出は、ブラウザの最新機能に対応できるかどうかを判断するために使用され、ポリフィルは JavaScript で不足している機能を追加するために使用されます。

[…]

プログレッシブ エンハンスメントは、ウェブ デベロッパーが複数の未知のユーザー エージェントで動作するウェブサイトの開発に集中できる便利な手法です。グレースフル デグラデーションは関連していますが、同じものではなく、多くの場合、プログレッシブ エンハンスメントとは逆方向であるとみなされます。実際には、どちらのアプローチも有効であり、しばしば相互に補完し合うことができます。

MDN のコントリビューター

それぞれのグリーティング カードを一から作成するのはかなり面倒な作業です。 では、ユーザーが画像をインポートして、そこから開始する機能を用意してはどうでしょうか。従来のアプローチでは、<input type=file> 要素を使用してこれを実現していました。まず、要素を作成して、その type'file' に設定し、MIME タイプを accept プロパティに追加します。その後、プログラムで「クリック」して変更をリッスンします。画像を選択すると、キャンバスに直接インポートされます。

const importImage = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

インポート機能がある場合、ユーザーがグリーティング カードをローカルに保存できるように、エクスポート機能が用意されているはずです。importimportファイルを保存する従来の方法は、download 属性を使用し、blob URL を href としてアンカーリンクを作成することです。また、プログラムで「クリック」してダウンロードをトリガーします。また、メモリリークを防ぐために、blob オブジェクトの URL は必ず無効にしてください。

const exportImage = async (blob) => {
  const a = document.createElement('a');
  a.download = 'fugu-greeting.png';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

でも、ちょっと待って。グリーティング カードを「ダウンロード」しているのではなく、「保存」しているということです。ファイルの保存先を選択できる「保存」ダイアログを表示する代わりに、ブラウザはユーザーの操作なしでグリーティング カードを直接ダウンロードし、[ダウンロード] フォルダに直接格納しています。うまくいかないね。

もっと良い方法があればいいと思いませんか? ローカル ファイルを開いて編集し、新しいファイルに保存したり、最初に開いた元のファイルに戻したりできるとしたらどうでしょうか。File System Access API を使用すると、ファイルやディレクトリを開いたり、作成したり、変更、保存したりできます。

API の特徴を検出するにはどうすればよいですか?File System Access API は、新しいメソッド window.chooseFileSystemEntries() を公開します。そのため、このメソッドを使用できるかどうかに応じて、条件付きでインポート モジュールとエクスポート モジュールを読み込む必要があります。その方法を以下に示します。

const loadImportAndExport = () => {
  if ('chooseFileSystemEntries' in window) {
    Promise.all([
      import('./import_image.mjs'),
      import('./export_image.mjs'),
    ]);
  } else {
    Promise.all([
      import('./import_image_legacy.mjs'),
      import('./export_image_legacy.mjs'),
    ]);
  }
};

File System Access API の詳細に入る前に 段階的な拡張パターンを簡単に紹介します現在 File System Access API をサポートしていないブラウザでは、以前のスクリプトを読み込みます。Firefox と Safari のネットワークタブは以下で確認できます。

Safari ウェブ インスペクタで以前のファイルが読み込まれる様子。
Safari Web Inspector の [ネットワーク] タブ。
以前のファイルが読み込まれる Firefox デベロッパー ツール。
Firefox デベロッパー ツールの [ネットワーク] タブ。

ただし、API をサポートするブラウザである Chrome では、新しいスクリプトのみが読み込まれます。これは、最新のすべてのブラウザでサポートされている動的な import() のおかげで簡単に実現できます。先ほど言ったように、最近は草がかなり緑がかったです。

最新のファイルが読み込まれる様子を示している Chrome DevTools。
Chrome DevTools の [ネットワーク] タブ。

File System Access API

ここまでで、File System Access API に基づく実際の実装について説明します。画像をインポートするには、window.chooseFileSystemEntries() を呼び出して、画像ファイルを指定する accepts プロパティを渡します。ファイル拡張子と MIME タイプの両方がサポートされています。これによりファイル ハンドルが作成され、このハンドルから getFile() を呼び出すことで実際のファイルを取得できます。

const importImage = async () => {
  try {
    const handle = await window.chooseFileSystemEntries({
      accepts: [
        {
          description: 'Image files',
          mimeTypes: ['image/*'],
          extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'],
        },
      ],
    });
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

イメージのエクスポートはほぼ同じですが、今回は 'save-file' の型パラメータを chooseFileSystemEntries() メソッドに渡す必要があります。すると、ファイル保存ダイアログが表示されます。 ファイル オープンの場合は、'open-file' がデフォルトですので、これは必要ありません。先ほどと同じように accepts パラメータを設定しますが、今回は PNG 画像のみに限定します。ここでもファイル ハンドルが返されますが、今回はファイルを取得する代わりに createWritable() を呼び出して書き込み可能なストリームを作成します。次に、blob(グリーティング カードの画像)をファイルに書き込みます。最後に、書き込み可能なストリームを閉じます。

常に失敗する可能性があります。ディスクの容量が不足している、書き込みエラーや読み取りエラーが発生した、ユーザーがファイル ダイアログをキャンセルしたなどが考えられます。そのため、常に try...catch ステートメントで呼び出しをラップしています。

const exportImage = async (blob) => {
  try {
    const handle = await window.chooseFileSystemEntries({
      type: 'save-file',
      accepts: [
        {
          description: 'Image file',
          extensions: ['png'],
          mimeTypes: ['image/png'],
        },
      ],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

File System Access API でプログレッシブエンハンスメントを使用すると 以前と同じようにファイルを開くことができますインポートしたファイルがキャンバスに直接描画されます。編集を加えたら、あとは実際の保存ダイアログ ボックスで保存します。このダイアログ ボックスでは、ファイルの名前と保存場所を選択できます。これで、ファイルを永続的に保存できるようになりました。

ファイルを開くダイアログが表示された Fugu Greetings アプリ。
ファイルを開くダイアログ。
Fugu Greetings アプリに画像がインポートされました。
インポートしたイメージ。
画像を変更した Fugu Greetings アプリ。
変更した画像を新しいファイルに保存します。

Web Share API と Web Share Target API

永遠に保存するどころか、実際にグリーティング カードを共有したいと思っているかもしれません。これは、Web Share APIWeb Share Target API によって実現できます。モバイルや最近のデスクトップ オペレーティング システムに、組み込みの共有メカニズムが導入されました。たとえば、私のブログの記事からトリガーされた、macOS のパソコン Safari の共有シートを以下に示します。[Share Article] ボタンをクリックすると、macOS メッセージ アプリなどを使用して、記事へのリンクを友だちと共有できます。

記事の [共有] ボタンから起動された macOS のパソコン Safari の共有シート
macOS 上のパソコンの Safari の Web Share API

これを実現するコードは、非常に簡単です。navigator.share() を呼び出し、オブジェクトでオプションの titletexturl を渡します。では、画像を添付したい場合はどうすればよいでしょうか。Web Share API レベル 1 では、まだこの機能はサポートされていません。 Web Share Level 2 にはファイル共有機能が追加されています。

try {
  await navigator.share({
    title: 'Check out this article:',
    text: `"${document.title}" by @tomayac:`,
    url: document.querySelector('link[rel=canonical]').href,
  });
} catch (err) {
  console.warn(err.name, err.message);
}

Fugu グリーティング カード アプリケーションを使用してこれを動作させる方法を紹介します。まず、1 つの blob から成る files 配列を持つ data オブジェクトを準備し、次に titletext を用意する必要があります。次に、ベスト プラクティスとして、新しい navigator.canShare() メソッドを使用します。このメソッドは、その名前が示すとおり、共有しようとしている data オブジェクトがブラウザによって技術的に共有できるかどうかを示します。navigator.canShare() からデータが共有可能であると通知されたら、以前と同様に navigator.share() を呼び出すことができます。すべてが失敗する可能性があるため、ここでも try...catch ブロックを使用しています。

const share = async (title, text, blob) => {
  const data = {
    files: [
      new File([blob], 'fugu-greeting.png', {
        type: blob.type,
      }),
    ],
    title: title,
    text: text,
  };
  try {
    if (!(navigator.canShare(data))) {
      throw new Error("Can't share data.", data);
    }
    await navigator.share(data);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

以前と同様に、プログレッシブ エンハンスメントを使用します。'share''canShare' の両方が navigator オブジェクトに存在する場合のみ、動的 import() を介して share.mjs を読み込みます。Mobile Safari などの 2 つの条件のいずれかしか満たさないブラウザでは、この機能は読み込みません。

const loadShare = () => {
  if ('share' in navigator && 'canShare' in navigator) {
    import('./share.mjs');
  }
};

Fugu Greetings では、Android 版 Chrome などの対応ブラウザで [共有] ボタンをタップすると、組み込みの共有シートが開きます。たとえば Gmail を選択すると、画像を添付したメール作成ウィジェットがポップアップ表示されます。

画像を共有するさまざまなアプリが表示されている OS レベルの共有シート。
ファイルを共有するアプリを選択する。
画像が添付された Gmail のメール作成ウィジェット。
ファイルが Gmail の作成ツールで新しいメールに添付されます。

Contact Picker API

次に、連絡先について説明します。連絡先とは、デバイスのアドレス帳や連絡先管理ツールです。グリーティング カードを作成するとき、相手の名前を正しく入力するのは必ずしも容易ではありません。たとえば、キリル文字で表記することを好む友人の Sergey がいます。ドイツ語の QWERTZ キーボードを使用していますが、名前の入力方法がわかりません。この問題は、Contact Picker API で解決できます。友人をスマートフォンの連絡帳アプリに保存しているので Contacts Picker API 経由でウェブから連絡先にアクセスできます

まず、アクセスするプロパティのリストを指定する必要があります。このケースでは名前のみが必要ですが、他のユースケースでは電話番号、メール、アバター アイコン、住所などが必要になる可能性があります。次に、複数のエントリを選択できるように、options オブジェクトを構成し、multipletrue に設定します。最後に、navigator.contacts.select() を呼び出します。これにより、ユーザーが選択した連絡先に必要なプロパティが返されます。

const getContacts = async () => {
  const properties = ['name'];
  const options = { multiple: true };
  try {
    return await navigator.contacts.select(properties, options);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

「API が実際にサポートされているときだけ ファイルを読み込む」というパターンはおわかりいただけたと思います

if ('contacts' in navigator) {
  import('./contacts.mjs');
}

Fugu Greeting で [連絡先] ボタンをタップして、親友である 2 人の親友、scер}{/ей сийловий Брин劳伦どおりそう爱德お使い·"私里"·佩奇の 2 人の連絡先を選択すると、連絡先のメールアドレスやその他のメールアドレスの情報のみが表示されますが、電話番号などのメールアドレスが表示されます。その人たちの名前がグリーティング カードに書かれています。

アドレス帳の 2 件の連絡先の名前が表示されている連絡先選択ツール。
アドレス帳から連絡先選択ツールで 2 つの名前を選択する。
グリーティング カードに描画された、以前に選択した 2 つの連絡先の名前。
2 つの名前がグリーティング カードに表示されます。

非同期クリップボード API

次はコピーと貼り付けですソフトウェア デベロッパーにおすすめしたいオペレーションの 1 つは、コピー&ペーストです。グリーティング カードの作成者として、同じようにしたい場合があります。作業中のグリーティング カードに画像を貼り付けるか、グリーティング カードをコピーして別の場所から編集を続けることをおすすめします。Async Clipboard API はテキストと画像の両方をサポートします。Fugu Greetings アプリに、コピーと貼り付けのサポートを追加した方法をご説明します。

システムのクリップボードに何かをコピーするには、そこに書き込む必要があります。 navigator.clipboard.write() メソッドは、クリップボード アイテムの配列をパラメータとして受け取ります。クリップボードの各アイテムは、基本的には blob を値として持つオブジェクトであり、blob の型をキーとして使用します。

const copy = async (blob) => {
  try {
    await navigator.clipboard.write([
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

貼り付けるには、navigator.clipboard.read() を呼び出して取得したクリップボード アイテムをループする必要があります。これは、複数のクリップボード アイテムが異なる表現でクリップボード上に存在する可能性があるためです。クリップボードの各アイテムには、使用可能なリソースの MIME タイプを示す types フィールドがあります。クリップボード アイテムの getType() メソッドを呼び出して、前に取得した MIME タイプを渡します。

const paste = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      try {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          return blob;
        }
      } catch (err) {
        console.error(err.name, err.message);
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
};

今では何も言わなくても大丈夫です。対応ブラウザを使用しているときのみ対応しています。

if ('clipboard' in navigator && 'write' in navigator.clipboard) {
  import('./clipboard.mjs');
}

では、これは実際にどのように機能するのでしょうか。macOS プレビュー アプリで画像を開き、クリップボードにコピーしています。[Paste] をクリックすると、Fugu Greetings アプリから、クリップボード上のテキストと画像の表示をアプリに許可するかどうかを尋ねられます。

クリップボードの権限プロンプトを表示する Fugu Greetings アプリ。
クリップボードの権限プロンプト。

最後に、権限を許可した後、画像がアプリに貼り付けます。その逆も同様です。グリーティング カードをクリップボードにコピーします。 次に [プレビュー] を開いて [ファイル]、[クリップボードから新規作成] の順にクリックすると、グリーティング カードが新しい無題の画像に貼り付けられます。

無題の画像を貼り付けたばかりの macOS プレビュー アプリ。
macOS プレビュー アプリに貼り付けられた画像

Badging API

もう 1 つの便利な API は、Badging API です。インストール可能な PWA である Fugu Greetings には、アプリドックやホーム画面に配置できるアプリアイコンが用意されています。API を楽しく簡単にデモンストレーションする方法として、Fugu Greetings でペン ストローク カウンタとして(悪用)する方法があります。pointerdown イベントが発生するたびにペンのストローク カウンタをインクリメントし、更新されたアイコンバッジを設定するイベント リスナーを追加しました。キャンバスがクリアされると、カウンタがリセットされ、バッジが削除されます。

let strokes = 0;

canvas.addEventListener('pointerdown', () => {
  navigator.setAppBadge(++strokes);
});

clearButton.addEventListener('click', () => {
  strokes = 0;
  navigator.setAppBadge(strokes);
});

この機能は段階的に強化されるため、読み込みのロジックは通常どおりです。

if ('setAppBadge' in navigator) {
  import('./badge.mjs');
}

この例では、数字 1 本につきペンの 1 ストロークで 1 から 7 までの数字を描画します。アイコンのバッジカウンタが 7 になりました。

グリーティング カードに 1 ~ 7 の数字がペンで 1 ストロークずつ描かれている。
7 本のペン ストロークを使って 1 から 7 までの数字を描きます。
数字の 7 が表示されている Fugu Greetings アプリのバッジアイコン。
アプリアイコン バッジ形式のペンのストローク カウンタ。

Periodic Background Sync API

新しい一日を新しいスタートにしませんか? Fugu Greetings アプリの優れた機能は、グリーティング カードを開始するための新しい背景画像を毎朝表示する機能です。アプリは、Periodic Background Sync API を使用してこれを実現します。

最初のステップは、Service Worker の登録に定期的な同期イベントをregisterすることです。'image-of-the-day' という同期タグをリッスンします。最小間隔は 1 日であるため、ユーザーは 24 時間ごとに新しい背景画像を取得できます。

const registerPeriodicBackgroundSync = async () => {
  const registration = await navigator.serviceWorker.ready;
  try {
    registration.periodicSync.register('image-of-the-day-sync', {
      // An interval of one day.
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch (err) {
    console.error(err.name, err.message);
  }
};

2 つ目のステップでは、Service Worker で periodicsync イベントをリッスンします。イベントタグが 'image-of-the-day'(以前に登録されたタグ)の場合、その日の画像が getImageOfTheDay() 関数で取得され、結果がすべてのクライアントに反映され、クライアントがキャンバスとキャッシュを更新できるようになります。

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        const blob = await getImageOfTheDay();
        const clients = await self.clients.matchAll();
        clients.forEach((client) => {
          client.postMessage({
            image: blob,
          });
        });
      })()
    );
  }
});

繰り返しになりますが、これは段階的な機能強化であるため、コードは API がブラウザでサポートされている場合にのみ読み込まれます。これは、クライアント コードと Service Worker コードの両方に当てはまります。対応していないブラウザでは、どちらも読み込まれません。 Service Worker では、動的な import()(Service Worker のコンテキストではまだサポートされていない)ではなく、従来の importScripts() を使用しています。

// In the client:
const registration = await navigator.serviceWorker.ready;
if (registration && 'periodicSync' in registration) {
  import('./periodic_background_sync.mjs');
}
// In the service worker:
if ('periodicSync' in self.registration) {
  importScripts('./image_of_the_day.mjs');
}

Fugu Greetings で [Wallpaper] ボタンを押すと、その日のグリーティング カードの画像が表示されます。この画像は、Periodic Background Sync API を介して毎日更新されます。

その日の新しいグリーティング カードの画像を表示する Fugu Greetings アプリ。
[壁紙] ボタンを押すと、その日の画像が表示されます。

通知トリガー API

たとえインスピレーションがたくさんあっても、開始したグリーティング カードを仕上げるきっかけが必要になることもあります。これは、Notification Triggers API で有効になる機能です。ユーザーは、グリーティング カードを完了するために案内される時間を入力できます。その時間になると、グリーティング カードが待っているという通知が表示されます。

ターゲット時刻の入力を求められたら、アプリケーションは showTrigger を使用して通知をスケジュール設定します。TimestampTrigger には、以前に選択した対象日付を指定できます。リマインダー通知はローカルでトリガーされるため、ネットワークやサーバー側は必要ありません。

const targetDate = promptTargetDate();
if (targetDate) {
  const registration = await navigator.serviceWorker.ready;
  registration.showNotification('Reminder', {
    tag: 'reminder',
    body: "It's time to finish your greeting card!",
    showTrigger: new TimestampTrigger(targetDate),
  });
}

これまで説明してきた他のものと同様に、これは段階的な拡張であるため、コードは条件付きでのみ読み込まれます。

if ('Notification' in window && 'showTrigger' in Notification.prototype) {
  import('./notification_triggers.mjs');
}

Fugu Greetings の [リマインダー] チェックボックスをオンにすると、グリーティング カードの作成を完了するリマインダーをいつ受け取るかを確認するメッセージが表示されます。

Fugu Greetings アプリで、いつグリーティング カードを終わらせるようリマインダーを受け取るかを確認するプロンプトが表示されている。
グリーティング カードの最後にリマインドするようなローカル通知のスケジュールを設定します。

スケジュール設定された通知が Fugu Greetings でトリガーされると、他の通知と同様に表示されますが、前述のように、ネットワーク接続は必要ありません。

Fugu Greetings からトリガーされた通知を表示している macOS 通知センター。
トリガーされた通知が macOS 通知センターに表示されます。

Wake Lock API

Wake Lock API も含めたいと思っています。ときには、インスピレーションがキスするまで画面を見つめ続けるだけでよいこともあります。最悪のケースは、画面がオフになることです。Wake Lock API を使用すると、このような事態を防ぐことができます。

まず、navigator.wakelock.request method() を使用して wake lock を取得します。文字列 'screen' を渡して画面の wake lock を取得します。次に、wake lock が解放されたときに通知されるようにイベント リスナーを追加します。 これは、タブの公開設定が変更された場合などに発生することがあります。 その場合は、タブが再び表示されたときに wake lock を再取得してください。

let wakeLock = null;
const requestWakeLock = async () => {
  wakeLock = await navigator.wakeLock.request('screen');
  wakeLock.addEventListener('release', () => {
    console.log('Wake Lock was released');
  });
  console.log('Wake Lock is active');
};

const handleVisibilityChange = () => {
  if (wakeLock !== null && document.visibilityState === 'visible') {
    requestWakeLock();
  }
};

document.addEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('fullscreenchange', handleVisibilityChange);

はい。これは段階的な機能強化であるため、ブラウザが API をサポートしている場合にのみ読み込む必要があります。

if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
  import('./wake_lock.mjs');
}

Fugu Greetings には [Insomnia] チェックボックスがあり、オンにすると画面がスリープ解除されます。

[不眠症] チェックボックスがオンになっている場合、画面は起動したままになります。
[Insomnia] チェックボックスをオンにすると、アプリは起動したままになります。

Idle Detection API

ときには、何時間も画面を見つめていても、グリーティング カードをどう使うべきかまったく思いつかないこともあります。Idle Detection API を使用すると、アプリでユーザーのアイドル時間を検出できます。ユーザーが長時間アイドル状態になると、アプリは初期状態にリセットされ、キャンバスがクリアされます。 この API は現在、通知権限によって制限されています。本番環境のアイドル検出のユースケースの多くは通知に関連しています(たとえば、ユーザーが現在使用しているデバイスにのみ通知を送信する場合など)。

通知権限が付与されていることを確認した後、アイドル検出機能をインスタンス化します。ユーザーと画面の状態など、アイドル状態の変更をリッスンするイベント リスナーを登録します。 ユーザーはアクティブでもアイドル状態でも、画面はロック解除またはロックできます。ユーザーがアイドル状態になると、キャンバスがクリアされます。 アイドル検出のしきい値は 60 秒に設定します。

const idleDetector = new IdleDetector();
idleDetector.addEventListener('change', () => {
  const userState = idleDetector.userState;
  const screenState = idleDetector.screenState;
  console.log(`Idle change: ${userState}, ${screenState}.`);
  if (userState === 'idle') {
    clearCanvas();
  }
});

await idleDetector.start({
  threshold: 60000,
  signal,
});

いつものように、このコードはブラウザでサポートされている場合にのみ読み込みます。

if ('IdleDetector' in window) {
  import('./idle_detection.mjs');
}

Fugu Greetings アプリでは、[Ephemeral] チェックボックスがオンになり、ユーザーが長時間アイドル状態になると、キャンバスがクリアされます。

ユーザーが長時間アイドル状態になった後、キャンバスがクリアされた Fugu Greetings アプリ。
[エフェメラル] チェックボックスがオンになっていて、ユーザーが長時間アイドル状態になっていると、キャンバスがクリアされます。

結び

それはすごいですね。1 つのサンプルアプリで多数の API を利用できます ブラウザがサポートしていない機能のダウンロード費用を ユーザーに支払う必要はありませんプログレッシブ エンハンスメントを使用することで、関連するコードだけが読み込まれるようにしています。HTTP/2 ではリクエストが安価であるため、このパターンは多くのアプリケーションに適していますが、非常に大規模なアプリではバンドラを検討することをおすすめします。

現在のブラウザでサポートされているコードを含むファイルのリクエストのみが表示されている Chrome DevTools の [Network] パネル。
Chrome DevTools の [Network] タブ。現在のブラウザでサポートされているコードを含むファイルのリクエストのみが表示されます。

すべてのプラットフォームがすべての機能をサポートしているわけではないため、ブラウザによってアプリの外観が若干異なる場合がありますが、コア機能は常に存在し、特定のブラウザの機能に応じて段階的に拡張されます。これらの機能は、アプリがインストール済みのアプリとして実行されているか、ブラウザタブで実行されているかによって、同じブラウザで変更される場合があります。

Android Chrome で実行されている Fugu Greetings。利用可能な機能の多くを示している。
Android Chrome で実行されている Fugu Greetings
パソコンの Safari で動作している Fugu Greetings で、利用できる機能が限定されています。
パソコンの Safari で実行されている Fugu Greetings
パソコンの Chrome で動作している Fugu Greetings。利用可能な機能の多くを示しています。
パソコンの Chrome で実行されている Fugu Greetings

Fugu Greetings アプリに興味をお持ちの場合は、GitHub でフォークしてください。

GitHub の Fugu Greetings リポジトリ。
GitHub の Fugu Greetings アプリ。

Chromium チームは、高度な Fugu API に関して、草地をより環境にやさしいものにすることに取り組んでいます。 アプリの開発に段階的なエンハンスメントを適用することで、すべてのユーザーにとって堅実で安定したエクスペリエンスを得ると同時に、より多くのウェブ プラットフォーム API をサポートするブラウザを使用するユーザーのエクスペリエンスはさらに向上させることができるのです。アプリの機能強化について皆様がお役に立てることを楽しみにしています。

謝辞

Fugu Greetings に協力してくれた Christian LiebelHemanth HM に感謝します。この記事は、Joe MedleyKayce Basques によってレビューされました。Jake Archibald は、Service Worker のコンテキストで動的 import() を使用する状況を見つけるのに役立ちました。