1. 作成するもの
この Codelab では、Angular ライブラリの最新版である AngularFire を使用して、リアルタイムの共同編集マップを備えた旅行ブログを作成します。完成したウェブアプリは、旅行先の各ロケーションに画像をアップロードできる旅行ブログになります。
AngularFire を使用してウェブアプリを構築し、Emulator Suite を使用してローカルでテストします。Authentication を使用してユーザーデータを追跡し、Firestore と Storage を使用してデータとメディアを永続化します。これらは Cloud Functions によって強化され、最後に Firebase Hosting を使用してアプリをデプロイします。
学習内容
- Emulator Suite を使用して Firebase プロダクトをローカルで開発する方法
- AngularFire を使用してウェブアプリを強化する方法
- Firestore でデータを永続化する方法
- Storage でメディアを永続化する方法
- アプリを Firebase Hosting にデプロイする方法
- Cloud Functions を使用してデータベースと API を操作する方法
必要なもの
- Node.js バージョン 10 以降
- Firebase プロジェクトの作成と管理に使用する Google アカウント
- Firebase CLI バージョン 11.14.2 以降
- 任意のブラウザ(Chrome など)
- Angular と JavaScript の基本的な知識
2. サンプルコードを取得する
コマンドラインから Codelab の GitHub リポジトリ のクローンを作成します。
git clone https://github.com/firebase/codelab-friendlychat-web
または、git がインストールされていない場合は、リポジトリを ZIP ファイルとしてダウンロードすることもできます。
GitHub リポジトリには、複数のプラットフォーム用のサンプル プロジェクトが含まれています。
この Codelab では、webframework リポジトリのみを使用します。
- 📁 webframework: この Codelab で作成する開始コード。
依存関係のインストール
クローンを作成したら、ウェブアプリをビルドする前に、ルートフォルダと functions フォルダに依存関係をインストールします。
cd webframework && npm install
cd functions && npm install
Firebase CLI のインストール
ターミナルで次のコマンドを使用して Firebase CLI をインストールします。
npm install -g firebase-tools
次のコマンドを使用して、Firebase CLI のバージョンが 11.14.2 より大きいことを確認します。
firebase --version
バージョンが 11.14.2 より前の場合は、次のコマンドを使用して更新してください。
npm update firebase-tools
3. Firebase プロジェクトを作成して設定する
Firebase プロジェクトを作成する
- Google アカウントを使用して Firebase コンソール にログインします。
- ボタンをクリックして新しいプロジェクトを作成し、プロジェクト名(
FriendlyChatなど)を入力します。
- [続行] をクリックします。
- Firebase の利用規約が表示されたら、内容を読み、同意して [続行] をクリックします。
- (省略可)Firebase コンソールで AI アシスタンス(「Gemini in Firebase」)を有効にします。
- この Codelab では Google アナリティクスは必要ないため、Google アナリティクスのオプションをオフ にします。
- [プロジェクトを作成] をクリックして、プロジェクトのプロビジョニングが完了するまで待ってから、[続行] をクリックします。
Firebase ウェブアプリをプロジェクトに追加する
- ウェブアイコンをクリックして、新しい Firebase ウェブアプリを作成します。
- 次のステップで、構成オブジェクトが表示されます。このオブジェクトの内容を
environments/environment.tsファイルにコピーします。
Firebase プロダクトを設定する
これから構築するアプリでは、ウェブアプリに使用できる Firebase プロダクトを使用します。
- ユーザーがアプリに簡単にログインできるようにする Firebase Authentication。
- 構造化データをクラウドに保存し、データが変更されたときに即座に通知を受け取る Cloud Firestore。
- ファイルをクラウドに保存する Cloud Storage for Firebase。
- アセットをホストして提供する Firebase Hosting。
- 内部 API と外部 API を操作する Functions。
この中には、特別な設定が必要になるプロダクトや、Firebase コンソールを使用して有効化する必要があるプロダクトがあります。
Firebase Authentication 用に Google ログインを有効にする
ユーザーが Google アカウントでウェブアプリにログインできるように、Google ログイン方法を使用します。
Google ログインを有効にするには:
- Firebase コンソールの左側のパネルで、[構築] セクションを見つけます。
- [Authentication] をクリックし、[ログイン方法] タブをクリックします(または、ここをクリックして直接移動します)。
- [Google] ログイン プロバイダを有効にし、[保存] をクリックします。
- アプリの一般公開名に <your-project-name> を設定し、プルダウン メニューから [プロジェクトのサポートメール] を選択します。
Cloud Firestore を有効にする
- Firebase コンソールの [構築] セクションで、[Firestore データベース] をクリックします
- [Cloud Firestore] ペインで [データベースを作成] をクリックします。
- Cloud Firestore データを保存するロケーションを設定します。デフォルトのままにするか、近くのリージョンを選択できます。
Cloud Storage を有効にする
このウェブアプリは Cloud Storage for Firebase を使用して画像ファイルを保存、アップロード、共有します。
- Firebase コンソールの [構築] セクションで [Storage] をクリックします。
- [利用開始] ボタンがない場合は、Cloud Storage はすでに
有効になっているため、以下の手順を行う必要はありません。
- [利用開始] をクリックします。
- Firebase プロジェクトのセキュリティ ルールに関する免責条項を確認し、[次へ] をクリックします。
- Cloud Storage のロケーションは、Cloud Firestore データベース用に選択したリージョンと同じリージョンで事前選択されています。[完了] をクリックしてセットアップを完了します。
デフォルトのセキュリティ ルールでは、すべての認証済みユーザーは Cloud Storage に自由に書き込むことができます。この Codelab の後半で、ストレージのセキュリティを強化します。
4. Firebase プロジェクトに接続する
Firebase コマンドライン インターフェース(CLI)を使用すると、Firebase Hosting を使用してウェブアプリをローカルで提供したり、ウェブアプリを Firebase プロジェクトにデプロイしたりできます。
コマンドラインがアプリのローカル webframework ディレクトリにアクセスしていることを確認します。
ウェブアプリのコードを Firebase プロジェクトに接続します。まず、コマンドラインで Firebase CLI にログインします。
firebase login
次に、次のコマンドを実行してプロジェクト エイリアスを作成します。$YOUR_PROJECT_ID は、Firebase プロジェクトの ID に置き換えます。
firebase use $YOUR_PROJECT_ID
AngularFire を追加する
AngularFire をアプリに追加するには、次のコマンドを実行します。
ng add @angular/fire
次に、コマンドラインの手順に沿って、Firebase プロジェクトに存在する機能を選択します。
Firebase を初期化する
Firebase プロジェクトを初期化するには、次のコマンドを実行します。
firebase init
次に、コマンドラインのプロンプトに沿って、Firebase プロジェクトで使用した機能とエミュレータを選択します。
エミュレータを起動する
webframework ディレクトリから次のコマンドを実行して、エミュレータを起動します。
firebase emulators:start
最終的には、次のように表示されます。
$ firebase emulators:start
i emulators: Starting emulators: auth, functions, firestore, hosting, functions
i firestore: Firestore Emulator logging to firestore-debug.log
i hosting: Serving hosting files from: public
✔ hosting: Local server: http://localhost:5000
i ui: Emulator UI logging to ui-debug.log
i functions: Watching "/functions" for Cloud Functions...
✔ functions[updateMap]: firestore function initialized.
┌─────────────────────────────────────────────────────────────┐
│ ✔ All emulators ready! It is now safe to connect your app. │
│ i View Emulator UI at http://localhost:4000 │
└─────────────────────────────────────────────────────────────┘
┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator │ Host:Port │ View in Emulator UI │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting │ localhost:5000 │ n/a │
└────────────────┴────────────────┴─────────────────────────────────┘
Emulator Hub running at localhost:4400
Other reserved ports: 4500
Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
✔All emulators ready! メッセージが表示されたら、エミュレータを使用する準備が完了しています。
旅行アプリの UI が(まだ)機能していない状態で表示されます。
それでは、さっそく構築を始めましょう。
5. ウェブアプリをエミュレータに接続する
エミュレータ ログの表に基づいて、Cloud Firestore エミュレータはポート 8080 でリッスンし、Authentication エミュレータはポート 9099 でリッスンします。
EmulatorUI を開く
ウェブブラウザで http://127.0.0.1:4000/ に移動します。Emulator Suite UI が表示されます。
エミュレータを使用するようにアプリをルーティングする
src/app/app.module.ts で、次のコードを AppModule のインポート リストに追加します。
@NgModule({
declarations: [...],
imports: [
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => {
const auth = getAuth();
if (location.hostname === 'localhost') {
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
}
return auth;
}),
provideFirestore(() => {
const firestore = getFirestore();
if (location.hostname === 'localhost') {
connectFirestoreEmulator(firestore, '127.0.0.1', 8080);
}
return firestore;
}),
provideFunctions(() => {
const functions = getFunctions();
if (location.hostname === 'localhost') {
connectFunctionsEmulator(functions, '127.0.0.1', 5001);
}
return functions;
}),
provideStorage(() => {
const storage = getStorage();
if (location.hostname === 'localhost') {
connectStorageEmulator(storage, '127.0.0.1', 5001);
}
return storage;
}),
...
]
これで、ローカル エミュレータを使用するようにアプリが構成され、ローカルでテストと開発を行うことができます。
6. Authentication を追加する
アプリのエミュレータが設定されたので、各ユーザーがメッセージを投稿する前にログインしていることを確認する Authentication 機能を追加できます。
これを行うには、signin 関数を AngularFire から直接インポートし、authState 関数を使用してユーザーの認証状態を追跡します。ログイン ページの関数を変更して、ページが読み込まれたときにユーザーの認証状態を確認するようにします。
AngularFire Auth を挿入する
src/app/pages/login-page/login-page.component.ts で、Auth から @angular/fire/auth をインポートし、LoginPageComponent に挿入します。Google などの認証プロバイダや、signin、signout などの関数も、同じパッケージから直接インポートしてアプリで使用できます。
import { Auth, GoogleAuthProvider, signInWithPopup, signOut, user } from '@angular/fire/auth';
export class LoginPageComponent implements OnInit {
private auth: Auth = inject(Auth);
private provider = new GoogleAuthProvider();
user$ = user(this.auth);
constructor() {}
ngOnInit(): void {}
login() {
signInWithPopup(this.auth, this.provider).then((result) => {
const credential = GoogleAuthProvider.credentialFromResult(result);
return credential;
})
}
logout() {
signOut(this.auth).then(() => {
console.log('signed out');}).catch((error) => {
console.log('sign out error: ' + error);
})
}
}
これでログイン ページが機能するようになりました。ログインを試して、Authentication エミュレータで結果を確認してください。
7. Firestore を構成する
このステップでは、Firestore に保存されている旅行ブログの投稿を投稿して更新する機能を追加します。
Authentication と同様に、Firestore 関数は AngularFire から事前パッケージ化されています。各ドキュメントはコレクションに属し、各ドキュメントにはネストされたコレクションを含めることもできます。旅行ブログの投稿を作成して更新するには、Firestore 内のドキュメントの path を知る必要があります。
TravelService を実装する
多くの異なるページでウェブアプリの Firestore ドキュメントを読み取って更新する必要があるため、src/app/services/travel.service.ts に関数を実装して、ページごとに同じ AngularFire 関数を繰り返し挿入しないようにします。
前のステップと同様に、Auth と Firestore をサービスに挿入します。現在の認証ステータスをリッスンする Observable user$ オブジェクトを定義することもできます。
import { doc, docData, DocumentReference, Firestore, getDoc, setDoc, updateDoc, collection, addDoc, deleteDoc, collectionData, Timestamp } from "@angular/fire/firestore";
export class TravelService {
firestore: Firestore = inject(Firestore);
auth: Auth = inject(Auth);
user$ = authState(this.auth).pipe(filter(user => user !== null), map(user => user!));
router: Router = inject(Router);
旅行の投稿を追加する
旅行の投稿は Firestore に保存されるドキュメントとして存在します。ドキュメントはコレクション内に存在する必要があるため、すべての旅行の投稿を含むコレクションの名前は travels になります。したがって、旅行の投稿のパスは travels/ になります。
AngularFire の addDoc 関数を使用すると、オブジェクトをコレクションに挿入できます。
async addEmptyTravel(userId: String) {
...
addDoc(collection(this.firestore, 'travels'), travelData).then((travelRef) => {
collection(this.firestore, `travels/${travelRef.id}/stops`);
setDoc(travelRef, {... travelData, id: travelRef.id})
this.router.navigate(['edit', `${travelRef.id}`]);
return travelRef;
})
}
データの更新と削除
旅行の投稿の uid が指定されている場合、Firestore に保存されているドキュメントのパスを推測できます。このパスは、AngularFire の updateFoc 関数と deleteDoc 関数を使用して読み取り、更新、削除できます。
async updateData(path: string, data: Partial<Travel | Stop>) {
await updateDoc(doc(this.firestore, path), data)
}
async deleteData(path: string) {
const ref = doc(this.firestore, path);
await deleteDoc(ref)
}
データを Observable として読み取る
旅行の投稿と途中の経由地は作成後に変更できるため、ドキュメント オブジェクトを Observable として取得して、行われた変更をサブスクライブすると便利です。この機能は、@angular/fire/firestore の docData 関数と collectionData 関数によって提供されます。
getDocData(path: string) {
return docData(doc(this.firestore, path), {idField: 'id'}) as Observable<Travel | Stop>
}
getCollectionData(path: string) {
return collectionData(collection(this.firestore, path), {idField: 'id'}) as Observable<Travel[] | Stop[]>
}
旅行の投稿に経由地を追加する
旅行の投稿の操作が設定されたので、経由地について考えてみましょう。経由地は、旅行の投稿のサブコレクションの下に存在します。travels/
これは旅行の投稿の作成とほぼ同じです。自分で実装するか、以下の実装を確認してください。
async addStop(travelId: string) {
...
const ref = await addDoc(collection(this.firestore, `travels/${travelId}/stops`), stopData)
setDoc(ref, {...stopData, id: ref.id})
}
いいですね。Firestore 関数が Travel サービスに実装されたので、実際に動作していることを確認できます。
アプリで Firestore 関数を使用する
src/app/pages/my-travels/my-travels.component.ts に移動し、TravelService を挿入してその関数を使用します。
travelService = inject(TravelService);
travelsData$: Observable<Travel[]>;
stopsList$!: Observable<Stop[]>;
constructor() {
this.travelsData$ = this.travelService.getCollectionData(`travels`) as Observable<Travel[]>
}
コンストラクタで TravelService が呼び出され、すべての旅行の Observable 配列を取得します。
現在のユーザーの旅行のみが必要な場合は、query 関数を使用します。
セキュリティを確保する他の方法としては、セキュリティ ルールを実装する、または以下の省略可能なステップで説明するように Firestore で Cloud Functions を使用する方法があります。
次に、TravelService に実装されている関数を呼び出します。
async createTravel(userId: String) {
this.travelService.addEmptyTravel(userId);
}
deleteTravel(travelId: String) {
this.travelService.deleteData(`travels/${travelId}`)
}
これで [My Travels] ページが機能するようになりました。新しい旅行の投稿を作成すると、Firestore エミュレータで何が起こるかを確認してください。
次に、/src/app/pages/edit-travels/edit-travels.component.ts の更新関数に対して繰り返します。
travelService: TravelService = inject(TravelService)
travelId = this.activatedRoute.snapshot.paramMap.get('travelId');
travelData$: Observable<Travel>;
stopsData$: Observable<Stop[]>;
constructor() {
this.travelData$ = this.travelService.getDocData(`travels/${this.travelId}`) as Observable<Travel>
this.stopsData$ = this.travelService.getCollectionData(`travels/${this.travelId}/stops`) as Observable<Stop[]>
}
updateCurrentTravel(travel: Partial<Travel>) {
this.travelService.updateData(`travels${this.travelId}`, travel)
}
updateCurrentStop(stop: Partial<Stop>) {
stop.type = stop.type?.toString();
this.travelService.updateData(`travels${this.travelId}/stops/${stop.id}`, stop)
}
addStop() {
if (!this.travelId) return;
this.travelService.addStop(this.travelId);
}
deleteStop(stopId: string) {
if (!this.travelId || !stopId) {
return;
}
this.travelService.deleteData(`travels${this.travelId}/stops/${stopId}`)
this.stopsData$ = this.travelService.getCollectionData(`travels${this.travelId}/stops`) as Observable<Stop[]>
}
8. Storage を構成する
次に、画像やその他の種類のメディアを保存する Storage を実装します。
Cloud Firestore は、JSON オブジェクトなどの構造化データを保存するのに最適です。Cloud Storage は、ファイルまたは BLOB を保存するように設計されています。このアプリでは、ユーザーが旅行の写真を共有できるようにするために使用します。
Firestore と同様に、Storage でファイルを保存して更新するには、ファイルごとに一意の識別子が必要です。
TraveService に関数を実装しましょう。
ファイルのアップロード
src/app/services/travel.service.ts に移動し、AngularFire から Storage を挿入します。
export class TravelService {
firestore: Firestore = inject(Firestore);
auth: Auth = inject(Auth);
storage: Storage = inject(Storage);
アップロード関数を実装します。
async uploadToStorage(path: string, input: HTMLInputElement, contentType: any) {
if (!input.files) return null
const files: FileList = input.files;
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (file) {
const imagePath = `${path}/${file.name}`
const storageRef = ref(this.storage, imagePath);
await uploadBytesResumable(storageRef, file, contentType);
return await getDownloadURL(storageRef);
}
}
return null;
}
Firestore からドキュメントにアクセスする場合と Cloud Storage からファイルにアクセスする場合の主な違いは、どちらもフォルダ構造のパスに従いますが、ベース URL とパスの組み合わせは getDownloadURL を介して取得され、保存して ファイルで使用できることです。
アプリで 関数を使用する
src/app/components/edit-stop/edit-stop.component.ts に移動し、次のコマンドを使用してアップロード関数を呼び出します。
async uploadFile(file: HTMLInputElement, stop: Partial<Stop>) {
const path = `/travels/${this.travelId}/stops/${stop.id}`
const url = await this.travelService.uploadToStorage(path, file, {contentType: 'image/png'});
stop.image = url ? url : '';
this.travelService.updateData(path, stop);
}
画像がアップロードされると、メディア ファイル自体が Storage にアップロードされ、URL が Firestore のドキュメントに保存されます。
9. アプリケーションのデプロイ
これで、アプリケーションをデプロイする準備が整いました。
src/environments/environment.ts から src/environments/environment.prod.ts に firebase 構成をコピーして、次のコマンドを実行します。
firebase deploy
次のように表示されます。
✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.
=== Deploying to 'friendly-travels-b6a4b'...
i deploying storage, firestore, hosting
i firebase.storage: checking storage.rules for compilation errors...
✔ firebase.storage: rules file storage.rules compiled successfully
i firestore: reading indexes from firestore.indexes.json...
i cloud.firestore: checking firestore.rules for compilation errors...
✔ cloud.firestore: rules file firestore.rules compiled successfully
i storage: latest version of storage.rules already up to date, skipping upload...
i firestore: deploying indexes...
i firestore: latest version of firestore.rules already up to date, skipping upload...
✔ firestore: deployed indexes in firestore.indexes.json successfully for (default) database
i hosting[friendly-travels-b6a4b]: beginning deploy...
i hosting[friendly-travels-b6a4b]: found 6 files in .firebase/friendly-travels-b6a4b/hosting
✔ hosting[friendly-travels-b6a4b]: file upload complete
✔ storage: released rules storage.rules to firebase.storage
✔ firestore: released rules firestore.rules to cloud.firestore
i hosting[friendly-travels-b6a4b]: finalizing version...
✔ hosting[friendly-travels-b6a4b]: version finalized
i hosting[friendly-travels-b6a4b]: releasing new version...
✔ hosting[friendly-travels-b6a4b]: release complete
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/friendly-travels-b6a4b/overview
Hosting URL: https://friendly-travels-b6a4b.web.app
10. 完了
これで、アプリケーションが完成し、Firebase Hosting にデプロイされました。すべてのデータと分析情報は Firebase コンソールで確認できます。
AngularFire、Functions、セキュリティ ルールに関するその他の機能については、以下の省略可能なステップと他の Firebase Codelabs をご覧ください。
11. 省略可: AngularFire 認証ガード
Firebase Authentication とともに、AngularFire はルートに基づく認証ガードも提供しているため、アクセス権が不十分なユーザーをリダイレクトできます。これにより、保護されたデータにアクセスするユーザーからアプリを保護できます。
src/app/app-routing.module.ts で、
import {AuthGuard, redirectLoggedInTo, redirectUnauthorizedTo} from '@angular/fire/auth-guard'
特定のページでユーザーをリダイレクトするタイミングと場所を関数として定義できます。
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['signin']);
const redirectLoggedInToTravels = () => redirectLoggedInTo(['my-travels']);
次に、ルートに追加します。
const routes: Routes = [
{path: '', component: LoginPageComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectLoggedInToTravels}},
{path: 'signin', component: LoginPageComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectLoggedInToTravels}},
{path: 'my-travels', component: MyTravelsComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectUnauthorizedToLogin}},
{path: 'edit/:travelId', component: EditTravelsComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectUnauthorizedToLogin}},
];
12. 省略可: セキュリティ ルール
Firestore と Cloud Storage はどちらもセキュリティ ルール(それぞれ firestore.rules と security.rules)を使用して、セキュリティを適用し、データを検証します。
現時点では、Firestore と Storage のデータは読み取りと書き込みにオープン アクセスできますが、他のユーザーの投稿を変更することはできません。セキュリティ ルールを使用して、コレクションとドキュメントへのアクセスを制限できます。
Firestore ルール
認証済みユーザーのみが旅行の投稿を表示できるようにするには、firestore.rules ファイルに移動して、次のコマンドを追加します。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/travels {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId;
}
}
セキュリティ ルールを使用してデータを検証することもできます。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/posts {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId;
&& "author" in request.resource.data
&& "text" in request.resource.data
&& "timestamp" in request.resource.data;
}
}
Storage ルール
同様に、セキュリティ ルールを使用して、storage.rules の Storage データベースへのアクセスを適用できます。より複雑なチェックに関数を使用することもできます。
rules_version = '2';
function isImageBelowMaxSize(maxSizeMB) {
return request.resource.size < maxSizeMB * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
service firebase.storage {
match /b/{bucket}/o {
match /{userId}/{postId}/{filename} {
allow write: if request.auth != null
&& request.auth.uid == userId && isImageBelowMaxSize(5);
allow read;
}
}
}