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

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

2003 年 3 月、Nick FinckSteve Champeon は、プログレッシブ エンハンスメントのコンセプトでウェブデザインの世界を驚かせました。これはウェブデザインの戦略で、まず主要なウェブページのコンテンツを最初に読み込み、それから次々と高度に微妙な、技術的に厳密なコンテンツおよび機能のレイヤを追加するものです。2003 年の段階的な拡張は、当時は最新の CSS 機能、目立たない JavaScript、さらには拡張可能なベクター グラフィックのみを使用して拡張することでした。2020 年以降のプログレッシブ エンハンスメントでは、最新のブラウザ機能を使用します。

漸進的な機能強化による将来を見据えたインクルーシブなウェブデザイン。フィンクとチャンピオンの元のプレゼンテーションのタイトル スライド。
スライド: プログレッシブ エンハンスメントによる未来に向けたインクルーシブなウェブデザイン。 (出典

最新の JavaScript

JavaScript について言えば、最新のコア ES 2015 JavaScript 機能のブラウザ サポート状況は良好です。新しい標準には、Promise、モジュール、クラス、テンプレート リテラル、アロー関数、letconst、デフォルト パラメータ、ジェネレータ、分解代入、REST とスプレッド、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 のコア機能に関しては、Google が優位です。 (権限とともに使用する Microsoft プロダクトのスクリーンショット)。

サンプルアプリ: Fugu Greetings

この記事では、Fugu GreetingsGitHub)というシンプルな PWA を使用します。このアプリの名前は、Android / iOS / パソコン アプリケーションのすべての機能をウェブに提供するためのプロジェクトである Project Fugu 🐡 に敬意を表しています。このプロジェクトについて詳しくは、ランディング ページをご覧ください。

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();
  });
};

インポート機能がある場合は、ユーザーがグリーティング カードをローカルに保存できるようにエクスポート機能も用意する必要があります。ファイルを保存する従来の方法では、download 属性と、href として blob URL を持つアンカーリンクを作成します。また、プログラムで「クリック」してダウンロードをトリガーし、メモリリークを防ぐために 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 ウェブ インスペクタの [ネットワーク] タブ。
以前のファイルが読み込まれている様子を示した 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 の共有シートの例です。[記事を共有] ボタンをクリックすると、記事へのリンクを友だちと共有できます(例: 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 Greeting card アプリケーションでこれを行う方法をご紹介します。 まず、1 つの blob から構成される files 配列と、次に titletext からなる data オブジェクトを準備する必要があります。次に、ベスト プラクティスとして、新しい 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 を読み込みます。2 つの条件のいずれかのみが満たされているモバイル Safari などのブラウザでは、機能を読み込みません。

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 人の親友である Сергей Михайлович Брин劳伦斯·爱德华·"拉里"·佩奇 を選択すると、連絡先選択ツールに名前のみが表示され、メールアドレスや電話番号などの情報は表示されません。名前がグリーティング カードに描かれます。

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

非同期クリップボード API

次は、コピーと貼り付けです。ソフトウェア デベロッパーにとって、コピーと貼り付けはよく使う操作の一つです。グリーティング カードの作成者として、同じことを行いたい場合もあります。作成中のグリーティング カードに画像を貼り付けたり、グリーティング カードをコピーして別の場所で編集を続けたりしたい場合があります。非同期クリップボード 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 フィールドがあります。前に取得した MIME タイプを渡して、クリップボード アイテムの getType() メソッドを呼び出します。

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 プレビュー アプリで画像を開き、クリップボードにコピーします。[貼り付け] をクリックすると、Fugu Greetings アプリから、クリップボード上のテキストと画像をアプリに表示することを許可するかどうかを尋ねられます。

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

最後に、権限を承認すると、画像がアプリケーションに貼り付けられます。逆の場合も同様です。グリーティング カードをクリップボードにコピーします。プレビューを開き、[ファイル]、[クリップボードから新規] の順にクリックすると、グリーティング カードが新しいタイトルなしの画像に貼り付けられます。

タイトルのない貼り付けたばかりの画像が表示された macOS プレビュー アプリ。
macOS プレビュー アプリに貼り付けられた画像。

Badging API

Badging API も便利な API です。インストール可能な PWA として、Fugu Greetings には、ユーザーがアプリドックまたはホーム画面に配置できるアプリアイコンがあります。Fugu Greetings で API をペンのストローク カウンターとして(悪用して)デモを行うのは、楽しく簡単な方法です。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~7 の数字を描画し、数字ごとに 1 本のペンストロークを描画しています。アイコンのバッジカウンターが 7 になっています。

1~7 の数字がグリーティング カードに描かれており、それぞれ 1 本のペンで描かれています。
7 本のペンストロークで 1~7 の数字を描画します。
番号 7 が表示された Fugu Greetings アプリのバッジ アイコン。
アプリアイコンのバッジ形式のペンストロークのカウンタ。

Periodic Background Sync API

毎日を新しいものにしましょう。 Fugu Greetings アプリの便利な機能として、毎朝新しい背景画像が表示され、グリーティング カードの作成をサポートします。アプリは Periodic Background Sync API を使用してこれを行います。

最初のステップは、Service Worker の登録で定期的な同期イベントを登録することです。'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 では、動的 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 によって毎日更新されます。

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

Notification Triggers 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 のメッセージカードで [リマインダー] チェックボックスをオンにすると、メッセージカードの作成を完了するためのリマインダーをいつ表示するかを尋ねるメッセージが表示されます。

Fugu Greetings アプリに、グリーティング カードの作成のリマインダーを受け取るタイミングを尋ねるプロンプトが表示されています。
グリーティング カードの作成を完了するためのリマインダーとしてローカル通知をスケジュール設定。

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

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

Wake Lock API

Wake Lock API も追加したいです。インスピレーションが降りてくるまで、画面をじっと見つめる必要があることもあります。最悪の場合、画面がオフになります。Wake Lock API を使用すると、これを防ぐことができます。

最初のステップは、navigator.wakelock.request method() を使用してウェイクロックを取得することです。文字列 'screen' を渡して、画面ウェイクロックを取得します。次に、ウェイクロックが解除されたときに通知されるようにイベント リスナーを追加します。これは、タブの公開設定が変更された場合などに発生することがあります。 この場合、タブが再び表示されたら、ウェイクロックを再取得できます。

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 は、サービス ワーカーのコンテキストで動的 import() の状況を把握するのに役立ちました。