每日一句來源:Daily English
The only limit to our realization of tomorrow will be our doubts of today. --實現明天理想的唯一障礙是今天的疑慮。(FranklinD. Roosevelt)
今天我們來實做登入中的功能,透過realtime database的特性來達到即時的登入狀態,並透過trigger來將資料一併更新到firestore。
透過realtime Database提供的特別物件'.info/connected'
來操作
建立一個LoginStatusService
來統一管理,
實作如下
import { Injectable } from '@angular/core';
import { BaseHttpService } from '@core/service/base-http.service';
import { map, tap, filter, combineLatest, skipWhile, switchMap } from 'rxjs/operators';
import * as firebase from 'firebase';
import { AuthService } from './auth.service';
import { dbTimeObject } from '@core/service/base-http.service/model/realtime-database/db.time.function';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { RxViewer } from '@shared/ts/rx.viewer';
import { catchError } from 'rxjs/operators/catchError';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
@Injectable()
export class LoginStatusService {
private _disconnection: firebase.database.OnDisconnect;
private userStatusRef: firebase.database.Reference;
constructor(private _http: BaseHttpService, private _auth: AuthService) {
this._http.object('.info/connected').get(false).pipe(
combineLatest(this._auth.currentUser$.pipe(
skipWhile(u => !!u),
map(u => {
if (u) {
return this.userStatusRef = firebase.database().ref(`/status/${u.uid}`);
} else {
if (this.userStatusRef) {
console.log('下線');
this.setOffLine(this.userStatusRef);
}
return null;
}
})
)),
tap(([connected, userRef]) => {
if (connected && userRef && !this._disconnection) {
this._disconnection = userRef.onDisconnect();
userRef.onDisconnect().set(dbTimeObject({ state: false }, false))
.then(() => {
console.log('上線');
return this.setOnLine(userRef);
})
.catch(e => console.log(e));
} else if (!connected || !userRef) {
if (this._disconnection) {
this._disconnection.cancel();
this._disconnection = undefined;
}
}
})
).subscribe();
}
setOnLine(userRef) {
return userRef.set(dbTimeObject({ state: true }, false));
}
setOffLine(userRef) {
return userRef.set(dbTimeObject({ state: false }, false));
}
}
這邊我們直接使用firebase js實做,因為在angularfire2並未包裝,注意當我們離線的時候記得把事件取消,不然關閉瀏覽器時,會多送離線事件出去。
接著我們透過trigger來更新firestore
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
export const userStatusFirestore = functions.database
.ref('/status/{uid}').onUpdate(event => {
const firestore = admin.firestore();
const eventStatus = event.data.val();
const userRef = firestore.doc(`users/${event.params.uid}`);
// 資料可能快速的被做修改,如果我們發現時間事件時間小於資料庫的更新時間,不做處理
return event.data.ref.once("value").then((statusSnapshot) => {
return statusSnapshot.val();
}).then((status) => {
if (status.updatedAt > eventStatus.updatedAt) return null;
// 把資料轉乘時間格式,直接寫回使用者的資料內
return userRef.update({
loginStatus: status.state,
lastSignInTime: new Date(eventStatus.updatedAt)
});
});
});
這裡因為我們的資料可能快速的被修改,而trigger的速度會慢一些,為了避免多餘的複寫,我們重新讀取一次資料,若發現時間較晚,不執行任何事情,會有下一個任務來負責複寫的行為。
最後我們在使用者手動登出的時候也要做處理,把資料庫的狀態做修改,這裡我們使用我們包裝好的document直接做處理即可,
因為我們最後是想要使用firestore來顯示登入狀態,我們就不必修改realtime的資料了,那只是為了透過realtime database的特行來觸發我們的trigger,把資料寫入firestore,當然你要使用realtime database再次啟用狀態也是可以的。
signOut() {
return this.currentUserHandler.update(<any>{
loginStatus: false,
lastSignInTime: firebase.firestore.FieldValue.serverTimestamp()
}, false).pipe(
mergeMap(() => this._cms.deleteToken()),
tap(() => {
this._router.navigate(environment.nonAuthenticationUrl);
this._afAuth.auth.signOut();
})
);
}
完成!
透過'.info/connected'來得知當前的連線狀態,並且當使用者斷開的時候直接透過先前設定好的方法回傳,最後再透過Trigger來將資料存到store,可說是很優雅,也是realtime真正的強大之處,雖然目前firestore並沒有realtime連線狀態的API不過或許將來可能會有,不過使用realtime搭配也是很不錯的方式。
注意筆者今天把所有rxjs升級到pipeable operator了,可能會有夥伴不習慣,不過其實大同小異,筆者很建議大家升級,這關西到最後檔案的大小、整體開發時的穩定性,不再使用rxjs/add的方式來加上方法了,詳細可以看這裡:
https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md
所有方法都包裝在pipe裡面,另外有幾個比較常用的operator也改了
do
-> tap
catch
-> catchError
switch
-> switchAll
finally
-> finalize
大家使用上要注意,另外還有Observable的修改,都是直接使用function開頭了,不會再使用Observable.()的方式
以下舉例:Observable.throw(new Error('no key!'))
-> ErrorObservable.create(new Error('no key!'));
Observable.of(1)
-> of(1)
大家要注意,筆者今天寫下來的感覺是變得蠻乾淨的,但是要熟悉一下,另外就是自定義跟封裝變得更簡單了,因為都是一個一個function,最重要的是檔案大小!!!!會被搖掉了~~~~大家可以看這篇比較文:
https://hackernoon.com/rxjs-reduce-bundle-size-using-lettable-operators-418307295e85
強烈建議大家要更新,我想官方也是迫於無奈才改了,不然原本使用.的方式來撰寫真的是很不錯了。
名稱 | 網址 |
---|---|
Angular | https://github.com/ZouYouShun/Angular-firebase-ironman/tree/day27_login_status |
functions | https://github.com/ZouYouShun/Angular-firebase-ironman-functions/tree/day27_login_status |
https://firebase.google.com/docs/reference/android/com/google/firebase/database/OnDisconnect
https://github.com/firebase/functions-samples/blob/master/presence-firestore/README.md