1. 作成する内容
この Codelab では、Angular ライブラリの最新である AngularFire を使用して、リアルタイムのコラボレーション マップを使用して旅行ブログを作成します。完成したウェブアプリは旅行ブログで、旅行した各場所に画像をアップロードできます。
ウェブアプリの構築には AngularFire、ローカルテストには Emulator Suite、ユーザーデータを追跡するには Authentication を使用します。さらに、Cloud Functions を利用してデータとメディアを保持する Firestore と Storage、最後に Firebase Hosting を使用してアプリをデプロイします。
学習内容
- Emulator Suite を使用して Firebase プロダクトをローカルで開発する方法
- AngularFire でウェブアプリを強化する方法
- Firestore でデータを保持する方法
- メディアをストレージに保存する方法
- アプリを 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 プロジェクトを作成する
- Firebase にログインします。
- Firebase コンソールで [プロジェクトを追加] をクリックし、Firebase プロジェクトに「<your-project>」という名前を付けます。Firebase プロジェクトのプロジェクト ID を覚えておいてください。
- [プロジェクトを作成] をクリックします。
重要: Firebase プロジェクトの名前は <your-project> ですが、<your-project>-1234 という形式の一意のプロジェクト ID が自動的に割り当てられます。この一意の識別子は(CLI を含む)プロジェクトが実際に識別される方法であり、<your-project> は単なる表示名です。
これから作成するアプリケーションは、ウェブアプリで利用可能な Firebase プロダクトを使用します。
- ユーザーがアプリに簡単にログインできるようにする Firebase Authentication。
- 構造化されたデータをクラウドに保存し、データが変更されたときに即座に通知を受け取る Cloud Firestore。
- ファイルをクラウドに保存する Cloud Storage for Firebase。
- アセットをホストして提供する Firebase Hosting。
- 内部 API と外部 API とやり取りするための関数。
これらのプロダクトの一部は、特別な構成が必要な場合や、Firebase コンソールを使用して有効にする必要があります。
プロジェクトに Firebase ウェブアプリを追加する
- ウェブアイコンをクリックして、新しい Firebase ウェブアプリを作成します。
- 次のステップでは、構成オブジェクトが表示されます。このオブジェクトの内容を
environments/environment.ts
ファイルにコピーします。
Firebase Authentication の Google ログインを有効にする
ユーザーが Google アカウントでウェブアプリにログインできるようにするには、Google のログイン方法を使用します。
Google ログインを有効にするには:
- Firebase コンソールで、左側のパネルにある [ビルド] セクションを見つけます。
- [Authentication] をクリックし、[Sign-in method] タブをクリックします(またはこちらをクリックして、直接移動します)。
- Google ログイン プロバイダを有効にして、[保存] をクリックします。
- アプリの公開名を <プロジェクト名> に設定し、プルダウン メニューから [プロジェクトのサポートメール] を選択します。
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. 認証の追加
アプリのエミュレータを設定できたので、メッセージを投稿する前に各ユーザーがログインしていることを確実にする認証機能を追加できます。
そのためには、AngularFire から signin
関数を直接インポートし、authState
関数でユーザーの認証状態を追跡します。読み込み時にユーザー認証状態を確認するように、ログインページの機能を変更します。
AngularFire 認証の挿入
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);
})
}
}
これで、ログインページが機能します。ログインして、Authentication Emulator で結果を確認します。
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}`)
}
これで、[旅行] ページが機能するようになりました。新しい旅行記事を作成する際の 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. アプリケーションのデプロイ
これで、アプリケーションをデプロイする準備が整いました。
firebase
構成を src/environments/environment.ts
から src/environments/environment.prod.ts
にコピーして、次のコマンドを実行します。
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 認証ガード
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.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;
}
}
}