1. 作成する内容
この Codelab では、Angular ライブラリの最新版である AngularFire を使用して、リアルタイムの共同マップを含む旅行ブログを作成します。完成したウェブアプリは旅行ブログで構成されます。ブログでは、旅行したそれぞれの場所に画像をアップロードできます。
AngularFire はウェブアプリの構築に使用します。Emulator Suite はローカルテストに使用します。Authentication はユーザーデータの追跡に使用します。Firestore と Storage は、Cloud Functions を活用してデータとメディアを保持するために使用します。最後に、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 プロジェクトに <実際のプロジェクト> という名前を付けます。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 コンソールの左側のパネルで、[Build] セクションを見つけます。
- [認証] をクリックし、[ログイン方法] タブをクリックします(または、ここをクリックして [ログイン方法] タブに直接移動します)。
- [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. 認証を追加する
アプリ用にエミュレータを設定したので、次は認証機能を追加して、各ユーザーがメッセージを投稿する前にログインしていることを確認します。
そのためには、signin
関数を AngularFire から直接インポートし、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[]>
}
すべての移動の Observable 配列を取得するために、コンストラクタで TravelService
が呼び出されます。
現在のユーザーの移動のみが必要な場合は、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;
}
}
}