プログレッシブWebアプリケーションの広範なガイド

公開: 2022-03-10
簡単な要約↬この記事では、古い非PWA Webサイトを閲覧しているユーザーの問題点と、Webを優れたものにするためのPWAの約束について説明します。 サービスワーカー、Webプッシュ通知、IndexedDBなど、クールなPWAを実現する重要なテクノロジーのほとんどを学びます。

お父さんの誕生日だったので、お父さんにチョコレートケーキとシャツを注文したかったのです。 Googleに行ってチョコレートケーキを検索し、検索結果の最初のリンクをクリックしました。 数秒間空白の画面がありました。 何が起こっているのか分かりませんでした。 辛抱強く見つめている数秒後、私のモバイル画面は美味しそうなケーキでいっぱいになりました。 そのうちの1つをクリックして詳細を確認するとすぐに、醜い太ったポップアップが表示され、ケーキを注文するときに滑らかで滑らかな体験ができるようにAndroidアプリケーションをインストールするように求められました。

それは残念でした。 私の良心では、「インストール」ボタンをクリックすることはできませんでした。 私がやりたかったのは、小さなケーキを注文して、途中にいることだけでした。

ポップアップの右端にある十字アイコンをクリックして、できるだけ早くポップアップから抜け出しました。 しかし、その後、インストールポップアップが画面の下部に表示され、スペースの4分の1を占めていました。 そして、不安定なUIでは、下にスクロールするのは困難でした。 私はどういうわけかオランダのケーキを注文することができました。

このひどい経験の後、私の次の挑戦は私の父のためにシャツを注文することでした。 以前と同じように、私はGoogleでシャツを検索します。 最初のリンクをクリックすると、あっという間にコンテンツ全体が目の前に表示されました。 スクロールはスムーズでした。 インストールバナーはありません。 まるでネイティブアプリケーションを閲覧しているように感じました。 ひどいインターネット接続が途絶えた瞬間がありましたが、恐竜ゲームではなくコンテンツを見ることができました。 インターネットがぎこちなくても、父のためにシャツとジーンズを注文することができました。 何より驚くべきことに、注文に関する通知を受け取っていました。

私はこれを絹のような滑らかな体験と呼んでいます。 これらの人々は何か正しいことをしていました。 すべてのウェブサイトはユーザーのためにそれを行う必要があります。 プログレッシブウェブアプリと呼ばれています。

アレックスラッセルが彼のブログ投稿の1つで述べているように:

「ウェブ上では、マーケティング部門や洗練されたパッケージの恩恵を受けずに強力なテクノロジーが存在するようになることが時々あります。 彼らは長居し、周辺で成長し、他の誰にもほとんど見えないまま、小さなグループの古い帽子になります。 誰かが名前を付けるまで。」

プログレッシブWebアプリケーションと呼ばれることもある、Web上でのシルキーでスムーズなエクスペリエンス

プログレッシブウェブアプリケーション(PWA)は、強力なウェブアプリケーションを作成するためのテクノロジーの組み合わせを含む方法論です。 ユーザーエクスペリエンスが向上すると、人々はより多くの時間をWebサイトに費やし、より多くの広告を見るようになります。 彼らはより多くを購入する傾向があり、通知の更新により、彼らは頻繁に訪問する可能性が高くなります。 Financial Timesは、2011年にネイティブアプリを放棄し、当時利用可能な最高のテクノロジーを使用してWebアプリを構築しました。 現在、この製品は本格的なPWAに成長しています。

しかし、なぜ、この間ずっと、ネイティブアプリが十分に機能するのに、Webアプリを構築するのでしょうか。

Google IO17で共有されているいくつかの指標を見てみましょう。

ジャンプした後もっと! 以下を読み続けてください↓

50億台のデバイスがWebに接続されており、Webはコンピューティングの歴史の中で最大のプラットフォームになっています。 モバイルウェブでは、月間1,140万人のユニークビジターがトップ1000のウェブプロパティにアクセスし、400万人がトップ1000のアプリにアクセスします。 モバイルWebは、ネイティブアプリケーションの約4倍のユーザーを獲得しています。 しかし、エンゲージメントに関しては、この数は急激に減少します。

ユーザーは、ネイティブアプリで平均188.6分、モバイルウェブでわずか9.3分を費やしています。 ネイティブアプリケーションは、オペレーティングシステムの機能を活用してプッシュ通知を送信し、ユーザーに重要な更新を提供します。 これらは、ブラウザのWebサイトよりも優れたユーザーエクスペリエンスを提供し、より迅速に起動します。 ユーザーは、WebブラウザーでURLを入力する代わりに、ホーム画面でアプリのアイコンをタップするだけです。

Web上のほとんどの訪問者が戻ってくる可能性は低いため、開発者は、ネイティブアプリケーションをインストールするためのバナーを表示して、ユーザーの関心を維持するための回避策を考え出しました。 しかし、その後、ユーザーはネイティブアプリケーションのバイナリをインストールするという面倒な手順を実行する必要があります。 ユーザーにアプリケーションのインストールを強制することは煩わしく、そもそもアプリケーションをインストールする可能性をさらに減らします。 ウェブの機会は明らかです。

推奨読書ネイティブおよびPWA:チャレンジャーではなく、選択肢!

Webアプリケーションに豊富なユーザーエクスペリエンス、プッシュ通知、オフラインサポート、インスタントロードが備わっている場合、Webアプリケーションは世界を征服することができます。 これは、プログレッシブWebアプリケーションが行うことです。

PWAにはいくつかの長所があるため、豊富なユーザーエクスペリエンスを提供します。

  • 速い
    UIは不安定ではありません。 スクロールはスムーズです。 また、アプリはユーザーの操作にすばやく応答します。

  • 信頼性のある
    通常のWebサイトでは、サーバーへの乗車に忙しい間、ユーザーは何もせずに待機する必要があります。 一方、PWAは、キャッシュからデータを瞬時にロードします。 PWAは、2G接続でもシームレスに機能します。 アセットまたはデータの一部をフェッチするすべてのネットワーク要求は、サービスワーカーを経由します(詳細は後で説明します)。サービスワーカーは、特定の要求に対する応答が既にキャッシュにあるかどうかを最初に確認します。 ユーザーは、接続が不十分な場合でも、ほぼ瞬時に実際のコンテンツを取得すると、アプリをより信頼し、信頼性が高いと見なします。

  • 魅力的
    PWAは、ユーザーのホーム画面で場所を獲得できます。 フルスクリーンの作業領域を提供することにより、ネイティブアプリのようなエクスペリエンスを提供します。 プッシュ通知を利用して、ユーザーの関心を維持します。

PWAがテーブルにもたらすものがわかったので、PWAがネイティブアプリケーションよりも優れている理由について詳しく見ていきましょう。 PWAは、サービスワーカー、Webアプリマニフェスト、プッシュ通知、キャッシュ用のIndexedDB /ローカルデータ構造などのテクノロジーで構築されています。 それぞれを詳しく見ていきましょう。

サービスワーカー

Service Workerは、ユーザーの操作を妨げることなくバックグラウンドで実行されるJavaScriptファイルです。 サーバーへのすべてのGET要求は、ServiceWorkerを経由します。 クライアント側のプロキシのように機能します。 ネットワーク要求をインターセプトすることにより、クライアントに返送される応答を完全に制御します。 サービスワーカーがキャッシュからのデータで応答することによりネットワークへの依存を排除​​するため、PWAは即座にロードされます。

サービスワーカーは、そのスコープ内にあるネットワーク要求のみを傍受できます。 たとえば、ルートスコープのサービスワーカーは、Webページからのすべてのフェッチ要求をインターセプトできます。 サービスワーカーは、イベント駆動型システムとして動作します。 不要な場合は休止状態になり、メモリを節約します。 WebアプリケーションでServiceWorkerを使用するには、最初にJavaScriptを使用してサービスワーカーをページに登録する必要があります。

 (function main () { /* navigator is a WEB API that allows scripts to register themselves and carry out their activities. */ if ('serviceWorker' in navigator) { console.log('Service Worker is supported in your browser') /* register method takes in the path of service worker file and returns a promises, which returns the registration object */ navigator.serviceWorker.register('./service-worker.js').then (registration => { console.log('Service Worker is registered!') }) } else { console.log('Service Worker is not supported in your browser') } })()

まず、ブラウザがServiceWorkerをサポートしているかどうかを確認します。 サービスワーカーをWebアプリケーションに登録するために、 navigator.serviceWorkerで使用できるregister関数のパラメーターとしてそのURLを提供します( navigatorは、スクリプトが自分自身を登録してアクティビティを実行できるようにするWeb APIです)。 サービスワーカーは一度だけ登録されます。 ページが読み込まれるたびに登録が行われるわけではありません。 ブラウザは、既存のアクティブ化されたサービスワーカーと新しいサービスワーカーの間にバイト差がある場合、またはそのURLが変更された場合にのみ、サービスワーカーファイル( ./service-worker.js )をダウンロードします。

上記のサービスワーカーは、ルート( / )からのすべての要求をインターセプトします。 Service Workerのスコープを制限するには、キーの1つをスコープとして持つオプションのパラメーターを渡します。

 if ('serviceWorker' in navigator) { /* register method takes in an optional second parameter as an object. To restrict the scope of a service worker, the scope should be provided. scope: '/books' will intercept requests with '/books' in the url. */ navigator.serviceWorker.register('./service-worker.js', { scope: '/books' }).then(registration => { console.log('Service Worker for scope /books is registered', registration) }) }

上記のServiceWorkerは、URLに/booksが含まれるリクエストをインターセプトします。 たとえば、 /productsでリクエストをインターセプトすることはありませんが、 /books/productsでリクエストをインターセプトすることはできます。

前述のように、サービスワーカーはイベント駆動型システムとして動作します。 イベント(インストール、アクティブ化、フェッチ、プッシュ)をリッスンし、それに応じてそれぞれのイベントハンドラーを呼び出します。 これらのイベントの一部は、サービスワーカーのライフサイクルの一部であり、サービスワーカーは、これらのイベントを順番に実行してアクティブ化します。

インストール

Service Workerが正常に登録されると、インストールイベントが発生します。 これは、キャッシュの設定やIndexedDBでのオブジェクトストアの作成などの初期化作業を行うのに適した場所です。 (IndexedDBの詳細を理解すれば、より意味がわかります。今のところ、これはKey-Valueペア構造であると言えます。)

 self.addEventListener('install', (event) => { let CACHE_NAME = 'xyz-cache' let urlsToCache = [ '/', '/styles/main.css', '/scripts/bundle.js' ] event.waitUntil( /* open method available on caches, takes in the name of cache as the first parameter. It returns a promise that resolves to the instance of cache All the URLS above can be added to cache using the addAll method. */ caches.open(CACHE_NAME) .then (cache => cache.addAll(urlsToCache)) ) })

ここでは、次のロードが瞬時に行われるように、いくつかのファイルをキャッシュしています。 selfはServiceWorkerインスタンスを指します。 event.waitUntilは、サービスワーカーに、その中のすべてのコードの実行が完了するまで待機させます。

アクティベーション

Service Workerをインストールすると、フェッチ要求をリッスンできなくなります。 むしろ、 activateイベントが発生します。 同じスコープ内のWebサイトでアクティブなサービスワーカーが動作していない場合、インストールされているサービスワーカーはすぐにアクティブ化されます。 ただし、Webサイトにすでにアクティブなサービスワーカーがある場合は、古いサービスワーカーで動作しているすべてのタブが閉じられるまで、新しいサービスワーカーのアクティブ化が遅延されます。 古いServiceWorkerが、新しいキャッシュで変更されたキャッシュのインスタンスを使用している可能性があるため、これは理にかなっています。 したがって、アクティベーション手順は古いキャッシュを取り除くのに適した場所です。

 self.addEventListener('activate', (event) => { let cacheWhitelist = ['products-v2'] // products-v2 is the name of the new cache event.waitUntil( caches.keys().then (cacheNames => { return Promise.all( cacheNames.map( cacheName => { /* Deleting all the caches except the ones that are in cacheWhitelist array */ if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName) } }) ) }) ) })

上記のコードでは、古いキャッシュを削除しています。 キャッシュの名前がcacheWhitelistと一致しない場合、そのキャッシュは削除されます。 待機フェーズをスキップしてすぐにServiceWorkerをアクティブ化するには、 skip.waiting()を使用します。

 self.addEventListener('activate', (event) => { self.skipWaiting() // The usual stuff })

Service Workerがアクティブ化されると、フェッチ要求とプッシュイベントをリッスンできます。

フェッチイベントハンドラ

Webページがネットワークを介してリソースのフェッチ要求を発生させるたびに、ServiceWorkerからのフェッチイベントが呼び出されます。 フェッチイベントハンドラーは、最初にキャッシュ内で要求されたリソースを探します。 キャッシュに存在する場合は、キャッシュされたリソースとともに応答を返します。 それ以外の場合は、サーバーへのフェッチリクエストを開始し、サーバーがリクエストされたリソースを含むレスポンスを送り返すと、後続のリクエストのためにそれをキャッシュに入れます。

 /* Fetch event handler for responding to GET requests with the cached assets */ self.addEventListener('fetch', (event) => { event.respondWith( caches.open('products-v2') .then (cache => { /* Checking if the request is already present in the cache. If it is present, sending it directly to the client */ return cache.match(event.request).then (response => { if (response) { console.log('Cache hit! Fetching response from cache', event.request.url) return response } /* If the request is not present in the cache, we fetch it from the server and then put it in cache for subsequent requests. */ fetch(event.request).then (response => { cache.put(event.request, response.clone()) return response }) }) }) ) })

event.respondWith使用すると、サービスワーカーはカスタマイズされた応答をクライアントに送信できます。

オフラインファーストが重要になりました。 重要でないリクエストについては、サーバーにアクセスするのではなく、キャッシュからの応答を提供する必要があります。 キャッシュにアセットが存在しない場合は、サーバーからアセットを取得し、後続のリクエストのためにキャッシュします。

サービスワーカーは、フェッチ要求の応答を操作する権限を持っているため、HTTPSWebサイトでのみ機能します。 悪意のある人がHTTPWebサイトでの要求に対する応答を改ざんする可能性があります。 したがって、HTTPSでPWAをホストすることは必須です。 サービスワーカーは、DOMの通常の機能を中断しません。 Webページと直接通信することはできません。 Webページにメッセージを送信するには、投稿メッセージを使用します。

Webプッシュ通知

携帯電話でゲームをプレイするのに忙しく、お気に入りのブランドが30%割引になるという通知がポップアップ表示されたとします。 それ以上の苦労なしに、あなたは通知をクリックして息を呑む。 たとえば、クリケットやサッカーの試合のライブアップデートを取得したり、通知として重要なメールやリマインダーを取得したりすることは、ユーザーを製品に引き付ける上で大きな問題です。 この機能は、PWAが登場するまでネイティブアプリケーションでのみ利用可能でした。 PWAは、Webプッシュ通知を利用して、ネイティブアプリがすぐに提供するこの強力な機能と競合します。 PWAがどのブラウザタブでも開いていなくても、またブラウザが開いていなくても、ユーザーはWebプッシュ通知を受け取ります。

Webアプリケーションは、プッシュ通知を送信するためにユーザーの許可を求める必要があります。

Webプッシュ通知の許可を求めるブラウザプロンプト
Webプッシュ通知の許可を求めるブラウザプロンプト。 (大プレビュー)

ユーザーが「許可」ボタンをクリックして確認すると、ブラウザによって一意のサブスクリプショントークンが生成されます。 このトークンは、このデバイスに固有です。 Chromeによって生成されるサブスクリプショントークンの形式は次のとおりです。

 { "endpoint": "https://fcm.googleapis.com/fcm/send/c7Veb8VpyM0:APA91bGnMFx8GIxf__UVy6vJ-n9i728CUJSR1UHBPAKOCE_SrwgyP2N8jL4MBXf8NxIqW6NCCBg01u8c5fcY0kIZvxpDjSBA75sVz64OocQ-DisAWoW7PpTge3SwvQAx5zl_45aAXuvS", "expirationTime": null, "keys": { "p256dh": "BJsj63kz8RPZe8Lv1uu-6VSzT12RjxtWyWCzfa18RZ0-8sc5j80pmSF1YXAj0HnnrkyIimRgLo8ohhkzNA7lX4w", "auth": "TJXqKozSJxcWvtQasEUZpQ" } }

上記のトークンに含まれるendpointは、サブスクリプションごとに一意になります。 平均的なWebサイトでは、何千人ものユーザーがプッシュ通知の受信に同意し、それぞれについて、このendpointは一意になります。 したがって、このendpointの助けを借りて、アプリケーションは、プッシュ通知を送信することにより、将来これらのユーザーをターゲットにすることができます。 expirationTimeは、サブスクリプションが特定のデバイスに対して有効である時間です。 expirationTimeが20日である場合、ユーザーのプッシュサブスクリプションは20日後に期限切れになり、ユーザーは古いサブスクリプションのプッシュ通知を受信できなくなります。 この場合、ブラウザはそのデバイスの新しいサブスクリプショントークンを生成します。 authキーとp256dhキーは暗号化に使用されます。

将来、これらの数千人のユーザーにプッシュ通知を送信するには、まずそれぞれのサブスクリプショントークンを保存する必要があります。 これらのユーザーにプッシュ通知を送信するのは、アプリケーションサーバー(バックエンドサーバー、場合によってはNode.jsスクリプト)の仕事です。 これは、リクエストペイロードの通知データを使用してエンドポイントURLにPOSTリクエストを行うのと同じくらい簡単に聞こえるかもしれません。 ただし、ユーザーを対象としたプッシュ通知がサーバーによってトリガーされたときにユーザーがオンラインでない場合でも、ユーザーがオンラインに戻ったときにその通知を受け取る必要があることに注意してください。 サーバーは、ユーザーに何千ものリクエストを送信するとともに、そのようなシナリオを処理する必要があります。 ユーザーの接続を追跡するサーバーは複雑に聞こえます。 したがって、中間の何かが、サーバーからクライアントへのWebプッシュ通知のルーティングを担当します。 これはプッシュサービスと呼ばれ、すべてのブラウザに独自のプッシュサービスの実装があります。 ブラウザは、通知を送信するために、プッシュサービスに次の情報を通知する必要があります。

  1. 生きる時間
    これは、メッセージがユーザーに配信されない場合に、メッセージがキューに入れられる時間です。 この時間が経過すると、メッセージはキューから削除されます。
  2. メッセージの緊急性
    これは、プッシュサービスが優先度の高いメッセージのみを送信することでユーザーのバッテリーを節約するためです。

プッシュサービスはメッセージをクライアントにルーティングします。 それぞれのWebアプリケーションがブラウザで開かれていない場合でも、クライアントはプッシュを受信する必要があるため、プッシュイベントは、バックグラウンドで継続的に監視するものによってリッスンされる必要があります。 あなたはそれを推測しました:それはサービスワーカーの仕事です。 Service Workerはプッシュイベントをリッスンし、ユーザーに通知を表示する役割を果たします。

これで、ブラウザー、プッシュサービス、サービスワーカー、およびアプリケーションサーバーが協調して動作し、ユーザーにプッシュ通知を送信することがわかりました。 実装の詳細を見てみましょう。

Webプッシュクライアント

ユーザーの許可を求めることは一度限りのことです。 ユーザーがプッシュ通知を受信するための許可をすでに付与している場合は、再度質問することはできません。 権限の値はNotification.permissionに保存されます。

 /* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) } /* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }

上記のsubscribeメソッドでは、 userVisibleOnlyapplicationServerKeyを渡してサブスクリプショントークンを生成しています。 userVisibleOnlyプロパティは、サーバーから送信されたプッシュ通知がクライアントに表示されることをブラウザーに通知するため、常にtrueである必要があります。 applicationServerKeyの目的を理解するために、シナリオを考えてみましょう。

何千ものサブスクリプショントークンを入手した人は、これらのサブスクリプションに含まれるエンドポイントに通知を送信できます。 エンドポイントを一意のIDにリンクする方法はありません。 Webアプリケーションで生成されたサブスクリプショントークンに一意のIDを提供するために、VAPIDプロトコルを利用します。 VAPIDを使用すると、アプリケーションサーバーは、プッシュ通知を送信するときに、プッシュサービスに対して自発的に自分自身を識別します。 次のように2つのキーを生成します。

 const webpush = require('web-push') const vapidKeys = webpush.generateVAPIDKeys()

web-pushはnpmモジュールです。 vapidKeysには、1つの公開鍵と1つの秘密鍵があります。 上記で使用されているアプリケーションサーバーキーは公開キーです。

Webプッシュサーバー

Webプッシュサーバー(アプリケーションサーバー)の仕事は簡単です。 サブスクリプショントークンに通知ペイロードを送信します。

 const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options) const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options) const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)

WebプッシュライブラリのsendNotificationメソッドを使用します。

サービスワーカー

Service Workerは、通知を次のようにユーザーに表示します。

 self.addEventListener('push', (event) => { let options = { body: event.data.body, icon: 'images/example.png', } event.waitUntil( /* The showNotification method is available on the registration object of the service worker. The first parameter to showNotification method is the title of notification, and the second parameter is an object */ self.registration.showNotification(event.data.title, options) ) })

これまで、サービスワーカーがキャッシュを使用してリクエストを保存し、PWAを高速で信頼性の高いものにする方法と、Webプッシュ通知がユーザーの関与を維持する方法を見てきました。

オフラインサポートのためにクライアント側に大量のデータを保存するには、巨大なデータ構造が必要です。 Financial TimesPWAを見てみましょう。 このデータ構造の力を自分で目撃する必要があります。 ブラウザにURLをロードしてから、インターネット接続をオフにします。 ページをリロードします。 ああ! それはまだ機能していますか? です。 (私が言ったように、オフラインは新しい黒です。)データは有線から来ていません。 それは家から出されています。 Chromeデベロッパーツールの[アプリケーション]タブに移動します。 「ストレージ」の下に「IndexedDB」があります。

IndexedDBは、記事データをFinancial TimesPWAに保存します
Financial TimesPWAのIndexedDB。 (大プレビュー)

「Articles」オブジェクトストアをチェックし、アイテムのいずれかを展開して、魔法を自分で確認してください。 Financial Timesは、オフラインサポートのためにこのデータを保存しています。 大量のデータを保存できるこのデータ構造は、IndexedDBと呼ばれます。 IndexedDBは、構造化データを格納するためのJavaScriptベースのオブジェクト指向データベースです。 このデータベースには、さまざまな目的でさまざまなオブジェクトストアを作成できます。 たとえば、上の画像でわかるように、「Resources」、「ArticleImages」、および「Articles」はオブジェクトストアと呼ばれます。 オブジェクトストア内の各レコードは、キーで一意に識別されます。 IndexedDBは、ファイルやBLOBの保存にも使用できます。

書籍を保存するためのデータベースを作成して、IndexedDBを理解してみましょう。

 let openIdbRequest = window.indexedDB.open('booksdb', 1)

データベースbooksdbがまだ存在しない場合、上記のコードはbooksdbデータベースを作成します。 openメソッドの2番目のパラメーターは、データベースのバージョンです。 バージョンを指定すると、将来発生する可能性のあるスキーマ関連の変更が処理されます。 たとえば、 booksdbには現在1つのテーブルしかありませんが、アプリケーションが大きくなると、さらに2つのテーブルを追加する予定です。 データベースが更新されたスキーマと同期していることを確認するために、前のバージョンよりも高いバージョンを指定します。

openメソッドを呼び出しても、データベースはすぐには開きません。 これは、 IDBOpenDBRequestオブジェクトを返す非同期リクエストです。 このオブジェクトには、成功とエラーのプロパティがあります。 接続の状態を管理するには、これらのプロパティに適切なハンドラーを作成する必要があります。

 let dbInstance openIdbRequest.onsuccess = (event) => { dbInstance = event.target.result console.log('booksdb is opened successfully') } openIdbRequest.onerror = (event) => { console.log('There was an error in opening booksdb database') } openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result let objectstore = db.createObjectStore('books', { keyPath: 'id' }) }

オブジェクトストアの作成または変更を管理するために(オブジェクトストアはSQLベースのテーブルに類似しています。キーと値の構造を持っています)、 openIdbRequestオブジェクトでonupgradeneededメソッドが呼び出されます。 onupgradeneededメソッドは、バージョンが変更されるたびに呼び出されます。 上記のコードスニペットでは、IDとして一意のキーを使用してbooksオブジェクトストアを作成しています。

このコードをデプロイした後、 usersと呼ばれるもう1つのオブジェクトストアを作成する必要があるとします。 したがって、データベースのバージョンは2になります。

 let openIdbRequest = window.indexedDB.open('booksdb', 2) // New Version - 2 /* Success and error event handlers remain the same. The onupgradeneeded method gets called when the version of the database changes. */ openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result if (!db.objectStoreNames.contains('books')) { let objectstore = db.createObjectStore('books', { keyPath: 'id' }) } let oldVersion = event.oldVersion let newVersion = event.newVersion /* The users tables should be added for version 2. If the existing version is 1, it will be upgraded to 2, and the users object store will be created. */ if (oldVersion === 1) { db.createObjectStore('users', { keyPath: 'id' }) } }

オープンリクエストの成功イベントハンドラーにdbInstanceをキャッシュしました。 IndexedDBでデータを取得または追加するには、 dbInstanceを使用します。 書籍オブジェクトストアに書籍レコードを追加してみましょう。

 let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') let bookRecord = { id: '1', name: 'The Alchemist', author: 'Paulo Coelho' } let addBookRequest = objectstore.add(bookRecord) addBookRequest.onsuccess = (event) => { console.log('Book record added successfully') } addBookRequest.onerror = (event) => { console.log('There was an error in adding book record') }

特にオブジェクトストアにレコードを書き込むときに、 transactionsを利用します。 トランザクションは、データの整合性を確保するための操作の単なるラッパーです。 トランザクション内のアクションのいずれかが失敗した場合、データベースに対してアクションは実行されません。

putメソッドを使用して本のレコードを変更してみましょう。

 let modifyBookRequest = objectstore.put(bookRecord) // put method takes in an object as the parameter modifyBookRequest.onsuccess = (event) => { console.log('Book record updated successfully') }

getメソッドを使用して本のレコードを取得してみましょう。

 let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') /* get method takes in the id of the record */ let getBookRequest = objectstore.get(1) getBookRequest.onsuccess = (event) => { /* event.target.result contains the matched record */ console.log('Book record', event.target.result) } getBookRequest.onerror = (event) => { console.log('Error while retrieving the book record.') }

ホーム画面にアイコンを追加する

PWAとネイティブアプリケーションの違いはほとんどないので、PWAに最高の位置を提供することは理にかなっています。 WebサイトがPWAの基本的な基準(HTTPSでホストされ、Service Workerと統合され、 manifest.jsonを持っている)を満たし、ユーザーがWebページでしばらく時間を過ごした後、ブラウザーは下部にプロンプ​​トを呼び出して尋ねます以下に示すように、ユーザーはアプリをホーム画面に追加します。

ホーム画面にFinancialTimesPWAを追加するように求める
ホーム画面にFinancialTimesPWAを追加するように求めます。 (大プレビュー)

ユーザーが「ホーム画面にFTを追加」をクリックすると、PWAはホーム画面とアプリドロワーに足を踏み入れます。 ユーザーが自分の電話でアプリケーションを検索すると、検索クエリに一致するPWAが一覧表示されます。 それらはシステム設定にも表示されるため、ユーザーはそれらを簡単に管理できます。 この意味で、PWAはネイティブアプリケーションのように動作します。

PWAはmanifest.jsonを利用してこの機能を提供します。 簡単なmanifest.jsonファイルを見てみましょう。

 { "name": "Demo PWA", "short_name": "Demo", "start_url": "/?standalone", "background_color": "#9F0C3F", "theme_color": "#fff1e0", "display": "standalone", "icons": [{ "src": "/lib/img/icons/xxhdpi.png?v2", "sizes": "192x192" }] }

short_nameは、ユーザーのホーム画面とシステム設定に表示されます。 nameは、クロムプロンプトとスプラッシュ画面に表示されます。 スプラッシュ画面は、アプリを起動する準備ができているときにユーザーに表示されるものです。 start_urlは、アプリのメイン画面です。 これは、ユーザーがホーム画面のアイコンをタップしたときに得られるものです。 background_colorは、スプラッシュ画面で使用されます。 theme_colorは、ツールバーの色を設定します。 displayモードのstandalone値は、アプリが全画面モードで操作されることを示します(ブラウザーのツールバーを非表示にします)。 ユーザーがPWAをインストールすると、そのサイズは、ネイティブアプリケーションのメガバイトではなく、キロバイト単位になります。

サービスワーカー、Webプッシュ通知、IndexedDB、およびホーム画面の位置は、オフラインサポート、信頼性、およびエンゲージメントを補います。 サービスワーカーが生き返り、最初のロードで作業を開始するわけではないことに注意してください。 すべての静的アセットとその他のリソースがキャッシュされるまで、最初のロードは依然として低速です。 最初の負荷を最適化するためのいくつかの戦略を実装できます。

アセットのバンドル

HTML、スタイルシート、画像、JavaScriptを含むすべてのリソースは、サーバーから取得されます。 ファイルが多いほど、それらをフェッチするために必要なHTTPSリクエストが多くなります。 WebPackなどのバンドラーを使用して静的アセットをバンドルできるため、サーバーへのHTTPリクエストの数を減らすことができます。 WebPackは、コード分割(つまり、現在のページの読み込みに必要なファイルをすべてバンドルするのではなく、それらのファイルのみをバンドルする)やツリーシェイク(つまり、重複する依存関係を削除する)などの手法を使用して、バンドルをさらに最適化する優れた機能を果たします。インポートされたがコードで使用されていない依存関係)。

往復の削減

Webの速度が低下する主な理由の1つは、ネットワークの遅延です。 バイトがAからBに移動するのにかかる時間は、ネットワーク接続によって異なります。 たとえば、Wi-Fiを介した特定のラウンドトリップには、3G接続では50ミリ秒と500ミリ秒かかりますが、2G接続では2500ミリ秒かかります。 これらのリクエストはHTTPプロトコルを使用して送信されます。つまり、特定の接続がリクエストに使用されている間は、前のリクエストの応答が提供されるまで、他のリクエストに使用することはできません。 Webサイトでは6つの接続を使用してHTTP要求を行うことができるため、Webサイトは一度に6つの非同期HTTP要求を行うことができます。 平均的なWebサイトは約100件のリクエストを行います。 したがって、最大6つの接続を使用できる場合、ユーザーは1回の往復で約833ミリ秒を費やすことになります。 (計算は833ミリ秒-100⁄6 = 1666です。往復にかかる時間を計算しているため、1666を2で割る必要があります。)HTTP2を使用すると、所要時間が大幅に短縮されます。 HTTP2は接続ヘッドをブロックしないため、複数のリクエストを同時に送信できます。

ほとんどのHTTP応答には、 last-modifiedヘッダーとetagヘッダーが含まれています。 last-modifiedヘッダーはファイルが最後に変更された日付であり、 etagはファイルの内容に基づく一意の値です。 ファイルの内容が変更された場合にのみ変更されます。 キャッシュされたバージョンがすでにローカルで利用可能な場合は、これらのヘッダーの両方を使用して、ファイルの再ダウンロードを回避できます。 ブラウザにこのファイルのバージョンがローカルで利用できる場合は、次の2つのヘッダーのいずれかをリクエストに追加できます。

有効なキャッシュアセットのダウンロードを防ぐために、ETagと最終変更ヘッダーを追加します
ETagと最終変更ヘッダー。 (大プレビュー)

サーバーは、ファイルの内容が変更されたかどうかを確認できます。 ファイルの内容が変更されていない場合は、ステータスコード304(変更されていない)で応答します。

If-None-Match Header to prevent downloading of valid cached assets
If-None-Match Header. (大プレビュー)

This indicates to the browser to use the locally available cached version of the file. By doing all of this, we've prevented the file from being downloaded.

Faster responses are in now place, but our job is not done yet. We still have to parse the HTML, load the style sheets and make the web page interactive. It makes sense to show some empty boxes with a loader to the user, instead of a blank screen. While the HTML document is getting parsed, when it comes across <script src='asset.js'></script> , it will make a synchronous HTTP request to the server to fetch asset.js , and the whole parsing process will be paused until the response comes back. Imagine having a dozen of synchronous static asset references. These could very well be managed just by making use of the async keyword in script references, like <script src='asset.js' async></script> . With the introduction of the async keyword here, the browser will make an asynchronous request to fetch asset.js without hindering the parsing of the HTML. If a script file is required at a later stage, we can defer the downloading of that file until the entire HTML has been parsed. A script file can be deferred by using the defer keyword, like <script src='asset.js' defer></script> .

結論

We've learned a lot of many new things that make for a cool web application. Here's a summary of all of the things we've explored in this article:

  1. Service workers make good use of the cache to speed up the loading of assets.
  2. Web push notifications work under the hood.
  3. We use IndexedDB to store a massive amount of data.
  4. Some of the optimizations for instant first load, like using HTTP2 and adding headers like Etag , last-modified and If-None-Match , prevent the downloading of valid cached assets.

皆さん、これですべてです。