ウェブサイト検索

ViteとVueを使用してオフライン利用可能なプログレッシブウェブアプリニュースアグリゲーターを構築する


紹介

プログレッシブウェブアプリ(PWA)は、ネイティブのような機能と向上したユーザーエクスペリエンスを提供するモバイルフレンドリーなウェブアプリケーションです。主な利点には、どのデバイスにもアプリをインストールし、オフラインでアクセスできるという能力が含まれます。PWAの重要なコンポーネントは、ウェブアプリマニフェストとサービスワーカーです。構造化データを永続化するためにIndexedDB APIを利用できます。

このチュートリアルでは、VueとViteを使用して構築されたサンプルウェブサイトを、エンドユーザーが任意のデバイスにインストールできるオフラインで利用可能なPWAに変換します。ウェブアプリマニフェストと、ウェブアプリのプレゼンテーション層をキャッシュするためのサービスワーカーを作成します。

アプリに関する構造化データを保存するためにIndexDBも使用しますので、データはオフラインの間でも利用可能です。

前提条件

このチュートリアルに従うには、以下のものが必要です:

  • Node.jsの開発環境、Nodeバージョン18.15以上およびnpmバージョン9以上
  • あなたの開発環境におけるGitのインストール
  • VueとViteを使った開発経験があります。
  • ニュースAPIアカウントとAPIキー。こちらで登録して取得できます。

ステップ1 - プロジェクトの設定

このステップでは、サンプルプロジェクトをローカル開発環境にクローンして設定します。このプロジェクトは、迅速でスリムな開発体験のためにViteとVueJSを使用し、スタイリングにはTailwind CSSを使用しています。

まず始めに、GitHubからこのチュートリアルのためにすでに構築されたVueアプリをクローンします。あなたは「What's New」アプリで作業し、このアプリはNews APIからデータを集め、見出し、一般的なニュース項目、そしてパーソナライズされたカスタマイズ可能なニュースフィードとして分類し、提示します。

GitHubからプロジェクトをクローンするには、まず、GitHubウェブインターフェースの右上隅にあるフォークボタンをクリックして、What's Newプロジェクトのコピーを作成する必要があります。

次に、コマンドラインアプリを開いて、以下のコマンドを実行してください:

git clone https://github.com/{your-github-username}/whats-new.git

上のコマンドでハイライトされたURLの部分をあなたの実際のGitHubユーザー名に置き換える必要があります。

コマンドラインで `whats-new` ディレクトリに移動します:

cd whats-new

スタートコードブランチdo/starter-codeをチェックしてください:

git checkout do/starter-code

まだ登録してAPIキーを取得していない場合は、今すぐ行うべきです。

次に、お気に入りのコードエディタを使用して、.env.exampleファイルのコピーを作成し、.envに名前を変更します。

VITE_NEWS_API_KEYのyour-news-api-keyプレースホルダーをNews APIからのAPIキーで置き換え、.envファイルを保存してください。

プロジェクトの依存関係をインストールします:

npm install

アプリケーションを実行する:

npm run dev

ターミナルに以下の出力が表示されるはずです:

> whats-new@0.0.0 dev
> vite

  VITE v5.0.10  ready in 3639 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

ブラウザでアプリを起動すると、次の画面が表示されます:

ニュース項目付きの新着ホームページ

ステップ2 - ウェブアプリマニフェストの設定を作成する

ウェブアプリケーションマニフェストは、ウェブアプリケーションに関連付けられたメタデータを格納するための集中管理された場所を提供するJSONベースのファイルです。

プログレッシブウェブアプリケーションの開発では、ウェブマニフェストがブラウザに必要な情報、例えばアプリ名やアイコンなどを提供するために使用されます。

根本的に、プログレッシブウェブアプリ用のウェブアプリマニフェストには、有効であるために4つのトップレベルのキー(メンバーとも呼ばれる)を含める必要があります。それは、name、icons、start_url、およびdisplayです。

Webアプリのマニフェストファイルの設定を手動で作成し、必要なアイコンを生成することができます。しかし、このチュートリアルの目的のために、設定とアイコンの両方を生成するために、このPWAマニフェストジェネレーターツールを使用します。

ツールを起動し、以下のようにフォームに記入してください:

PWAマニフェストジェネレーターを使用してマニフェストの構成を生成しています。

アイコンフィールドには、whats-newプロジェクトフォルダのルートにあるapp-icon-image.pngファイルをアップロードしてください。

マニフェスト生成ボタンをクリックしてマニフェストを生成します。このツールは、ダウンロード可能な圧縮フォルダを生成します。圧縮フォルダからファイルを抽出してください。

あなたはmanifest.webmanifestファイルと、各アイコンの側面に接頭辞が付けられた画像ファイルを確認するべきです。それらの画像ファイルをアプリケーションの/publicフォルダーにコピーしてください。

manifest.webmanifestファイルを任意のテキストエディタで開いてください。その内容は、下の画像に示されているスニペットに似ているはずです。

生成されたマニフェストファイルのスクリーンショットを撮る。

次のセクションでは、ファイル内のJSONオブジェクトを使用する必要があります。

ステップ3 - ウェブアプリのマニフェストとサービスワーカーの生成

このステップでは、上記の設定を持つアプリケーションのウェブアプリケーションマニフェストを生成し、vite-plugin-pwaというViteプラグインを使用してサービスワーカーを作成します。

このカスタマイズ可能なプラグインを使用すると、既存のViteアプリケーションにPWA機能を追加できます。

プラグインをアプリケーションに追加するには、npmを介してインストールする必要があります:

npm install -D vite-plugin-pwa

次に、以下に示すように vite.config.js ファイルを編集して、アプリケーションのプラグインを設定します:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    VitePWA({})
  ],
})

vite-plugin-pwa パッケージは、プラグインを構成するためのさまざまなオプションのセットを引数として受け取る VitePWA という関数をエクスポートします。このプラグインに設定できるオプションの1つが manifest オプションです。このオプションを設定することで、プラグインがウェブアプリマニフェストを生成するために使用するマニフェスト構成をプラグインに提供することになります。

VitePWA()関数呼び出しのオブジェクトにmanifestキーを追加し、それを生成されたmanifest.webmanifestファイルからのJSONオブジェクトに設定し、変更を保存します。最終的には次のようなViteの構成オブジェクトになります:

export default defineConfig({
  plugins: [
    vue(),
    VitePWA({
      manifest: {
        "theme_color": "#0F172A",
        "background_color": "#f5f8fa",
        "display": "standalone",
        "scope": "/",
        "start_url": "/",
        "name": "What's New - Vue News Aggregator Site",
        "short_name": "What's New",
        "description": "A news aggregator pulling news items from News API.",
        "icons": [
          {
            "src": "/icon-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
          },
          {
            "src": "/icon-256x256.png",
            "sizes": "256x256",
            "type": "image/png"
          },
          {
            "src": "/icon-384x384.png",
            "sizes": "384x384",
            "type": "image/png"
          },
          {
            "src": "/icon-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
          }
        ]
      }
    })
  ],
})

あなたは今、作業可能なインストール可能なPWAを持っているはずです。アプリをビルドするたびに、プラグインはブラウザのためのウェブアプリマニフェストを生成し、それをアプリケーションのエントリーポイントである/に設定します。また、サービスワーカーを作成し、そのスクリプトをブラウザに登録するために追加します。これらのアーティファクトがブラウザに存在するかどうかを確認し、アプリが現在PWAであることを確認するにはどうすればよいですか?

PWAを確認しています

ブラウザの開発者ツールを開き、アプリケーションタブに移動する必要があります。このチュートリアルでは、Chromeブラウザをリファレンスとして使用します。Chromeで開発者ツールを開くには、CTRL+SHIFT+I(Windows)またはOPTION+CMD+I(Mac)のキーを押します。サイドバーのアプリケーションタブの下に、アプリケーションと呼ばれるセクションが表示されているはずです。そのセクションにはマニフェストと呼ばれるサブリンクがあります。そのリンクをクリックすると、上記のプラグインに提供した情報に関連する情報が表示されるはずです。ただし、この段階では、以下の画面が表示されます。

アプリケーションタブのマニフェストセクションが空です

さらに、サービスワーカーセクションには登録されたサービスワーカーがないことがわかります。

また、ストレージセクションの下で、キャッシュストレージをクリックすると、いくつかのキャッシュ情報が見つかるはずですが、やはりマニフェスト*セクションと同様に、それは空であることがわかります。

ブラウザツールバーセクションに矢印下向きアイコンのボタンがあるデスクトップも見つけるべきです。

インストールアイコンボタンのあるツールバー

これらの画面は現在空白で、開発モードで実行しているため、ブラウザでアプリケーションを表示している間はボタンを見ることができません。この動作は予期されるもので、あなたのアプリケーションが開発モードで実行しているため、プラグインがアーティファクトを生成し、本番モードで期待される動作をデフォルトで提供するように設定されています。次の二つのセクションでは、この問題に対処するためのオプションについて学びます。

アプリの構築とプレビュー

アプリをプレビューするオプションがあります。npm run buildおよびnpm run previewコマンドを連続して実行して、PWAを確認し、ブラウザでURLを起動してください。

ビルドプレビューのURLは、開発ビルドのために開発サーバーによって生成されたURLとは異なります。

上記のプロセスを繰り返します。今、あなたはマニフェスト、キャッシュストレージにいくつかのアイテム、そしてインストールボタン(黄色でマークされています)を見ることができるはずです。以下の画像を参照してください:

マニフェストセクションの設定データ

キャッシュストレージセクション

このオプションは、コードに変更を加えるたびにアプリを再構築する必要があり、かなり不便です。次のオプションは、以下に示すように、この不便さを解消します。ただし、次の主要なステップで発見するいくつかの注意点があります。しかし、チュートリアルのこの時点では、現在の目的のために、開発モードでプラグインを使用する方法を学ぶことになります。

Vite PWAプラグインの設定でdevOptionsオプションを使用すること

vite-plugin-pwaパッケージは、v0.11.13から開発モードでアプリをPWAとして表示する機能をサポートするためのdevOptionsオプションを提供します。以下のコードブロックで強調表示された行のように、enabledサブオプションをtrueに設定します:

export default defineConfig({
  plugins: [
    vue(),
    VitePWA({
      devOptions: {
        enabled: true
      },
      manifest: {...}
    })
  ],
})

変更を保存して開発サーバーを再起動すると、以下の強調表示された行に示されているように、コンソール出力にいくつかの追加の行が表示されることに気付くでしょう。

> whats-new@0.0.0 dev
> vite

  VITE v5.0.10  ready in 1490 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

PWA v0.17.4
mode      generateSW
precache  2 entries (0.12 KiB)
files generated
  dev-dist\sw.js
  dev-dist\workbox-9637eeee.js
12:28:22 p.m. [vite] vite.config.js changed, restarting server...
12:28:22 p.m. [vite] server restarted.

ログは、プラグインが使用するように設定されているモードを示しています。これはサービスワーカーを生成するように設定されているため、あなたが見ることができる generateSW 値が表示されています。また、ログには dev-dist フォルダに生成されたファイルがいくつかあることも示されています。sw.js ファイルは、プラグインによって作成されたサービスワーカーのスクリプトです。このスクリプトにはキャッシング戦略が含まれています。プラグインは workbox-build パッケージを利用しているため、サービスワーカーを構築するために使用されるID関連のワークボックスファイルも生成します。最後に、ログに表示されていない registerSW.js というファイルも生成されます。registerSW.js ファイルは、ブラウザがサービスワーカーをサポートしているかどうかを確認し、アプリケーションがロードされたときにサービスワーカーを登録するスクリプトです。

アプリケーションが起動してブラウザに読み込まれると、registerSW.jsファイルがsw.jsで作成されたサービスワーカーを登録し、私たちのブラウザに対する更新が表示されます。上記の手順を再試行して、ブラウザのマニフェストとキャッシュストレージインターフェースを確認してください。

さらに、以下に示すように、アプリケーションタブのサービス ワーカーセクションでアクティブなサービス ワーカーを見ることができます。

アクティブサービスワーカー

ステップ4 - アプリケーションファイルとアセットのキャッシュ処理

チュートリアルのこの段階で、あなたのアプリケーションはPWAですが、まだオフライン対応ではありません。これを確認したい場合は、ブラウザのデベロッパーツールのネットワークタブに移動し、オフラインで実行するように設定してください。すると、空の画面が表示されることがわかります。

アプリケーションファイルやアセットをキャッシュしてオフライン動作をサポートする必要があります。そのためには、以下の強調表示した行のように、Vite PWAプラグインの設定に追加の修正を行う必要があります。

export default defineConfig({
  plugins: [
    vue(),
    VitePWA({
      devOptions: {...},
      includeAssets: [
        "**/*"
      ],
      manifest: {...},
      workbox: {
        globPatterns: ["**/*.{js,css,html,png}"]
      }
    })
  ],
})

includeAssetsオプションは、プラグインに対してサービスワーカーのプリキャッシュにpublicフォルダ内に見つかった他の静的資産を含めるよう指示します。このような静的資産の例としては、あなたのウェブサイトのファビコン、SVGファイル、publicフォルダに追加したフォントファイルなどがあります。

先にpublicディレクトリに追加されたアイコンは、プラグイン設定のmanifestオプションの下に追加されたため、デフォルトで含まれています。

workboxの下のglobPatternsオプションは、キャッシュしたいアプリケーションファイルの種類を制限するために使用されます。この場合、メインアプリケーションファイルである.js、.css、.html、および.pngファイルをキャッシュしたいと考えています。

これらの新しい変更により、アプリケーションはオフラインモードで実行できるようになりました。それを実際に見るには、別のターミナルを開き、現在開発モードで実行している場合は npm run build を使ってアプリケーションを再ビルドする必要があります。

あなたはこう尋ねるかもしれません:‘なぜこの状況は異なるのですか?’ ‘devOptionsオプションは、開発モードでプログレッシブウェブアプリケーションを作成することに関するすべてのことを処理するのに十分ではありませんか?’ これらの質問への答えは、開発サーバーを実行する際に生成されるビルドと本番ビルドの違いを見直すことに帰着します。

Viteの開発モードでアプリケーションを実行する場合、開発ビルドはメモリ内で管理され、ディスクには書き込まれません。言い換えれば、本番アプリケーションビルドで得られるようなdistフォルダ内のアプリケーションファイルのビルド出力はありません。そのため、サービスワーカーがプレキャッシュに追加するものは何もありません。プレキャッシュに追加するものがなければ、ブラウザのキャッシュストレージには何もキャッシュされないことになります。デフォルトでプレキャッシュに追加される唯一のアイテムは、エントリーポイントであるindex.htmlファイルとregisterSW.jsファイルです。

index.htmlファイルは、アプリケーションに必要なすべてを読み込むためにmain.jsファイルを必要とします。あなたがすでに知っているように、このファイルやそれがインポートする他のファイルはキャッシュされていないため、オフラインアプリケーションとして開発モードでアプリケーションを実行すると、常に空白の画面が表示されます。

キャッシュされていないことを確認するために、アプリケーションのプロダクションビルドを実行した後、dev-distディレクトリとdistディレクトリの両方にあるsw.jsファイルの内容を比較してください。あなたはprecacheAndRoute関数に引数として渡されるアイテムに注意を払うべきです。

アプリケーションのプロダクションビルドを npm run preview を使用して実行し、ブラウザで起動します。開発ビルドを実行しているブラウザタブの横に配置できます。ブラウザの開発者ツールの ネットワーク タブに移動し、オフラインで実行するように設定してページをリフレッシュします。それでもアプリケーションの画面を見ることができるはずです。

これは、あなたのアプリケーションが読み込まれたときに、サービスワーカーが登録され、ビルドファイルがキャッシュされたためです。ファイルを見るには、ApplicationタブのCache storageセクションに移動し、workbox-precache-*とラベル付けされたキャッシュされたアイテムをクリックしてください。あなたのアプリケーションのdistフォルダーに対応するファイル名が表示されるはずです。

こちらが参考用のスクリーンショットです。

キャッシュストレージとdistファイル

あなたはまた、開発サーバーのブラウザタブのキャッシュと比較することもできます。こちらはあなたの参考のための別のスクリーンショットです:

キャッシュストレージ なしでのディストファイル

注意すべきいくつかの重要な点は、エントリーの数(青でマーク)、ポート番号(黄でマーク)およびキャッシュされたファイルのテーブル(赤でマーク)の違いです。

このポイントまでたどり着いたなら、おめでとうございます!これで、アプリケーションをオフラインで利用できるように成功しました。次のステップは何ですか?あなたのアプリケーションはAPIから取得したデータを表示します。現在、アプリケーションファイルだけがキャッシュされています。次のセクションでは、このギャップを埋めるためにIndexedDBを追加します。

ステップ5 - IndexDBを使用したアプリケーションデータのキャッシング

IndexedDBは、クライアント側で構造化データやBlob(バイナリデータ)を保存するためのブラウザAPIです。これは、JavaScriptのオブジェクトに似た構造でデータをキーと値のペアとして保存するトランザクショナルデータベースシステムです。

このセクションでは、アプリケーションにIndexedDBデータベースを追加し、それを使用してアプリケーションのデータキャッシングを管理します。

アプリを更新してAPIからデータを取得します。

まず、アプリで表示されるデータの取得方法にいくつかの変更を加える必要があります。現在、表示されているデータはアプリ内変数として保存されています。

NewsItems.vueファイルを開きます。testNewsItemsData変数と関連するコメントを削除します。

以下に示された<script setup>タグ内にあるgetCustomizedTabNewsItemsという関数の部分を見つけ、コメントに書かれていることを実行してください。

const getCustomizedTabNewsItems = () => {
  //...
  if (definedCustomizations) {
    //...
    /** TODO: Remove the the line below after setting up your API KEY and delete this comment */
    newsItems.value = [
      {
        source: {
          id: 'buzzfeed',
          name: 'Buzzfeed'
        },
        author: 'Melanie Aman',
        title: '37 Beauty Products Under $25 That Don\'t Skimp On Results',
        description: 'A heavy serving of getting more than you pay for ...don\'t mind if we do.View Entire Post ›',
        url: 'https://www.buzzfeed.com/melanie_aman/beauty-products-under-25-that-dont-skimp-on-results',
        urlToImage: 'https://img.buzzfeed.com/buzzfeed-static/static/2023-12/11/20/enhanced/46ff3dce9b4d/original-451-1702327901-3.jpg?crop=4205:2208;0,0%26downsize=1250:*',
        publishedAt: '2023-12-24T09:00:03Z',
        content: 'Psst! Bio-Oil contains retinol, which accelerates skin turnover but can make you more sensitive to the sun so don\'t forget your sunscreen!\r\nPromising review: \'I was skeptical of how amazing the revie… [+689 chars]'
      },
      // ...
    ]

    /** TODO: Uncomment after setting up your API KEY */
    // const { fetchedNewsItems, getNewsItems } = useNewsItems(requestUrl)
    // nextTick(async () => {
    //   await getNewsItems()
    //   newsItems.value = fetchedNewsItems.value
    // })
  }
}

ファイルの中で <script setup> タグのさらに下に進み、以下にハイライトされたコードを見つけてください:

if (props.tab.id === APPLICATION_TABS[2].id && props.retrieveCustomCuratedContent) {
  // ...
} else if (props.tab.id === APPLICATION_TABS[2].id && !props.retrieveCustomCuratedContent) {
  // ...
} else {
  // get news items for other tabs ...
  // TODO: Remove the the line below after setting up your API KEY and delete this comment
  newsItems.value = testNewsItemsData

  /** TODO: Uncomment after setting up your API KEY */
  // const { fetchedNewsItems, getNewsItems } = useNewsItems(requestUrl)
  // nextTick(async () => {
  //   await getNewsItems()
  //   newsItems.value = fetchedNewsItems.value
  // })
}

また、コメントに書いてあることも実行してください。

最後に、SourceToggleTokens.vueファイルを開いて、下に示されているコードを見つけてください:

watch(sourcesRetrievalParameters, async (newParams) => {
  // ...

  /** TODO: Uncomment after setting up your API KEY */
  // const { newsSources: apiNewsSources, getNewsSources } = useNewsSources(queryString.value)
  // await getNewsSources()
  // newsSources.value = apiNewsSources.value
})

指示に従ってコードのコメントを解除し、変更を保存してください。

最初に期待通りにAPIキーを追加し、上記のようにファイルを更新した場合、ホームページに異なるニュース項目が表示されるはずです。

APIからのニュースアイテム

さらに、上に青で強調表示された設定アイコンをクリックすると、以下の画面に示されているような多数のニュースソースの長いリストが表示されます。

APIからのニュースソース

何を保存するかを決定する

これまで、あなたはアプリケーションのビジュアルにのみ焦点を当ててきました。何を保存するかを理解するために、アプリケーションがニュースAPIをどのように問い合わせるかを簡単に説明します。

アプリケーションには3つのタブがあります:ヘッドライン、探る、および私のためにキュレーションされた。constants/general.jsというファイルには、APPLICATION_TABSという変数に各タブに関する情報が格納されています。各タブオブジェクトには、apiCallURLとapiCallParametersという2つのプロパティがあります。各apiCallURLの値は、News APIを問い合わせるための指定されたエンドポイントで始まる文字列で、カッコ波括弧内のプレースホルダー値がそのエンドポイントの期待されるリクエストパラメータを表しています。それらのプレースホルダー値は、そのタブの対応するapiCallParameters配列の要素と同じです。以下に示すヘッドラインタブのオブジェクトを見てみましょう:

export const APPLICATION_TABS = [
  {
    name: 'Headlines',
    id: 'headlines-tab',
    target: '#headlines',
    ariaControls: 'headlines',
    apiCallURL: 'https://newsapi.org/v2/top-headlines?country={country}&category={category}',
    apiCallParameters: ['category', 'country']
  },
  { // ... },
  { //... }
]

URLは、dynamic-string-parser.jsユーティリティのparseという関数を使用して完全なエンドポイントを生成するためにフォーマットされます。次に、それはuseNewsItemsという関数への引数として渡されます。useNewsItems関数はコンポーザブルであり、2つのものを返します:fetchedNewsItems - APIからのデータを保持する反応型配列と、getNewsItems - 実際のネットワーク取得を行い、fetchedNewsItemsオブジェクトを更新する関数です。

私たちはgetNewsItems関数を呼び出し、返された配列をnewsItemsに設定します。

以下のスニペットはgetNewsItems関数を示しています。

async function getNewsItems() {
  try {
   const apiResponse = await fetch(url.value, {
    headers: {
     'X-Api-Key': import.meta.env.VITE_NEWS_API_KEY
    }
   })
   const data = await apiResponse.json()
   fetchedNewsItems.value = data.articles
   count.value = data.totalResults
  } catch (error) {

  }
 }

次のセクションで作成するindexedDBデータベースにAPIからのニュース項目を保存するために、この関数を更新します。

アプリ内でIndexedDBデータベースをインストールして設定すること

最初に、npmからidbパッケージをインストールする必要があります:

npm install idb

idb パッケージは、クライアントサイドストレージと対話するために使用される IndexedDB API の軽量ラッパーです。IndexedDB API は、その生の形式で使用する際に少し使いづらいとされ、ユーザーフレンドリーではありません。このパッケージは、使用を簡単にするための改良を提供します。

次に、useIDBという名前のコンポーザブルファイルを作成し、以下のコードを追加する必要があります。このコンポーザブルには、ニュースAPIからデータを取得するコンポーネントに組み込むことができる再利用可能なコードが含まれています。

import { openDB } from 'idb'
import { ref } from 'vue'

const versionNumber = ref(1)

const useIDB = () => {
    const db = ref(null)

  return {
   db,
   versionNumber
  }
}

export default useIDB

このコンポーザブルは、すぐに idb の openDB 関数を使用してインスタンス化する db 変数を返します。また、グローバルな versionNumber も返します。versionNumber がファイルの最上部にある理由は、アプリケーションの実行中にいつでもグローバルなものであることを保証するためであり、コンポーザブルを呼び出すたびに新しいものを作成しないためです。

以下のスニペットは上記を基にしており、openDBを使用してデータベースを開いた後、返された値をdbに割り当てます。

const useIDB = () => {
    const db = ref(null)
    const getDB = async (version, objectStoreName, keyPath) => {
        versionNumber.value += 1
        db.value = await openDB('whats-new', version, {
            upgrade(db, oldVersion) {
                if (version === 1 && oldVersion === 0) {
                    db.createObjectStore(objectStoreName, {
                        keyPath
                    })
                }

                if (version > 1) {
                    if (!db.objectStoreNames.contains(objectStoreName)) {
                        db.createObjectStore(objectStoreName, {
                            keyPath
                        })
                    }
                }
            }
        })
    }

    return {
        db,
        versionNumber,
        getDB
    }
}

getDBと呼ばれる新しい関数は、3つの引数version、objectStoreName、keyPathを取る非同期関数です。IndexedDB APIについて話すときに使われるいくつかの用語があります。1つはデータベースのバージョンです。これは、データベースを何らかの方法で修正する必要があるときに役立ちます。もう1つはオブジェクトストアの概念です。

オブジェクトストレージはSQLデータベースのテーブルに似ていると考えてください。

最後に、keyPathというものがあります。keyPathは、オブジェクトストアに追加したいデータの中でユニークなフィールドまたはカラム名です。

getDB 関数は、上のコードに見られるように、データベースを作成するために消費するクライアントコンポーネントからこの3つのパラメータを必要とします。versionNumber をインクリメントした後、db の値は、作成したいデータベースの名前 - 'whats-new' - とバージョン番号を引数として取る openDB の呼び出しの結果に設定されます。

openDB 関数には、3 番目の引数としてオブジェクトもあります。このオブジェクトには、データベースをセットアップするために使用されるイベントが含まれています。

このユースケースでは、upgradeDb イベントだけが必要です。upgradeDb イベントは、作成されたデータベースのバージョン番号が既存のデータベースのそれよりも大きいときに実行されます。もしoldVersion - ブラウザに存在するデータベースの現在のバージョン番号 - が 0 で、作成したいバージョンが 1 の場合、提供されたobjectStoreName と keyPath でオブジェクトストアを作成することになります。

しかし、作成したいバージョンが1より大きい場合は、指定されたobjectStoreNameのオブジェクトストアが存在しない場合にのみ、objectStoreNameおよびkeyPathを使用してオブジェクトストアを作成します。

最終的に、getDB関数を返します。

このコンポーザブルが最後に必要とすることは、db からデータを取得するためのインターフェースを提供することです。以下のコードスニペットを参照して、どのようにそれを実現するかを見てください。

const useIDB = () => {
    const db = ref(null)
    // getDB

    const getDataFromObjectStore = async (objectStoreName) => {
        let data

        if (db.value) {
            data = await db.value.getAll(objectStoreName)
        } else {
            const db = await openDB('whats-new', undefined)
            data = await db.getAll(objectStoreName)
        }

        return data
    }

    return {
        //...
        getDataFromObjectStore
    }
}

もしdb.valueがnullでない場合、オブジェクトストアからすべてのデータを取得します。そうでない場合は、ブラウザで現在のdbを開き、すべてのデータを取得します。

作成したコンポーザブルを使用して、以下に示すように getNewsItems に必要な変更を加えることができます:

async function getNewsItems() {
  const { db, getDB, versionNumber, getDataFromObjectStore } = useIDB()

  try {
   // ... apiResponse
   const data = await apiResponse.json()
   // setting fetchedNewsItems

   await getDB(versionNumber.value, url.value, 'url')
   data.articles.forEach(async (article) => {
    await db.value.put(url.value, article)
   })
  } catch (error) {
   if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
    const cachedItems = await getDataFromObjectStore(url.value)
    fetchedNewsItems.value = cachedItems
   }
  }
 }

APIから記事を取得する試みが成功した場合、getDBを呼び出し、indexedDB APIのput関数を使ってデータベースに記事を保存します。put関数は、最初のパラメータとして指定されたオブジェクトストアにアイテムを追加します。

フェッチの試行が失敗した場合、エラーがフェッチエラーであることを確認し、getDataFromObjectStoreを使用してデータベースからデータを取得します。

useNewsSourcesのgetNewsSourcesについても、以下のように同様に行うことができます:

async function getNewsSources() {
  const { db, getDB, versionNumber, getDataFromObjectStore } = useIDB()
    try {
      // apiResponse
      const data = await apiResponse.json()
      // ...
      await getDB(versionNumber.value, baseUrl.value, 'id')
 data.sources.forEach(async (source) => {
  await db.value.put(baseUrl.value, source)
 })
    } catch (error) {
  if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
   const cachedItems = await getDataFromObjectStore(baseUrl.value)
   newsSources.value = cachedItems
  }
    }
  }

変更を保存し、アプリを再構築して、以下のコマンドを使用してプレビュー モードで実行します。

npm run build
npm run preview

ブラウザでアプリを起動してください。ブラウザのデベロッパーツールのアプリケーションタブにあるストレージセクションのIndexedDBサブセクションの下に新しいエントリが表示されるはずです。

新しいIndexedDBエントリ

次に、ネットワークタブに移動します。ネットワーク制限オプションをオフラインに設定します。以下のように表示されます。小さな黄色い警告アイコンが見えるはずです。

ネットワークタブがオフラインに設定されました

アプリを再読み込みしてください。アプリケーションは以前のように読み込まれるはずです.

一時的にインターネットなしの画面が表示されることがありますが、すぐにアプリは起動します。

結論

この記事では、VueのシングルページアプリケーションをサービスワーカーとIndexedDB APIを使用してプログレッシブウェブアプリに変換しました。次のステップとして、ユーザーにページの新しい更新を通知し、更新されたバージョンを見るためにページの再読み込みを促すことが考えられます。vite-plugin-pwaパッケージのドキュメントにあるVueのセクションは、良い出発点を提供します。

関連記事: