今天的目標:將會員有關的元件及服務從使用者服務轉為向 ngrx/store 做 dispatch 跟 select
第一步:修改 src/app/user/login/login.component.ts
// ... 省略
import { Store } from '@ngrx/store';
import * as fromStore from '../../store';
//... 省略
export class LoginComponent implements OnInit {
    public form: FormGroup;
    constructor(
        private fb: FormBuilder,
        private store: Store<fromStore.State>,
        private router: Router,
        private snackbar: MatSnackBar
    ) { }
    login() {
        this.store.dispatch(new fromStore.LoginAction(this.form.value));
        this.store.select(fromStore.getIsLogin)
            .subscribe(res => {
                if (res) {
                    this.snackbar.open('登入成功', 'OK', { duration: 3000 });
                    this.router.navigate(['/member']);
                } else {
                    this.snackbar.open('請檢查使用者名稱及密碼', 'OK', { duration: 3000 });
                }
            })
    }
}
userService 的部分換成 store.disptch(new fromStore.LoginAction(),接著用 store.select(fromStore.getIsLogin) 來選擇狀態樹中 login 的狀態檢查一下瀏覽器,一且順利的話,畫面截圖如下
可以看到 Redux devtools 已經看到會員狀態已經改變,但是頁面還沒轉到會員,接下來
修改 src/app/guards/auth.guards.ts
//...省略
import { Store } from '@ngrx/store';
import * as fromStore from '../store';
import { tap, take } from 'rxjs/operators';
import { of } from 'rxjs/observable/of';
@Injectable()
export class AuthGuard implements CanActivate, CanLoad {
    loginStatus$: Observable<boolean>
    constructor(
        private store: Store<fromStore.State>,
        private router: Router
    ) {
        this.loginStatus$ = store.select(fromStore.getIsLogin);
    }
    //... 省略
一樣將使用者服務刪除,導入 Store,在 constructor 將 loginStatus$ 變為 store.select(fromStore.getIsLogin) 就完成
現在登入後會轉到會員畫面,但是瀏覽列還沒變,我們來修改一下
src/app/navbar/navbar.component.ts
//... 省略
import { Store } from '@ngrx/store';
import * as fromStore from '../store';
import { User } from '../models';
//... 省略
export class NavbarComponent implements OnInit {
    login$: Observable<boolean>;
    user$: Observable<string>;
    constructor(
        private store: Store<fromStore.State>,
        private router: Router
    ) { }
    ngOnInit() {
        this.login$ = this.store.select(fromStore.getIsLogin);
        this.user$ = this.store.select(fromStore.getCurrentUser);
    }
    logout() {
        this.store.dispatch(new fromStore.LogoutAction());
        this.router.navigate(['/'])
    }
}
login$ 跟 user$ 換成用 store.select()
logout 時用  store.dispatch(new fromStore.LogoutAction()) 來改變狀態接下來,修改開始服務
src/app/services/startup.service.ts
//... 省略
import { UtilsService } from '.';
import { Store } from '@ngrx/store';
import * as fromStore from '../store';
@Injectable()
export class StartupService {
    constructor(
        private injector: Injector,
        private utils: UtilsService,
        private store: Store<fromStore.State>
    ) { }
    
    load(): Promise<any> {
        return new Promise((resolve, reject) => {
            if (!this.utils.isTokenExpired()) {
                this.store.dispatch(new fromStore.GetUserAction());
                return this.store.select(fromStore.getIsLogin)
                    .pipe(
                    filter(status => status)
                    )
                    .subscribe(res => {
                        if (res) {
                            setInterval(() => {
                                this.checkStatus();
                            }, 1000 * 60 * 5)    // check current status every 5 min
                        }
                        resolve(res);
                    }, err => {
                        console.log(err);
                        reject(err);
                    });
            } else {
                resolve('no token or token expired');
            }
        });
    }
    checkStatus() {
        if (this.utils.isTokenExpired()) {   // if token expired or not exist
            this.store.dispatch(new fromStore.LogoutAction());
            const router = this.injector.get(Router);
            router.navigate(['/']);
        }
    }
}
load() 加入判斷 token 有沒有過期store.dispatch(new fromStore.GetUserAction()) 來從後端取得使用者資訊,這裡是非同步,我們用 filter 來等後端資訊完成,因為 token 未過期,所以 status 一定要是真checkStatus() 當 token 過期時,使用 store.dispatch(new fromStore.LogoutAction()) 來改變狀態src/app/user/service/user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { AppConfig } from '../../share';
import { User, Response } from '../../models';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { of } from 'rxjs/observable/of';
import { UtilsService } from '../../services';
@Injectable()
export class UserService {
    constructor(
        private http: HttpClient,
        private appConfig: AppConfig,
        private utils: UtilsService
    ) { }
    loginServer(loginData): Observable<Response> {
        let username = loginData.username.trim();
        let password = loginData.password.trim();
        return this.http.post<Response>(this.appConfig.apiUrl + '/users/authenticate', { username: username, password: password });
    }
    // get user from server
    getUserFromServer(): Observable<User> {
        if (!this.utils.isTokenExpired()) {
            const token = this.utils.getToken();
            return this.http.post(this.appConfig.apiUrl + '/users/currentUser', { 'token': token })
                .map((res: Response) => {
                    if (res.success) {
                        return { username: res.payload };
                    } else {
                        return null;
                    }
                })
        } else {
            return of(null);
        }
    }
}
loginServer() 跟 getUserFromServer()
getUserFromServer(),這裡有一個 Bug, 應該要回 { username: res.payload } 而不是 res.payload 因為後端的 res.payload 只是一個 string我們已經完成會員的部分,也就是所有跟會員有關的元件,現在只對 Store,不再經由使用者服務。
接下來在做跟報告有關的元件之前,也們先來看一下路由的部分,路由 (Router) 也可以當成是狀態的一部份,因為在報告的模組中,我們會用到 /member/report/3 這樣的路徑,而之前我們用報告服務來抓這個單獨的報告,現在我們想從 Store 的狀態樹中,結合報告狀態跟路由狀態來取得這份單獨的報告,所以我們先來做路由 (Router) 的 ngrx/store