アーキテクチャについて話しましょう。
今回は、重要でありながら誤解されがちなトピック、つまりウェブアプリに使用するアーキテクチャ、特にプログレッシブ ウェブアプリを構築する際にアーキテクチャの決定がどのように影響するかについて説明します。
「アーキテクチャ」という言葉は曖昧に聞こえることがあり、なぜこれが重要なのかがすぐにわからないことがあります。アーキテクチャについて考える 1 つの方法は、次のような質問を自分に投げかけることです。ユーザーがサイトのページにアクセスしたときに、どの HTML が読み込まれるか?そして、別のページにアクセスしたときに読み込まれるものは何ですか?
これらの質問に対する答えは必ずしも単純ではありません。プログレッシブ ウェブアプリについて考え始めると、さらに複雑になる可能性があります。そこで、私が効果的だと判断したアーキテクチャの 1 つをご紹介します。この記事では、プログレッシブ ウェブアプリを構築するための「私のアプローチ」として、私が行った決定にラベルを付けます。
独自の PWA を構築する際に、私の方法を自由に使用できますが、同時に、他にも有効な代替手段が常に存在します。すべての要素がどのように組み合わされているかを確認することで、皆様がインスピレーションを得て、ニーズに合わせてカスタマイズできるようになることを願っています。
Stack Overflow PWA
この記事に付随して、Stack Overflow PWA を構築しました。私は Stack Overflow を読んだり、投稿したりすることに多くの時間を費やしています。そこで、特定のトピックに関するよくある質問を簡単に閲覧できるウェブアプリを構築したいと考えました。これは、公開されている Stack Exchange API を基盤として構築されています。これはオープンソースです。詳細については、GitHub プロジェクトをご覧ください。
マルチページ アプリ(MPA)
具体的な説明に入る前に、いくつかの用語を定義し、基盤となるテクノロジーについて説明します。まず、私が「マルチページ アプリ」(MPA)と呼んでいるものについて説明します。
MPA は、ウェブの初期から使用されている従来のアーキテクチャの名称です。ユーザーが新しい URL に移動するたびに、ブラウザはそのページに固有の HTML を段階的にレンダリングします。ナビゲーション間でページの状態やコンテンツを保持しようとはしません。新しいページにアクセスするたびに、新しいセッションが開始されます。
これは、ウェブアプリを構築するためのシングルページ アプリ(SPA)モデルとは対照的です。SPA モデルでは、ユーザーが新しいセクションにアクセスすると、ブラウザが JavaScript コードを実行して既存のページを更新します。SPA と MPA はどちらも有効なモデルですが、この記事では、マルチページ アプリのコンテキストで PWA のコンセプトを検討します。
信頼性に優れた高速 Wi-Fi
私(および他の多くの人)が「プログレッシブ ウェブアプリ」(PWA)という言葉を使っているのを聞いたことがあるでしょう。このサイトの他の場所で、背景資料の一部をすでにご存じかもしれません。
PWA は、最高レベルのユーザー エクスペリエンスを提供し、ユーザーのホーム画面に配置する価値のあるウェブアプリと考えることができます。「FIRE」という頭字語は、Fast(高速)、Integrated(統合)、Reliable(信頼性)、Engaging(エンゲージメント)の略で、PWA を構築する際に考慮すべきすべての属性をまとめたものです。
この記事では、これらの属性のサブセットである「高速」と「信頼性」に焦点を当てます。
高速: 「高速」の意味はコンテキストによって異なりますが、ここでは、ネットワークからの読み込みを最小限に抑えることによる速度上のメリットについて説明します。
信頼性: しかし、単に高速なだけでは不十分です。PWA のような操作感を実現するには、ウェブアプリの信頼性を高める必要があります。ネットワークの状態に関係なく、常に何か(カスタマイズされたエラーページだけでも)を読み込めるだけの十分な復元力が必要です。
信頼性の高い高速性: 最後に、PWA の定義を少し言い換えて、信頼性の高い高速性を備えたものを構築することの意味を見ていきます。低レイテンシのネットワークに接続しているときにのみ高速で信頼性が高いだけでは不十分です。信頼性の高い高速性とは、基盤となるネットワークの状態に関係なく、ウェブアプリの速度が一貫していることを意味します。
実現を可能にするテクノロジー: Service Worker + Cache Storage API
PWA は、速度と復元力に関する高い基準を導入しています。幸いなことに、ウェブ プラットフォームには、そのようなパフォーマンスを実現するためのビルディング ブロックがいくつか用意されています。Service Worker と Cache Storage API のことです。
受信リクエストをリッスンし、一部をネットワークに渡し、Cache Storage API を介して将来の使用のためにレスポンスのコピーを保存する Service Worker を構築できます。
ウェブアプリが次回同じリクエストを行うと、サービス ワーカーはキャッシュをチェックして、以前にキャッシュに保存されたレスポンスを返すことができます。
可能な限りネットワークを回避することは、信頼性の高い高速なパフォーマンスを実現するうえで重要な要素です。
「アイソモーフィック」JavaScript
もう 1 つのコンセプトは、「アイソモーフィック」または「ユニバーサル」JavaScript と呼ばれるものです。簡単に言うと、同じ JavaScript コードを異なるランタイム環境間で共有できるという考え方です。PWA を構築したとき、バックエンド サーバーと Service Worker の間で JavaScript コードを共有したいと考えました。
この方法でコードを共有するには、さまざまな有効なアプローチがありますが、私のアプローチは、ES モジュールを決定的なソースコードとして使用することでした。次に、Babel と Rollup を組み合わせて、サーバーと Service Worker 用にこれらのモジュールをトランスパイルしてバンドルしました。私のプロジェクトでは、.mjs ファイル拡張子を持つファイルは ES モジュールに存在するコードです。
サーバー
これらのコンセプトと用語を念頭に置いて、Stack Overflow PWA を実際に構築する方法について説明します。まず、バックエンド サーバーについて説明し、それがアーキテクチャ全体にどのように適合するかを説明します。
動的バックエンドと静的ホスティングの組み合わせを探していたところ、Firebase プラットフォームを使用する方法にたどり着きました。
Firebase Cloud Functions は、リクエストを受信すると Node ベースの環境を自動的に起動し、私がすでに使い慣れていた人気の Express HTTP フレームワークと統合します。また、サイトのすべての静的リソースをすぐにホストできます。サーバーがリクエストを処理する方法を見てみましょう。
ブラウザがサーバーに対してナビゲーション リクエストを行うと、次のフローが実行されます。
サーバーは URL に基づいてリクエストをルーティングし、テンプレート ロジックを使用して完全な HTML ドキュメントを作成します。Stack Exchange API のデータと、サーバーがローカルに保存する部分的な HTML フラグメントを組み合わせて使用しています。サービス ワーカーが応答方法を認識すると、ウェブアプリへの HTML のストリーミングを開始できます。
この図には、ルーティングとテンプレートという、詳しく調べる価値のある 2 つの部分があります。
ルーティング
ルーティングに関しては、Express フレームワークのネイティブ ルーティング構文を使用する方法を採用しました。シンプルな URL 接頭辞だけでなく、パスの一部としてパラメータを含む URL にも一致する柔軟性を備えています。ここでは、ルート名と、照合する基盤となる Express パターンの間にマッピングを作成します。
const routes = new Map([
['about', '/about'],
['questions', '/questions/:questionId'],
['index', '/'],
]);
export default routes;
このマッピングは、サーバーのコードから直接参照できます。特定の Express パターンに一致するものがある場合、適切なハンドラが、一致するルートに固有のテンプレート ロジックで応答します。
import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
// Templating logic.
});
サーバーサイド テンプレート
このテンプレート ロジックはどのようなものでしょうか。私は、部分的な HTML フラグメントを順番に 1 つずつ組み合わせていくアプローチを採用しました。このモデルはストリーミングに適しています。
サーバーは最初の HTML ボイラープレートをすぐに返します。ブラウザはその部分的なページをすぐにレンダリングできます。サーバーは、残りのデータソースをまとめながら、ドキュメントが完成するまでブラウザにストリーミングします。
具体的には、ルートの 1 つの Express コードをご覧ください。
app.get(routes.get('index'), async (req, res) => {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end();
});
response オブジェクトの write() メソッドを使用し、ローカルに保存された部分テンプレートを参照することで、外部データソースをブロックすることなく、レスポンス ストリームをすぐに開始できます。ブラウザはこの最初の HTML を受け取り、意味のあるインターフェースと読み込みメッセージをすぐにレンダリングします。
ページの次の部分では、Stack Exchange API のデータを使用します。このデータを取得するには、サーバーがネットワーク リクエストを行う必要があります。ウェブアプリは、レスポンスを受け取って処理するまで、他のものをレンダリングできませんが、少なくともユーザーは待機中に空白の画面を見つめる必要はありません。
ウェブアプリは、Stack Exchange API からレスポンスを受信すると、カスタム テンプレート関数を呼び出して、API からのデータを対応する HTML に変換します。
テンプレート言語
テンプレート化は、驚くほど議論の余地があるトピックであり、私が採用した方法は、数あるアプローチの一つにすぎません。特に、既存のテンプレート フレームワークとのレガシーな関係がある場合は、独自のソリューションに置き換えることをおすすめします。
私のユースケースに適していたのは、JavaScript のテンプレート リテラルに依存し、一部のロジックをヘルパー関数に分割することでした。MPA を構築するメリットの一つは、状態の更新を追跡して HTML を再レンダリングする必要がないことです。そのため、静的 HTML を生成する基本的なアプローチが有効でした。
ウェブアプリのインデックスの動的 HTML 部分をテンプレート化する方法の例を次に示します。ルートと同様に、テンプレート ロジックはサーバーと Service Worker の両方にインポートできる ES モジュールに保存されます。
export function index(tag, items) {
const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
const form = `<form method="GET">...</form>`;
const questionCards = items
.map(item =>
questionCard({
id: item.question_id,
title: item.title,
})
)
.join('');
const questions = `<div id="questions">${questionCards}</div>`;
return title + form + questions;
}
これらのテンプレート関数は純粋な JavaScript であり、必要に応じてロジックをより小さなヘルパー関数に分割すると便利です。ここでは、API レスポンスで返された各アイテムを、そのような関数の 1 つに渡します。この関数は、適切な属性がすべて設定された標準の HTML 要素を作成します。
function questionCard({id, title}) {
return `<a class="card"
href="/questions/${id}"
data-cache-url="${questionUrl(id)}">${title}</a>`;
}
特に注目すべきのは、各リンクに追加するデータ属性 data-cache-url です。これは、対応する質問を表示するために必要な Stack Exchange API URL に設定されています。この点に注意してください。後で確認します。
ルートハンドラに戻り、テンプレート化が完了したら、ページの HTML の最後の部分をブラウザにストリーミングし、ストリームを終了します。これは、プログレッシブ レンダリングが完了したことをブラウザに通知する合図です。
app.get(routes.get('index'), async (req, res) => {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end();
});
以上が、サーバー設定の簡単な説明です。ウェブアプリに初めてアクセスしたユーザーは、常にサーバーからレスポンスを受け取りますが、ユーザーがウェブアプリに再度アクセスすると、Service Worker がレスポンスを開始します。詳しく見ていきましょう
Service Worker
この図は、以前に説明した多くの要素が少し異なる配置で示されているため、見覚えがあるかもしれません。サービス ワーカーを考慮して、リクエスト フローを見てみましょう。
サービス ワーカーは、特定の URL に対する受信ナビゲーション リクエストを処理します。サーバーと同様に、ルーティングとテンプレート ロジックを組み合わせて、応答方法を決定します。
アプローチは以前と同じですが、fetch() や Cache Storage API などの低レベルのプリミティブが異なります。これらのデータソースを使用して HTML レスポンスを作成し、サービス ワーカーがウェブアプリに返します。
Workbox
低レベルのプリミティブを使用してゼロから始めるのではなく、Workbox と呼ばれる高レベルのライブラリ セットの上にサービス ワーカーを構築します。これは、サービス ワーカーのキャッシュ、ルーティング、レスポンス生成ロジックの強固な基盤となります。
ルーティング
サーバーサイド コードと同様に、サービス ワーカーは、受信リクエストを適切なレスポンス ロジックと照合する方法を認識する必要があります。
私のアプローチは、regexparam という便利なライブラリを使用して、各 Express ルートを対応する正規表現に変換することでした。この変換が完了すると、Workbox に組み込まれている正規表現ルーティングのサポートを利用できます。
正規表現を含むモジュールをインポートした後、各正規表現を Workbox のルーターに登録します。各ルート内で、レスポンスを生成するカスタム テンプレート ロジックを指定できます。サービス ワーカーでのテンプレート化は、バックエンド サーバーよりも少し複雑ですが、Workbox は多くの重労働を支援します。
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templating logic.
);
静的アセットのキャッシュ保存
テンプレート化の重要な部分の 1 つは、部分的な HTML テンプレートが Cache Storage API を介してローカルで利用可能であり、ウェブアプリに変更をデプロイするときに最新の状態に保たれるようにすることです。キャッシュのメンテナンスは手動で行うとエラーが発生しやすいため、ビルドプロセスの一環として Workbox を使用して事前キャッシュを処理します。
構成ファイルを使用して、事前キャッシュに保存する URL を Workbox に指示します。このファイルは、すべてのローカル アセットを含むディレクトリと、一致するパターンのセットを指します。このファイルは、サイトを再ビルドするたびに実行される Workbox の CLI によって自動的に読み取られます。
module.exports = {
globDirectory: 'build',
globPatterns: ['**/*.{html,js,svg}'],
// Other options...
};
Workbox は各ファイルの内容のスナップショットを取得し、URL とリビジョンのリストを最終的な Service Worker ファイルに自動的に挿入します。Workbox には、事前キャッシュされたファイルを常に利用可能にし、最新の状態に保つために必要なものがすべて揃っています。結果は、次のような内容を含む service-worker.js ファイルです。
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746a9ecc6',
},
// etc.
]);
より複雑なビルドプロセスを使用するユーザー向けに、Workbox にはコマンドライン インターフェースに加えて、webpack プラグインと汎用ノード モジュールの両方が用意されています。
ストリーミング
次に、サービス ワーカーが、事前キャッシュに保存された部分的な HTML をウェブアプリにすぐにストリーミングするようにします。これは「信頼できる速さ」を実現するうえで重要な要素です。つまり、常に意味のあるものがすぐに画面に表示されます。幸い、Service Worker 内で Streams API を使用することで、これが可能になります。
Streams API については、以前に耳にしたことがあるかもしれません。同僚の Jake Archibald は、何年も前からこの機能を称賛しています。彼は、2016 年はウェブ ストリームの年になるという大胆な予測を立てました。Streams API は 2 年前と同様に素晴らしいものですが、重要な違いがあります。
当時は Chrome のみが Streams をサポートしていましたが、現在では Streams API がより広くサポートされています。全体的に見て、状況は良好です。適切なフォールバック コードを使用すれば、今すぐ Service Worker でストリームを使用できます。
ただし、Streams API の実際の仕組みを理解するという、1 つのハードルがあるかもしれません。非常に強力なプリミティブ セットが公開されており、これに慣れているデベロッパーは、次のような複雑なデータフローを作成できます。
const stream = new ReadableStream({
pull(controller) {
return sources[0]
.then(r => r.read())
.then(result => {
if (result.done) {
sources.shift();
if (sources.length === 0) return controller.close();
return this.pull(controller);
} else {
controller.enqueue(result.value);
}
});
},
});
しかし、このコードの意味を完全に理解することは、誰にでもできることではありません。このロジックを解析するのではなく、Service Worker のストリーミングに対する私のアプローチについて説明します。
ここでは、新しい高レベルのラッパー workbox-streams を使用しています。これを使用すると、キャッシュと、ネットワークから取得される可能性のあるランタイム データの両方から、ストリーミング ソースを組み合わせて渡すことができます。Workbox は、個々のソースを調整して 1 つのストリーミング レスポンスにまとめる処理を行います。
さらに、Workbox は Streams API がサポートされているかどうかを自動的に検出し、サポートされていない場合は、同等の非ストリーミング レスポンスを作成します。つまり、ストリームが 100% のブラウザ サポートに近づくにつれて、フォールバックの作成を心配する必要がなくなります。
ランタイム キャッシュ
サービス ワーカーが Stack Exchange API からのランタイム データをどのように処理するかを見てみましょう。Workbox に組み込まれている stale-while-revalidate キャッシュ戦略のサポートと有効期限を使用して、ウェブアプリのストレージが無制限に増えないようにしています。
Workbox で 2 つの戦略を設定して、ストリーミング レスポンスを構成するさまざまなソースを処理します。Workbox では、数回の関数呼び出しと構成で、数百行の手書きコードが必要になる処理を実行できます。
const cacheStrategy = workbox.strategies.cacheFirst({
cacheName: workbox.core.cacheNames.precache,
});
const apiStrategy = workbox.strategies.staleWhileRevalidate({
cacheName: API_CACHE_NAME,
plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});
最初の戦略では、部分的な HTML テンプレートなど、プリキャッシュされたデータを読み取ります。
もう 1 つの戦略では、stale-while-revalidate キャッシュ ロジックを実装し、エントリ数が 50 に達すると、最も長い間使われていないキャッシュを期限切れにします。
これらの戦略を導入したら、あとは Workbox に指示して、それらを使用して完全なストリーミング レスポンスを構築する方法を指示するだけです。ソースの配列を関数として渡すと、それらの関数がすぐに実行されます。Workbox は、各ソースから結果を取得し、ウェブアプリに順番にストリーミングします。配列内の次の関数がまだ完了していない場合にのみ遅延します。
workbox.streams.strategy([
() => cacheStrategy.makeRequest({request: '/head.html'}),
() => cacheStrategy.makeRequest({request: '/navbar.html'}),
async ({event, url}) => {
const tag = url.searchParams.get('tag') || DEFAULT_TAG;
const listResponse = await apiStrategy.makeRequest(...);
const data = await listResponse.json();
return templates.index(tag, data.items);
},
() => cacheStrategy.makeRequest({request: '/foot.html'}),
]);
最初の 2 つのソースは、Cache Storage API から直接読み取られた、事前にキャッシュに保存された部分テンプレートであるため、常にすぐに利用できます。これにより、サーバーサイド コードと同様に、サービス ワーカーの実装がリクエストに迅速に応答できるようになります。
次のソース関数は、Stack Exchange API からデータを取得し、レスポンスをウェブアプリが想定する HTML に処理します。
stale-while-revalidate 戦略とは、この API 呼び出しに対して以前にキャッシュに保存されたレスポンスがある場合、それをすぐにページにストリーミングできる一方で、次回のリクエストに備えて「バックグラウンド」でキャッシュ エントリを更新できることを意味します。
最後に、フッターのキャッシュに保存されたコピーをストリーミングし、最後の HTML タグを閉じてレスポンスを完了します。
コードを共有して同期を維持する
サービス ワーカーのコードの一部に見覚えがあることに気づくでしょう。サービス ワーカーで使用される部分的な HTML とテンプレート ロジックは、サーバーサイド ハンドラで使用されるものと同じです。このコード共有により、ユーザーがウェブアプリに初めてアクセスする場合でも、Service Worker によってレンダリングされたページに戻る場合でも、一貫したエクスペリエンスを得ることができます。これがアイソモーフィック JavaScript の優れた点です。
動的でプログレッシブ エンハンスメント
PWA のサーバーと Service Worker の両方について説明しましたが、最後に説明するロジックが 1 つあります。各ページが完全にストリーミングされた後に、各ページで実行される少量の JavaScript があります。
このコードはプログレッシブ エンハンスメントによりユーザー エクスペリエンスを段階的に向上させますが、必須ではありません。実行されなくてもウェブアプリは動作します。
ページ メタデータ
私のアプリは、クライアントサイドの JavaScript を使用して、API レスポンスに基づいてページのメタデータを更新します。各ページで同じ初期ビットのキャッシュに保存された HTML を使用するため、ウェブアプリのドキュメントの head に汎用タグが追加されます。しかし、テンプレートとクライアントサイドのコードを連携させることで、ページ限定のメタデータを使用してウィンドウのタイトルを更新できます。
テンプレート コードの一部として、適切にエスケープされた文字列を含むスクリプトタグを含める方法を採用しています。
const metadataScript = `<script>
self._title = '${escape(item.title)}';
</script>`;
その後、ページが読み込まれたら、その文字列を読み取ってドキュメントのタイトルを更新します。
if (self._title) {
document.title = unescape(self._title);
}
独自のウェブアプリで更新するページ限定のメタデータが他にもある場合は、同じ方法を使用できます。
オフライン UX
追加したもう 1 つのプログレッシブ エンハンスメントは、オフライン機能に注目してもらうために使用されます。信頼性の高い PWA を構築しました。オフラインでも、以前にアクセスしたページを読み込めることをユーザーに知らせたいと考えています。
まず、Cache Storage API を使用して、以前にキャッシュに保存されたすべての API リクエストのリストを取得し、それを URL のリストに変換します。
以前に説明した、質問を表示するために必要な API リクエストの URL をそれぞれ含む特別なデータ属性を覚えていますか?これらのデータ属性をキャッシュに保存された URL のリストと相互参照し、一致しないすべての質問リンクの配列を作成できます。
ブラウザがオフライン状態になると、キャッシュに保存されていないリンクのリストをループ処理し、機能しないリンクをグレー表示にします。これは、ユーザーにこれらのページから何が期待できるかを視覚的に示すヒントにすぎません。実際にリンクを無効にしたり、ユーザーが移動できないようにしたりしているわけではありません。
const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);
const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filter(card => {
return !cachedUrls.includes(card.dataset.cacheUrl);
});
const offlineHandler = () => {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '0.3';
}
};
const onlineHandler = () => {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '1.0';
}
};
window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);
主な注意点
マルチページ PWA を構築するための私のアプローチについて説明しました。独自のアプローチを考案する際には、考慮すべき要素が数多くあります。そのため、私とは異なる選択をすることになるかもしれません。この柔軟性は、ウェブ向けに構築する際の大きなメリットの一つです。
独自のアーキテクチャを決定する際に陥りやすい落とし穴がいくつかあります。
HTML 全体をキャッシュに保存しない
完全な HTML ドキュメントをキャッシュに保存することはおすすめしません。1 つは、スペースの無駄になることです。ウェブアプリで各ページに同じ基本的な HTML 構造を使用している場合、同じマークアップのコピーが何度も保存されることになります。
さらに重要なのは、サイトの共有 HTML 構造に変更をデプロイすると、以前にキャッシュに保存されたページはすべて古いレイアウトのままになることです。リピーターが古いページと新しいページが混在しているのを見たときの不満を想像してみてください。
サーバー / サービス ワーカーのドリフト
もう 1 つの注意点は、サーバーと Service Worker の同期がずれることです。私のアプローチは、アイソモーフィック JavaScript を使用して、同じコードを両方の場所で実行することでした。既存のサーバー アーキテクチャによっては、それが不可能な場合もあります。
どのようなアーキテクチャ上の決定を下すにしても、サーバーと Service Worker で同等のルーティング コードとテンプレート コードを実行するための戦略が必要です。
最悪のシナリオ
レイアウト / デザインに一貫性がない
これらの落とし穴を無視するとどうなるでしょうか。さまざまな障害が考えられますが、最悪のシナリオは、リピーターが非常に古いレイアウトのキャッシュ ページにアクセスすることです。たとえば、ヘッダー テキストが古くなっている、または無効になった CSS クラス名を使用しているなどです。
最悪のシナリオ: ルーティングの破損
また、ユーザーが、サーバーで処理されるものの、Service Worker では処理されない URL にアクセスすることもあります。ゾンビ レイアウトや行き止まりだらけのサイトは、信頼できる PWA ではありません。
成功のためのヒント
でもご安心ください。次のヒントは、こうした落とし穴を回避するのに役立ちます。
多言語実装に対応したテンプレート ライブラリとルーティング ライブラリを使用する
JavaScript 実装を含むテンプレート ライブラリとルーティング ライブラリを使用してみてください。すべてのデベロッパーが、現在のウェブサーバーとテンプレート言語から移行できるわけではないことは承知しています。
ただし、一般的なテンプレート フレームワークやルーティング フレームワークの多くは、複数の言語で実装されています。JavaScript と現在のサーバーの言語の両方に対応しているものが見つかれば、Service Worker とサーバーの同期を維持する一歩に近づきます。
ネストされたテンプレートではなく、順次テンプレートを優先する
次に、一連の順次テンプレートを使用して、1 つずつストリーミングすることをおすすめします。HTML の最初の部分をできるだけ早くストリーミングできる限り、ページの後半部分でより複雑なテンプレート ロジックを使用しても問題ありません。
Service Worker で静的コンテンツと動的コンテンツの両方をキャッシュに保存する
最適なパフォーマンスを得るには、サイトの重要な静的リソースをすべてプリキャッシュする必要があります。また、API リクエストなどの動的コンテンツを処理するランタイム キャッシュ ロジックも設定する必要があります。Workbox を使用すると、すべてをゼロから実装するのではなく、十分にテストされた本番環境対応の戦略を基盤として構築できます。
ネットワークでのブロックは、どうしても必要な場合にのみ行う
これに関連して、キャッシュからレスポンスをストリーミングできない場合にのみ、ネットワークをブロックする必要があります。キャッシュに保存された API レスポンスをすぐに表示すると、新しいデータを待つよりもユーザー エクスペリエンスが向上することがよくあります。