# はじめに
諸事情により、次のようにテーマが劣化しています。
「Firebase + Angular(AngularFire)でインスタっぽいサービスを作ってみる」
↓
「Firebase Cloud Firestoreでフォロー/フォロワー機能の構築をしてみた」
↓
「Firebase Realtime Databaseでフォロー/フォロワー機能の構築をしてみた」 ←現タイトル
気を取り直して進めていきます。
今回は、Firebase + Angular(AngularFire)でインスタっぽいサービスを作る前段として、TwitterやInstagramのようなユーザー同士のフォロー機能の部分の実装を行った手順を紹介します。
以前、Qiitaに投稿したAngular&Firebaseでユーザー認証してみるという記事の中で作ったAngular + AngularFireのアプリをベースに機能を追加していきたいと思います。
今回追加する機能
- 全ユーザー一覧からユーザーのフォロー/アンフォローが出来るページ
RealtimeDatabase
RealtimeDatabaseで保持するフォロー/フォロワーの情報は次のようになります。
画面上から、Follow
をクリックすると、followers
ノードにフォローしたユーザーのUIDのノードが作成され、
その子にフォロー操作を行ったユーザーのUIDとbooleanのステータスを含むオブジェクトを格納します。
同時に、following
ノードにはフォロー操作を行ったユーザーのUIDのドードが作成され、
その子にフォローしたユーザーのUIDとbooleanのステータスを含むオブジェクトを格納します。
本当は...
Cloud Firestoreでやりたかった。
Cloud FirestoreのSubCollectionとQueryっていつ使うの問題という記事で紹介されているQuery型を使って、新たなフォローが行われるたびに、followers
コレクションに次のようなオブジェクトが追加されていくという方法を想定していましたが、WHEREによる検索を行うと、ほんの数件の取得でも画面で見て取れるほどのもたつきが発生したのと、ドキュメント削除の方法がわからない問題があり、
RealtimeDatabaseかつ冗長な方法で実装することにしました。
{
"followee": <UID>,
"follower": <UID>,
}
手順
Angular&Firebaseでユーザー認証してみるの手順が全て済んでおり、アプリのベースができているのを前提に進めていきます。
Database(RealtimeDatabase)
今回はテスト的に構築するため、Databaseの「ルール」タブから書き込み・読み込み権限を次のように変更しておきます。
FollowService
AngularFireのAPI(RealtimeDatabase)をハンドリングするためのサービスを作成します。
$ ng g service services/follow
import { Injectable } from '@angular/core';
import { AngularFireDatabase } from 'angularfire2/database';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
@Injectable()
export class FollowService {
constructor(
private db: AngularFireDatabase
) { }
getFollowers(userId: string) {
return this.db.object(`followers/${userId}`)
.valueChanges();
}
getFollowing(followerId: string, followedId: string) {
return this.db.object(`following/${followerId}/${followedId}`)
.valueChanges()
.pipe(
map(value => {
return value !== null ? true : false;
})
);
}
follow(followerId: string, followedId: string) {
this.db.object(`followers/${followedId}`).update({ [followerId]: true } );
this.db.object(`following/${followerId}`).update({ [followedId]: true } );
}
unfollow(followerId: string, followedId: string) {
this.db.object(`followers/${followedId}/${followerId}`).remove();
this.db.object(`following/${followerId}/${followedId}`).remove();
}
}
- getFollowers()
- 対象のUIDを受け取ってdbから全フォローワーを返す。
- getFollowing()
- 対象のUIDとログイン中のユーザーのUIDを受け取って対象のユーザーをフォロー中かどうかを返す
- follow()、unfollow()
- その名の通り。
UsersService
AngularFireのAPI(CloudFirestore)をハンドリングするためのサービスを作成します。
ユーザー一覧ページでユーザー一覧の取得を行うために使います。
$ ng g service services/users
import { Injectable } from '@angular/core';
import { AngularFirestore } from 'angularfire2/firestore';
@Injectable()
export class UsersService {
constructor(
private afStore: AngularFirestore
) { }
getAllUsers() {
const usersRef = this.afStore.collection('users');
return usersRef.valueChanges();
}
}
- getAllUsers()
- CloudFirestoreの
users
コレクションを取得しています。
- CloudFirestoreの
UserDetailComponent
1ユーザーごとの詳細情報とFollow
/Unfollow
ボタンを配置するコンポーネントを作成します。
次のスクリーンショットの青枠の部分です。
$ ng g component components/user-detail
テンプレート側
<div style="display: flex; justify-content: space-around; margin: 10px;">
<img [src]="user.photoURL" style="width: 100px; height: 100px">
<p>{{user.displayName}}</p>
<p>Follower: {{followerCount}}</p>
<p>
<button *ngIf="!isFollowing" (click)="toggleFollow()">Follow</button>
<button *ngIf="isFollowing" (click)="toggleFollow()">Unfollow</button>
</p>
</div>
TypeScript側
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { FollowService } from './../../services/follow.service';
@Component({
selector: 'app-user-detail',
templateUrl: './user-detail.component.html',
styleUrls: ['./user-detail.component.css']
})
export class UserDetailComponent implements OnInit, OnDestroy {
@Input() user;
@Input() currentUser;
public followerCount: number;
public isFollowing: boolean;
followers: Subscription;
following: Subscription;
constructor(
private followService: FollowService
) { }
ngOnInit() {
const userId = this.user.uid;
const currentUserId = this.currentUser.uid;
this.following = this.followService.getFollowing(currentUserId, userId)
.subscribe(following => {
this.isFollowing = following;
});
this.followers = this.followService.getFollowers(userId)
.subscribe(followers => {
if (followers === null) {
this.followerCount = 0;
} else {
this.followerCount = Object.keys(followers).length;
}
});
}
ngOnDestroy() {
this.followers.unsubscribe();
this.following.unsubscribe();
}
toggleFollow() {
const userId = this.user.uid;
const currentUserId = this.currentUser.uid;
if (this.isFollowing) {
this.followService.unfollow(currentUserId, userId);
} else {
this.followService.follow(currentUserId, userId);
}
}
}
UserListComponent
ユーザーリストを表示するページコンポーネントをを作成します。
UserDetailComponentの項目にあるスクリーンショットの赤枠の部分です。
$ ng g component components/user-list
テンプレート側
<p>ユーザー一覧</p>
<div *ngIf="auth.user | async as currentUser">
<div *ngIf="users | async as users">
<div *ngFor="let user of users">
<app-user-detail [user]="user" [currentUser]="currentUser"></app-user-detail>
</div>
</div>
</div>
TypeScript側
import { Component, OnInit } from '@angular/core';
import { AuthService } from './../../services/auth.service';
import { UsersService } from './../../services/users.service';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.css']
})
export class UserListComponent implements OnInit {
public users: Observable<any>;
constructor(
public auth: AuthService,
public usersService: UsersService
) { }
ngOnInit() {
this.users = this.usersService.getAllUsers();
}
}
AppModuleとAppRoutingModule
上記までの実装に伴って、AppModuleと、AppRoutingModuleにも変更が入ります。
AppModule
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ReactiveFormsModule } from '@angular/forms';
import { AngularFireModule } from 'angularfire2';
import { AngularFireAuthModule } from 'angularfire2/auth';
import { AngularFirestoreModule } from 'angularfire2/firestore';
import { AngularFireDatabaseModule } from 'angularfire2/database';
import { UserLoginComponent } from './pages/user-login/user-login.component';
import { UserProfileComponent } from './pages/user-profile/user-profile.component';
import { UserSignupComponent } from './pages/user-signup/user-signup.component';
import { UserListComponent } from './pages/user-list/user-list.component';
import { AuthService } from './services/auth.service';
import { AuthGuard } from './guard/auth.guard';
import { UsersService } from './services/users.service';
import { FollowService } from './services/follow.service';
import { UserDetailComponent } from './components/user-detail/user-detail.component';
import { environment } from './../environments/environment';
@NgModule({
declarations: [
AppComponent,
UserLoginComponent,
UserProfileComponent,
UserSignupComponent,
UserListComponent,
UserDetailComponent
],
imports: [
BrowserModule,
AppRoutingModule,
ReactiveFormsModule,
AngularFireModule.initializeApp(environment.firebase),
AngularFireAuthModule,
AngularFirestoreModule,
AngularFireDatabaseModule
],
providers: [
AuthService,
AuthGuard,
UsersService,
FollowService
],
bootstrap: [AppComponent]
})
export class AppModule { }
AppRoutingModule
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { UserLoginComponent } from './pages/user-login/user-login.component';
import { UserSignupComponent } from './pages/user-signup/user-signup.component';
import { UserProfileComponent } from './pages/user-profile/user-profile.component';
import { UserListComponent } from './pages/user-list/user-list.component';
import { AuthService } from './services/auth.service';
import { AuthGuard } from './guard/auth.guard';
const routes: Routes = [
{ path: '', redirectTo: '/profile', pathMatch: 'full' },
{ path: 'profile', component: UserProfileComponent, canActivate: [AuthGuard] },
{ path: 'userlist', component: UserListComponent, canActivate: [AuthGuard] },
{ path: 'login', component: UserLoginComponent },
{ path: 'signup', component: UserSignupComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
- userlist
- 今回追加した
userlist
のルーティング定義を追加しています。
- 今回追加した
上記までで、userlist
ユーザーのフォロー/アンフォロー/フォロワー数の確認が出来るようになりました。
終わりに
ユーザー情報の保存のCloudFirestoreを使いつつも、フォロー機能ではRealtimeDatabaseを使うという統一感の内向性になってしまっています。次はCloudFirestoreでリベンジしてみたいと思います。
参考
-
angularfire2 -GitHub
- AngularでFirebaseを扱うためのライブラリ
-
Cloud FirestoreのSubCollectionとQueryっていつ使うの問題
- 元々Cloud Firestoreを使おうと思っていたで、こちらの記事でfollowerコレクションの実現方法に関して言及されている部分が大変参考になりました。