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 を操作する関数。
この中には、特別な設定が必要になるプロダクトや、Firebase コンソールを使用して有効化する必要があるプロダクトがあります。
Google ログインを Firebase Authentication 用に有効にする
ユーザーが Google アカウントでウェブアプリにログインできるように、Google ログインを使用します。
Google ログインを有効にするには:
- Firebase コンソールの左側のパネルで、[構築] セクションを見つけます。
- [Authentication] をクリックし、[Sign-in method] タブをクリックします(または、ここをクリックして直接移動します)。
- [Google] ログイン プロバイダを有効にして、[保存] をクリックします。
- アプリの一般公開名に「<プロジェクト名>」を設定し、プルダウン メニューから [プロジェクトのサポートメール] を選択します。
Cloud Firestore を有効にする
- Firebase コンソールの [構築] セクションで、[Firestore データベース] をクリックします
- [Cloud Firestore] ペインで [データベースを作成] をクリックします。
- Cloud Firestore データを保存するロケーションを設定します。デフォルトのままにするか、近くのリージョンを選択できます。
Cloud Storage を有効にする
このウェブアプリは Cloud Storage for Firebase を使用して画像ファイルを保存、アップロード、共有します。
- Firebase コンソールの [構築] セクションで、[ストレージ] をクリックします。
- [開始] ボタンが表示されない場合は、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. 認証の追加
アプリのエミュレータが設定されたので、各ユーザーがメッセージを投稿する前にログインしていることを確認する認証機能を追加できます。
そのためには、AngularFire から signin
関数を直接インポートし、authState
関数を使用してユーザーの認証状態を追跡します。ログインページの関数を変更して、ページが読み込み時にユーザー認証状態を確認するようにします。
AngularFire Auth の挿入
src/app/pages/login-page/login-page.component.ts
で、@angular/fire/auth
から 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);
})
}
}
これでログインページが機能するようになりました。ログインを試して、認証エミュレータで結果を確認します。
7. Firestore の構成
このステップでは、Firestore に保存されている旅行ブログ投稿を投稿および更新する機能を追加します。
Authentication と同様に、Firestore 関数は AngularFire から事前にパッケージ化されています。各ドキュメントはコレクションに属し、各ドキュメントにはネストされたコレクションを含めることもできます。旅行ブログ投稿を作成して更新するには、Firestore のドキュメントの path
を知る必要があります。
TravelService の実装
ウェブアプリでは、さまざまなページで Firestore ドキュメントの読み取りと更新が必要になるため、src/app/services/travel.service.ts
で関数を実装して、各ページで同じ AngularFire 関数を繰り返し挿入しないようにします。
前の手順と同様に、Auth
と Firestore
をサービスに挿入することから始めます。現在の認証ステータスをリッスンするオブザーバブル 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)
}
オブザーバブルとしてデータを読み取る
旅行の投稿と途中の立ち寄り場所は作成後に変更できるため、ドキュメント オブジェクトをオブザーバブルとして取得し、行われた変更をサブスクライブする方が便利です。この機能は、@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 を実装して、画像やその他の種類のメディアを保存します。
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);
}
画像をアップロードすると、メディアファイル自体がストレージにアップロードされ、それに応じて 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 Codelab もご確認ください。
11. 省略可: AngularFire 認証ガード
AngularFire は Firebase Authentication とともに、ルートベースの認証ガードも提供します。これにより、アクセス権が不十分なユーザーをリダイレクトできます。これにより、保護されたデータにユーザーがアクセスするのを防ぐことができます。
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.rules
のストレージ データベースへのアクセスを強制できます。より複雑なチェックには関数を使用することもできます。
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;
}
}
}