最新のブラウザ向けに構築し、2003 年と同様に段階的に機能を強化
2003 年 3 月、Nick Finck と Steve Champeon は、プログレッシブ エンハンスメントのコンセプトでウェブデザインの世界を驚かせました。これはウェブデザインの戦略で、まず主要なウェブページのコンテンツを最初に読み込み、それから次々と高度に微妙な、技術的に厳密なコンテンツおよび機能のレイヤを追加するものです。2003 年の段階的な拡張は、当時は最新の CSS 機能、目立たない JavaScript、さらには拡張可能なベクター グラフィックのみを使用して拡張することでした。2020 年以降のプログレッシブ エンハンスメントでは、最新のブラウザ機能を使用します。
最新の JavaScript
JavaScript について言えば、最新のコア ES 2015 JavaScript 機能のブラウザ サポート状況は良好です。新しい標準には、Promise、モジュール、クラス、テンプレート リテラル、アロー関数、let
と const
、デフォルト パラメータ、ジェネレータ、分解代入、REST とスプレッド、Map
/Set
、WeakMap
/WeakSet
などが含まれています。すべてサポートされています。
非同期関数は ES 2017 の機能で、個人的にも気に入っている機能のひとつです。すべての主要ブラウザで使用できます。async
キーワードと await
キーワードを使用すると、非同期の Promise ベースの動作をよりクリーンなスタイルで記述できるため、Promise チェーンを明示的に構成する必要がなくなります。
また、オプション チェーンや 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
サンプルアプリ: Fugu Greetings
この記事では、Fugu Greetings(GitHub)というシンプルな PWA を使用します。このアプリの名前は、Android / iOS / パソコン アプリケーションのすべての機能をウェブに提供するためのプロジェクトである Project Fugu 🐡 に敬意を表しています。このプロジェクトについて詳しくは、ランディング ページをご覧ください。
Fugu Greetings は、仮想のグリーティング カードを作成して大切な人に送信できる描画アプリです。これは、PWA のコアコンセプトの例です。信頼性が高く、完全にオフラインで使用できるため、ネットワークがなくても使用できます。また、デバイスのホーム画面にインストールすることもできます。スタンドアロン アプリケーションとしてオペレーティング システムとシームレスに統合されます。
プログレッシブ エンハンスメント
では、プログレッシブ エンハンスメントについて説明します。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 のネットワーク タブは次のとおりです。
ただし、API をサポートするブラウザである Chrome では、新しいスクリプトのみ読み込まれます。これは、すべての最新ブラウザがサポートしている動的 import()
によって、エレガントに実現できます。先ほどお伝えしたとおり、最近は芝生がとても緑色です。
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 のプログレッシブ エンハンスメントを使用すると、以前と同じようにファイルを開くことができます。インポートしたファイルがキャンバスに直接描画されます。編集を加えたら、実際の保存ダイアログ ボックスで保存できます。このダイアログ ボックスで、ファイルの名前と保存場所を選択できます。 これで、ファイルを永久に保存する準備が整いました。
Web Share API と Web Share Target API
保存するだけでなく、実際にグリーティング カードを共有したい場合。 これは、Web Share API と Web Share Target API で実現できます。モバイル オペレーティング システム、最近ではデスクトップ オペレーティング システムにも、共有メカニズムが組み込まれています。以下は、私のブログの記事からトリガーされた、macOS 上のデスクトップ Safari の共有シートの例です。[記事を共有] ボタンをクリックすると、記事へのリンクを友だちと共有できます(例: macOS メッセージ アプリ経由)。
これを実現するコードは非常に簡単です。navigator.share()
を呼び出し、オブジェクトでオプションの title
、text
、url
を渡します。では、画像を添付したい場合はどうすればよいでしょうか?これは、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
配列と、次に title
と text
からなる 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 を選択すると、画像が添付されたメール作成ウィジェットがポップアップ表示されます。
Contact Picker API
次に、デバイスのアドレス帳、または連絡先管理ツールアプリである連絡先について説明します。グリーティング カードを書くときに、人の名前を正確に書き込むことは必ずしも容易ではありません。たとえば、私の友人の Sergey は、自分の名前をキリル文字で表記することを希望しています。ドイツ語の QWERTZ キーボードを使用しているので、名前の入力方法がわかりません。これは、Contact Picker API で解決できる問題です。スマートフォンの連絡帳アプリに友人を保存しているため、Contacts Picker API を使用して、ウェブから連絡先にアクセスできます。
まず、アクセスするプロパティのリストを指定する必要があります。この場合は名前のみを取得しますが、他のユースケースでは、電話番号、メールアドレス、アバター アイコン、住所を取得することもあります。次に、options
オブジェクトを構成し、multiple
を true
に設定して、複数のエントリを選択できるようにします。最後に、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 人の親友である Сергей Михайлович Брин と 劳伦斯·爱德华·"拉里"·佩奇 を選択すると、連絡先選択ツールに名前のみが表示され、メールアドレスや電話番号などの情報は表示されません。名前がグリーティング カードに描かれます。
非同期クリップボード 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 アプリから、クリップボード上のテキストと画像をアプリに表示することを許可するかどうかを尋ねられます。
最後に、権限を承認すると、画像がアプリケーションに貼り付けられます。逆の場合も同様です。グリーティング カードをクリップボードにコピーします。プレビューを開き、[ファイル]、[クリップボードから新規] の順にクリックすると、グリーティング カードが新しいタイトルなしの画像に貼り付けられます。
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 になっています。
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 によって毎日更新されます。
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 でスケジュール設定された通知がトリガーされると、他の通知と同様に表示されますが、前述のようにネットワーク接続は必要ありません。
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] チェックボックスがあり、オンにすると画面がスリープ状態にならないように設定できます。
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] チェックボックスがオンになっていて、ユーザーが長時間アイドル状態になると、キャンバスが消去されます。
結びの言葉
ふう、すごい経験ですね。たった 1 つのサンプルアプリにこれほど多くの API が含まれています。また、ブラウザがサポートしていない機能のダウンロード料金をユーザーに請求することは決してありません。プログレッシブ エンハンスメントを使用すると、関連するコードのみが読み込まれます。HTTP/2 ではリクエストが低コストであるため、このパターンは多くのアプリケーションで適切に機能します。ただし、非常に大きなアプリの場合はバンドルツールの使用を検討してください。
すべてのプラットフォームがすべての機能をサポートしているわけではないため、ブラウザによってアプリの外観が若干異なる場合がありますが、コア機能は常に存在し、特定のブラウザの機能に応じて段階的に強化されます。なお、これらの機能は、アプリがインストール済みアプリとして実行されているか、ブラウザのタブで実行されているかによって、同じブラウザでも変わる可能性があります。
Fugu Greetings アプリに興味をお持ちの場合は、GitHub でフォークしてください。
Chromium チームは、高度な Fugu API のリリース時に芝を緑にすることに全力で取り組んでいます。アプリの開発でプログレッシブ エンハンスメントを適用することで、すべてのユーザーに優れたベースライン エクスペリエンスを提供しながら、より多くのウェブ プラットフォーム API をサポートするブラウザを使用しているユーザーには、さらに優れたエクスペリエンスを提供できます。アプリのプログレッシブ エンハンスメントについて、皆様がどんなことを成し遂げるのか楽しみにしています。
謝辞
Fugu Greetings に貢献してくれた Christian Liebel と Hemanth HM に感謝します。この記事は、Joe Medley と Kayce Basques によってレビューされました。Jake Archibald は、サービス ワーカーのコンテキストで動的 import()
の状況を把握するのに役立ちました。