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: この Codelab で作成する開始用コードです。
依存関係のインストール
クローンを作成したら、ウェブアプリをビルドする前に、root フォルダと 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 が Firebase によって自動的に割り当てられます。この一意の識別子は、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 コンソールの左側のパネルで、[ビルド] セクションを見つけます。
- [認証] をクリックし、[ログイン方法] タブをクリックします(または、ここをクリックして [ログイン方法] タブに直接移動します)。
- [Google] ログイン プロバイダを有効にして、[保存] をクリックします。
- アプリの公開名を <your-project-name> に設定し、プルダウン メニューからプロジェクト サポートのメールアドレスを選択します。
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 認証の挿入
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}`)
}
これで [マイ トリップ] ページが機能するようになります。新しい旅行投稿を作成した場合の 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 からストレージを注入します。
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 認証ガード
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;
}
}
}