尝试使用Angular操作Firebase实时数据库

首先

这次我想尝试一下Angular,上次用过React。

由于Firebase的设置与上次相同,因此本次我们将从创建项目开始。
(有关Firebase设置的信息,请参考上次的记录。)

太长不看。

我写的这段代码

逐步实施

创建项目

让我们使用Angular CLI开始吧。既然有机会,我们可以尝试一下从v10开始新增的严格模式。

$ npx @angular/cli new realtime-db-sample-with-angular --strict

安装库

由于提供了公式库[AngularFire](https://github.com/angular/angularfire),因此请安装该库。
在实现登录功能时,需要使用SDK,所以也要安装该SDK。

$ npm run ng -- add @angular/fire
$ npm install --save firebase

Firebase的相关部分

我认为与Firebase操作无关的部分是与显示相关的,并且可能是通用的,所以将其封装成库。

$ npm run ng -- g library firebase-library

初始化处理

参考Quick Start进行快速添加初始化处理。

将配置文件分离到各个独立的文件中。

import { FirebaseOptions } from '@angular/fire';

export const firebaseConfig: FirebaseOptions = {
  production: false,
  firebase: {
        // コピペ
  }
};

在firebase-library.module.ts中进行初始化时,加载它。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AngularFireModule } from '@angular/fire';
import { AngularFireDatabaseModule } from '@angular/fire/database';
import { FirebaseUsecaseService } from './service/firebase-usecase.service';
// ↑で作ったconfigファイル
import { firebaseConfig } from './config/config';
import { LoginComponent } from './components/login/login.component';
import { AngularFireAuthModule } from '@angular/fire/auth';
import { FirebaseFormatterService } from './service/firebase-formatter.service';

@NgModule({
  declarations: [],
  imports: [
        // 初期化のときにconfigを渡してあげる
    AngularFireModule.initializeApp(firebaseConfig.firebase),
        // RealtimeDatabaseを使うので必要なmoduleをimport
    AngularFireDatabaseModule,
        // ログイン周りに必要なmoduleをimport
    AngularFireAuthModule
  ],
  providers: [],
  exports: []
})
export class FirebaseLibraryModule { }

创建一个阅读写作服务

创建一个用于执行CRUD操作的服务。
在参考官方示例的基础上进行。
首先从CLI中生成。

$ npm run ng -- g service firebse-usecase --project=firebase

由于基本上与使用React时相同,我们将继续进行CRUD操作,因此这次决定只实现全部获取、注册和删除。

import { Injectable } from '@angular/core';
import { AngularFireDatabase, SnapshotAction } from '@angular/fire/database';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { FirebaseFormatterService } from './firebase-formatter.service';
import { FirebaseKeyValue } from '../types/firebase-types';

@Injectable({
  providedIn: 'root'
})
export class FirebaseUsecaseService {
  private items: Observable<SnapshotAction<string>[]>;
  constructor(private readonly db: AngularFireDatabase, private readonly formatter: FirebaseFormatterService) {
        // 指定したURL以下の値がリスト形式で取得される
        // 絞りたければ渡すパスを絞っていけば良い  
        this.items = this.db.list<string>('/sample').snapshotChanges();
  }

    /**
   * 全件取得する
   */
  fetchDocumentAll() {
        // 余分なパラメータが多いので、使いやすい形式に整形する
    return this.items.pipe(map(this.documentToResponse));
  }

    /**
   * 登録を行う
   */
  async setDocument(registerKeyValue: FirebaseKeyValue) {
        // setでの更新は上書きになってしまうので、登録済みのデータを取得しておく
        // 今回は値を取得してから後続処理に移りたいため、Promiseに変換して取得を待つ
    const item = await this.fetchDocumentAll().pipe(take(1)).toPromise();
        // 登録したいデータと登録されているデータをマージしつつ整形する
    const registerDocument = this.objectToDocument([...item, registerKeyValue]);
      // sampleのデータを上書き登録
    this.db.object('/sample').set(registerDocument);
  }

    /**
     * 指定したパス配下のデータを全て削除する
     */
  deleteAll() {
    this.db.object('/sample').remove();
  }

    /**
     * 取得したデータからkey,valueの値のみを取り出す
     */
    private documentToResponse(document: SnapshotAction<string>[]): FirebaseKeyValue[] {
    return document.map(item => ({ key: item.key, value: item.payload.val() }));
  }

    /**
     * 登録用のデータに整形する
   */
    private objectToDocument(keyValues: FirebaseKeyValue[]): FirebaseDocument {
        // 登録したいデータは{[key: string]: value}形式なので整形
    return keyValues.reduce((previous, current) => {
            // keyが無い場合は今回考慮しない
      const registereData = {[current.key!]: current.value};
      return {...previous, ...registereData};
    }, {});
  }
}

登录组件

由于仅在登录后才能进行注册和删除操作,因此需要进行登录处理。
我们将在已创建的库中提供用于执行登录的组件。

$ npm run ng -- g component components/login --project=firebase-library

首先要参考官方的示例,然后继续进行。首先是从ts端开始。

import { Component } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { Observable } from 'rxjs';
import { auth, User } from 'firebase/app';

@Component({
  selector: 'lib-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent {
  private _user: Observable<User | null>

  constructor(private auth: AngularFireAuth) {
    this._user = this.auth.user;
  }

  login() {
        // Googleログインを行う
    this.auth.signInWithPopup(new auth.GoogleAuthProvider());
  }
  logout() {
        // ログアウト
    this.auth.signOut();
  }

  get user() {
    return this._user;
  }
}

接下来开始编写HTML。
如果能够获取到用户信息,则显示登出按钮。
如果无法获取到用户信息,则显示登录按钮。

<ng-container *ngIf="user | async; else showLoginButton">
  <button (click)="logout()">ログアウト</button>
</ng-container>
<ng-template #showLoginButton>
  <button (click)="login()">ログイン</button>
</ng-template>

在 app.module 中添加 */
将文本追加到 app.module 中

将已创建的library添加到app.module中,以便可以使用。

import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ListComponent } from './components/list/list.component';
import { FirebaseLibraryModule } from 'firebase-library';
import { FormComponent } from './components/form/form.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
        // これを追加する
    FirebaseLibraryModule,
        // ルーティングを使いたいのでimportしておく
    AppRoutingModule,
        // 後でリアクティブフォームを使いたいので、importしておく
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

创建结果表示页面

我将从这里开始准备显示系统。
首先创建一个显示获取结果的页面。
从命令行界面(CLI)创建组件开始。

$ npm run ng -- g component components/list

以下是TS端的具体步骤:
使用创建的库进行全面获取→将结果显示在组件中。

import { Component } from '@angular/core';
import { FirebaseUsecaseService } from 'firebase-library';
import { BehaviorSubject } from 'rxjs';
@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.scss']
})
export class ListComponent {
  private items$ = new BehaviorSubject<any[]>([]);
  constructor(private readonly firebase: FirebaseUsecaseService) {
    firebase.fetchDocumentAll().subscribe(res => {
      this.items$.next(res);
    }, err => {
      console.log('error', err);
    });
  }

  get items() {
    return this.items$;
  }
}

以下是HTML的代码。
由于items是一个BehaviorSubject,我们可以使用async管道来显示它。

<dl>
  <ng-container *ngFor="let item of items | async">
    <dt>key: {{item.key}}</dt>
    <dt>value: {{item.value}}</dt>
  </ng-container>
</dl>

创建注册和删除页面

开始快速制作组件。

$ npm run ng -- g component components/form

我会使用响应式表单创建输入注册数据的部分。关于响应式表单,我之前有个笔记可以作为参考。

Angular的反应式表单综述-水无瀬的编程日记

import { FormGroup, FormControl, Validators } from '@angular/forms';
import { Component } from '@angular/core';
import { FirebaseUsecaseService } from 'firebase-library';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.scss']
})
export class FormComponent {
    // key、valueはどちらも必須とする
  readonly formGroup = new FormGroup({
    key: new FormControl('', [Validators.required]),
    value: new FormControl('', [Validators.required])
  });

  constructor(private readonly firebase: FirebaseUsecaseService) { }

  registerData() {
        // setDocumentはasync functionだけど、今回は待つ必要が無いので呼び出しっぱなし
    this.firebase.setDocument({key: this.key?.value, value: this.value?.value});
  }
  deleteAll() {
    this.firebase.deleteAll();
  }

  get key() {
    return this.formGroup.get('key');
  }
  get value() {
    return this.formGroup.get('value');

  }
}

在HTML页面上按如下方式配置。
在此处预先加载登录按钮。

<div>
    <!-- 作成したログイン/ログアウトボタンコンポーネント -->
  <lib-login></lib-login>
</div>
<form [formGroup]="formGroup">
  <label>key: <input type="text" formControlName="key"/></label>
    <!-- 必須項目未入力のときのエラーメッセージ -->
  <div *ngIf="key?.invalid && (key?.dirty || key?.touched)">
    <span *ngIf="key?.hasError('required')">必須です。</span>
  </div>
  <label>value: <input type="text" formControlName="value"/></label>
    <!-- 必須項目未入力のときのエラーメッセージ -->
  <div *ngIf="value?.invalid && (value?.dirty || value?.touched)">
    <span *ngIf="value?.hasError('required')">必須です。</span>
  </div>
  <button (click)="registerData()">登録</button>
</form>
<button (click)="deleteAll()">全消し</button>

设置路由

为了完成列表显示、注册和删除,暂时将它们分成不同的页面。
需要设置路由,所以先快速实现一下。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ListComponent } from './components/list/list.component';
import { FormComponent } from './components/form/form.component';

const routes: Routes = [
  {path: 'list', component: ListComponent},
  {path: 'form', component: FormComponent}
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

确认动作

由于实施已经完成,接下来要进行验证操作。
由于需要先构建库文件,所以首先从这一步开始。

$ npm run build -- firebase-library

如果构建成功,就启动应用程序。

$ npm start

如果能启动的话,尝试访问http://localhost:4200/list。
如果能够展示图像获取的结果,那就OK。

image.png

接下来尝试访问http://localhost:4200/form。
如果显示出像图片一样的登录按钮、注册表单和删除按钮,则可以。

image.png

总结

这次我尝试使用Angular来使用RealtimeDatabase。
由于官方提供了库,我觉得它相当容易实现。
由于我匆忙制作,对页面的分割等处理不太好,所以觉得再好好制作一下可能会更好。

因为公式库的帮助,使得很容易上手,所以我希望将来能够创造出一些东西。

参考链接

    • angular/angularfire: The official Angular library for Firebase.

 

    • angularfire/install-and-setup.md at master · angular/angularfire

 

    • angularfire/objects.md at master · angular/angularfire

 

    angularfire/getting-started.md at master · angular/angularfire
bannerAds