Cloud Firestore

この Codelab は、Google Developers トレーニング チームが開発した「Developing Progressive Web Apps」トレーニング コースの一部です。Codelab を順番に扱うことで、このコースを最大限に活用できます。

コースの詳細については、プログレッシブ ウェブアプリ開発の概要をご覧ください。

学習内容

  • Firebase で Cloud Firestore プロジェクトを設定する方法
  • Firestore による基本的な読み取りと書き込み
  • (省略可)オフラインの Cloud Firestore の使用方法

前提となる知識

  • 基本的な HTML、CSS、JavaScript
  • ES2015 約束
  • コマンドラインからコマンドを実行する方法
  • (省略可)Service Worker と Workbox に関する知識

必要なもの

  • ターミナル/シェルにアクセスできるパソコン
  • インターネットへの接続
  • テキスト エディタ
  • Nodenpm

GitHub から pwa-training-labs リポジトリをダウンロードまたは複製し、必要に応じて LTS バージョンの Node.js をインストールします。

任意のテキスト エディタで firestore-lab/project/ フォルダを開きます。project/ フォルダは、ラボを作成する場所です。

新しい Cloud Firestore プロジェクトを作成します。

  1. Firebase コンソールを開き、新しいプロジェクトを作成します。
  2. [データベース] セクションで [Firestore ベータ版を試す] をクリックします。
  3. テストモード」で開始する方法を確認する
  4. [有効にする] をクリックします。

注: 同じプロジェクトで Cloud Firestore と Cloud Datastore の両方を使用することはできません。両方を使用すると、App Engine を使用するアプリが影響を受ける可能性があります。プロジェクトですでに Cloud Datastore を使用している場合は、別のプロジェクトで Cloud Firestore を使用してみてください。

次に、</body> 終了タグの直前に、次のスクリプトを index.html に追加します。

index.html

<script src="/__/firebase/4.9.0/firebase-app.js"></script>
<script src="/__/firebase/4.9.0/firebase-auth.js"></script>
<script src="/__/firebase/4.9.0/firebase-firestore.js"></script>
<script src="/__/firebase/init.js"></script>

このコードは、アプリが Firebase Hosting を使用してホストされている場合、必要なライブラリをインポートします。

Firebase CLI の Cloud Firestore 対応バージョンを入手する必要があります。

コマンドライン ウィンドウを開き、ディレクトリを project/ フォルダに変更します。

そのうえで、次のコマンドを実行します。

npm install -g firebase-tools

ツールのインストールが完了したら、ログインして、Firebase CLI で Firebase プロジェクトとやり取りできるようにします。

firebase login

次に、project/ ディレクトリで次のコマンドを実行して構成ファイルを作成し、Firebase アプリを初期化します。

firebase init

上記のコマンドを実行すると、ターミナルにプロンプトが表示されます。画面の指示に沿って操作します。

  1. (Firestore のオプションにカーソルを移動して Space を押して)Firestore を選択し、return を押します
  2. 作成した Firebase プロジェクトを選択して return を押します
  3. return を押して、Firestore ルールのデフォルトのファイルを使用します。
  4. return キーを押して、Firestore インデックスのデフォルトのファイルを使用します。

ウェブアプリを実行すると、使用すべき Firebase(および Firestore)プロジェクトが自動的に認識されます。ただし、アプリを提供できるように、さらにいくつかの手順が必要です。

firebase.json ファイルを開き、hosting 構成を追加します。ファイル全体は次のようになります。

firebase.json

{
  "hosting": {
    "public": "./",
    "ignore": [
      "firebase.json",
      "database-rules.json",
      "storage.rules",
      "functions"
    ],
    "headers": [
      {
        "source": "**/*.@(js|html)",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "max-age=0"
          }
        ]
      }
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  },
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  }
}

説明

hosting 設定では、Firebase によるアプリのホスト方法を定義できます。Firebase 開発用サーバーを使用するには、後のステップでこのオプションを定義する必要があります。詳細については、デプロイ構成のリファレンスをご覧ください。

認証はこの Codelab の主要なトピックではありませんが、アプリではなんらかの形の認証を行うことが重要です。匿名ログインを使用するため、ユーザーは通知なくログインできます。

匿名認証は、Firebase コンソールを使用してアプリで有効にすることができます。次のコマンドを実行して、[ログイン プロバイダ] 構成ページに自動的に移動します。

firebase open auth

または、プロジェクトの Firebase コンソールで、[開発]、[認証]、[ログイン方法] の順に移動します。

このページで [匿名] をクリックし、[有効にする] をクリックして、[保存] をクリックします。

アプリで実際に作業を開始する準備が整いました。firebase コマンドを使用してローカルで実行します。

firebase serve

ブラウザを開いて localhost:5000 を表示します。Firebase プロジェクトに接続されているスペースレースのコピーが表示されます。

アプリは自動的にプロジェクトに接続され、匿名ユーザーとして自動的にログインされていました。

このセクションでは、アプリの UI を実装できるように、Firestore にデータを書き込みます。これは Firebase コンソールを使用して手動で行うこともできますが、基本的な Firestore の書き込みを行うため、アプリ内で実行します。

アプリの主なモデル オブジェクトは宇宙船です。Firestore のデータはドキュメントで表され、コレクションとサブコレクションにまとめられます。このアプリでは、各船は「ships;ships」という最上位コレクションにドキュメントとして保存されます。Firestore データモデルの詳細については、こちらのドキュメントをご覧ください。

このアプリの Cloud Firestore データベースには現在データがありません。プロジェクトの Firebase コンソールで Develop &gg; Database に移動し、データベースが空であることを確認します。

アプリにデータを追加するための関数を実装しましょう。

scripts/SpaceRace.Data.js を開き、関数 SpaceRace.prototype.addShip を見つけます。関数全体を以下のコードで置き換えます。

SpaceRace.Data.js

SpaceRace.prototype.addShip = function(data) {
  const collection = firebase.firestore().collection('ships');
  return collection.add(data);
};

上記のコードは、ships Firestore コレクションに新しいドキュメントを追加します。この関数は最初に ships コレクションへの参照を取得し、adddata になります。ドキュメント data はプレーンな JavaScript オブジェクトから取得されます。

セキュリティ ルール

もうすぐです。Cloud Firestore にドキュメントを書き込むには、Firestore のセキュリティ ルールを構成する必要があります。これらのルールでは、データベースのどの部分を読み取りおよび書き込み可能にするのか、またどのユーザーがどのデータベースを使用するのかを記述します。現時点では、認証されたすべてのユーザーはデータベース全体を読み書きできます。これは本番環境アプリにとっては制限が大きすぎるかもしれませんが、開発プロセスでは十分なテストを行い、テスト中に常に認証の問題が発生しないようにします。

firestore.rules というファイルを開き、ファイル全体を次のコードで置き換えます。

firestore.rules

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      // Only authenticated users can read or write data
      allow read, write: if request.auth != null;
    }
  }
}

次に、コマンドラインで以下を実行します。

firebase deploy --only firestore:rules

これにより、firestore.rules ファイルが Firebase プロジェクトにデプロイされます。

または、Firebase コンソールを開いて、[Develop & > Database > Rules] に移動して、デフォルトのルールを新しいルールに置き換えます。

注: セキュリティ ルールの詳細については、セキュリティ ルールのドキュメントをご覧ください。

ページを更新し、[モックデータを追加] ボタンをタップします。このボタンは、前に定義した addShip 関数を使用する、SpaceRace.Mock.js で定義された addMockShips 関数を呼び出します。addMockShips は配送ドキュメントのバッチを作成します(ただし、これはまだアプリに表示されません)。データの取得を実装する必要があります。

次に、Firebase コンソールの [データベース] タブに移動します。ships コレクションに新しいエントリが表示されます(ページの更新が必要になる場合があります)。

これで、ウェブアプリから Cloud Firestore にデータが書き込まれました。次のセクションでは、Firestore からデータを取得してアプリに表示する方法を学びます。

このセクションでは、Firestore からデータを取得してアプリに表示する方法を学びます。主なステップは、クエリの作成と、スナップショット リスナーの追加の 2 つです。このリスナーは、クエリと一致する既存のデータをすべて通知し、リアルタイムで更新を受信します。

船舶のリストを提供するクエリを構築しましょう。SpaceRace.prototype.getAllShips() メソッドを次のコードに置き換えます。

SpaceRace.Data.js

SpaceRace.prototype.getAllShips = function(render) {
  const query = firebase.firestore()
    .collection('ships')
    .limit(50);
  this.getDocumentsInQuery(query, render);
};

以下のスニペットでは、「ships」という名前のトップレベル コレクションから、最大 50 隻の船を取得するクエリを作成します。このクエリを宣言したら、クエリを getDocumentsInQuery() メソッドに渡します。このメソッドがデータの読み込みとレンダリングを行います。このメソッドは、スナップショット リスナーを使用します。SpaceRace.prototype.getDocumentsInQuery() メソッドを次のコードに置き換えます。

SpaceRace.Data.js

SpaceRace.prototype.getDocumentsInQuery = function(query, render) {
  query.onSnapshot(snapshot => {
    if (!snapshot.size) return render();
    snapshot.docChanges.forEach(change => {
      if (change.type === 'added') {
        render(change.doc);
      }
      else if (change.type === 'removed') {
        document.getElementById(change.doc.id).remove();
      }
    });
  });
};

上記のコードの query.onSnapshot は、クエリの結果に変更が加えられるたびにコールバック引数をトリガーします。初回の場合は、クエリの結果セット全体(Firestore の ships コレクション)でコールバックがトリガーされます。個々のドキュメントの変更には、どのように変更されたかを示す type プロパティがあります。ドキュメントが added の場合、ドキュメントは render 関数に渡されます。ドキュメントが removed の場合、対応する発送カードは DOM から削除されます。

両方のデータ取得メソッドを実装したので、アプリを更新して、Cloud Firestore に追加した(および Firestore コンソールに表示される)船がアプリに表示されることを確認します。このセクションを正常に完了すると、アプリは Cloud Firestore でデータの読み取りと書き込みを行うことができます。

アプリのフォームに記入し、[追加] をクリックして、カスタム船を追加してみましょう。発送リストが変更されると、このリスナーは自動的に更新されます。Firebase コンソールに移動して、手作業で船を追加しましょう。すぐにサイトがサイトに表示されます。

注: Query.get() メソッドを使用してリアルタイムの更新をリッスンする代わりに、Firestore からドキュメントを一度だけフェッチすることもできます。

コレクションから特定の船を削除する関数を作成しましょう。

SpaceRace.Data.jsSpaceRace.prototype.deleteShip() メソッドを次のコードで置き換えます。

SpaceRace.Data.js

SpaceRace.prototype.deleteShip = function(id) {
  const collection = firebase.firestore().collection('ships');
  return collection.doc(id).delete()
    .catch(function(error) {
      console.error('Error removing document: ', error);
    });
};

これを実装すると、船のカードの右上隅にある [X] をクリックすると、特定の船を削除できます。ファイルを保存してアプリを更新し、船を削除します。

また、Cloud Firestore は、オフライン データの永続性もサポートしています。この機能を使用すると、アプリのデータのローカルコピーがキャッシュに保存され、ユーザーのオフライン時に変更して、接続が復元されると同期できます。Service Worker と組み合わせることで、アプリをオフラインで動作させることができます。

project/ ディレクトリのルートに次のコードを含む sw.js ファイルを作成します。

importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.4.1/workbox-sw.js');

if (workbox) {

  // Pre-cache HTML, CSS, and image assets
  workbox.precaching.precacheAndRoute([
    {
      "url": "index.html",
      "revision": "a7a5b45e7a48ecf2cb10fd8bddf70342"
    },
    {
      "url": "style/main.css",
      "revision": "7ca18ea2f5608b3c3f67339a57a4fc8e"
    },
    {
      "url": "images/delete.svg",
      "revision": "840ae217e9fe8c73c6d76286aefef63f"
    },
    {
      "url": "images/rocket-form.svg",
      "revision": "6bcd12b01e14547c1f9e0069c3da5f0d"
    },
    {
      "url": "images/rocket-icon.png",
      "revision": "f61c19851368484e8cb7efebf4d26a77"
    },
    {
      "url": "images/rocket.svg",
      "revision": "19df337059a0d6420869bcd20bdc6fab"
    },
    {
      "url": "images/ship_0.jpg",
      "revision": "58bb2ed6c80b6ca362c18515f07f2aee"
    },
    {
      "url": "images/ship_1.jpg",
      "revision": "94895878d03c00fae4f19583efb53ad2"
    },
    {
      "url": "images/ship_2.jpg",
      "revision": "992f720b3d4d3d21c83a7e71057effc9"
    },
    {
      "url": "images/ship_3.jpg",
      "revision": "06c2a683898186f728c564c9e518d16c"
    },
    {
      "url": "images/ship_4.jpg",
      "revision": "04673dcead6d46a65fdfb7c78984afd8"
    },
    {
      "url": "images/ship_5.jpg",
      "revision": "d08b0352c8971af5f881dcde0542ed97"
    },
    {
      "url": "images/ship_6.jpg",
      "revision": "0ccd8c0c257264e0496eed72d4beb936"
    },
    {
      "url": "images/ship_7.jpg",
      "revision": "af52e423fd57b2205c95bd308d50663e"
    },
    {
      "url": "images/ship_8.jpg",
      "revision": "00a5102cdfac3dbc041fb5b286e6b0e7"
    },
    {
      "url": "images/ship_9.jpg",
      "revision": "4b57c477216cb8c106b0c09ee9376249"
    }
  ]);

  // Force update of newest service worker
  workbox.skipWaiting();
  workbox.clientsClaim();

  // Google Fonts
  workbox.routing.registerRoute(
    new RegExp('https://fonts.(?:googleapis|gstatic).com/(.*)'),
    workbox.strategies.staleWhileRevalidate()
  );

  // Material Design & navigation library
  workbox.routing.registerRoute(
    new RegExp('https://unpkg.com/(.*)'),
    workbox.strategies.staleWhileRevalidate()
  );

  // App scripts
  workbox.routing.registerRoute(
    new RegExp('/scripts/(.*)'),
    workbox.strategies.staleWhileRevalidate()
  );

  // Firebase libraries
  workbox.routing.registerRoute(
    new RegExp('http://localhost:5000/__/firebase'),
    workbox.strategies.staleWhileRevalidate()
  );

} else {
  console.log(`Workbox didn't load 😬`);
}

次に、index.html で終了タグ body の直前にスクリプトを追加して、作成した Service Worker を登録します。

index.html

  <script>
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
          .then(reg => {
            console.log('Service worker registered! 😎', reg);
          })
          .catch(err => {
            console.log('😥 Registration failed: ', err);
          });
      });
    }
  </script>

最後に、SpaceRace.jsSpaceRace 関数を次のコードに置き換えます。これは enablePersistence メソッドを呼び出します。

scripts/SpaceRace.js

function SpaceRace() {
  firebase.auth().signInAnonymously().then(() => {
    firebase.firestore().enablePersistence()
      .then(() => {
        this.initTemplates();
        this.initRouter();
      });
  }).catch(err => {
    console.log(err);
  });
}

ブラウザでアプリを 2 回更新します(Service Worker のインストールとサイト アセットのキャッシュに 1 回)。次に、Ctrl + c を使用して Firebase ローカル サーバーを無効にし、お使いのパソコンの Wi-Fi をオフにします。ウェブアプリを再読み込みし、オフラインで読み込まれることを確認します。新しい船を追加したり、既存の船を削除したりしてみてください。Wi-Fi 接続を復元し、firebase serve でサーバーを再起動します。ページを再読み込みして、オフラインの変更がすべて反映されていることを確認します。

注: オフライン対応の Firestore は、複数のタブが開いている場合に想定どおりに動作しない可能性があります。必ず 1 つのタブでアプリをテストします。詳しくは、こちらのドキュメントをご覧ください。

説明

提供されている Service Worker は、Workbox を使用してサイトのアセット(HTML、CSS、JavaScript など)をデバイスにローカルでキャッシュに保存します。これによって、ネットワークからリソースを取得できなくても、アプリをすぐに読み込むことができるようになります。

Service Worker API をオフライン リソースに使用するだけでなく、アプリを初期化する関数 SpaceRaceenablePersistence メソッドを追加しました。これで、Firestore のデータのコピーをローカルでキャッシュに保存するように、Cloud Firestore インスタンスを構成します。ローカルデータの使用には 2 つの大きなメリットがあります。

  1. ネットワークを待機しなくて済むので、アプリのデータは非常に高速に読み込まれます。
  2. このアプリは、オフラインのときでもデータにアクセスして変更し、接続が回復したら変更内容を同期できます。

セキュリティに関する重要な違いなど、Firestore でのオフラインの永続性については、こちらのドキュメントをご覧ください。

この Codelab では、Firestore で基本的な読み取りと書き込みを実行する方法と、セキュリティ ルールでデータアクセスを保護する方法を学びました。また、Cloud Firestore と Service Worker を使用してオフライン機能を有効にする方法も学習しました。

Firestore について詳しくは、以下のリソースをご覧ください。

PWA トレーニング コースのすべての Codelab については、コースのようこそ Codelab をご覧ください。