@ngrx/effect
前面我们提到,在 Book 的 reducer 中,并没有 Search 这个 Action 的处理,由于它需要发出一个异步的请求,等到请求返回前端,我们需要根据返回的结果来操作 store。所以,真正操作 store 的应该是 Search_Complete 这个 Action。我们在 recducer 已经看到了。
对于 Search 来说,我们需要见到这个 Action 就发出一个异步的请求,等到异步处理完毕,根据返回的结果,构造一个 Search_Complete 来将处理的结果派发给 store 进行处理。
这个解耦是通过 @ngrx/effect 来处理的。
@ngrx/effect 提供了装饰器 @Effect 和 Actions 来帮助我们检查 store 派发出来的 Action,将特定类型的 Action 过滤出来进行处理。监听特定的 Action, 当发现特定的 Action 发出之后,自动执行某些操作,然后将处理的结果重新发送回 store 中。
Book 搜索处理
以 Book 为例,我们需要监控 Search 这个 Action, 见到这个 Action 就发出异步的请求,然后接收请求返回的数据,如果在接收完成之前,又遇到了下一个请求,那么,直接结束上一个请求的返回数据。如何找到下一个请求呢?使用 skip 来获取下一个 Search Action。
源码
/src/effects/book.ts
import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/skip'; import 'rxjs/add/operator/takeUntil'; import { Injectable } from '@angular/core'; import { Effect, Actions, toPayload } from '@ngrx/effects'; import { Action } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import { empty } from 'rxjs/observable/empty'; import { of } from 'rxjs/observable/of'; import { GoogleBooksService } from '../services/google-books'; import * as book from '../actions/book'; /** * Effects offer a way to isolate and easily test side-effects within your * application. * The `toPayload` helper function returns just * the payload of the currently dispatched action, useful in * instances where the current state is not necessary. * * Documentation on `toPayload` can be found here: * https://github.com/ngrx/effects/blob/master/docs/api.md#topayload * * If you are unfamiliar with the operators being used in these examples, please * check out the sources below: * * Official Docs: http://reactivex.io/rxjs/manual/overview.html#categories-of-operators * RxJS 5 Operators By Example: https://gist.github.com/btroncone/d6cf141d6f2c00dc6b35 */ @Injectable() export class BookEffects { @Effect() search$: Observable<Action> = this.actions$ .ofType(book.ActionTypes.SEARCH) .debounceTime(300) .map(toPayload) .switchMap(query => { if (query === '') { return empty(); } const nextSearch$ = this.actions$.ofType(book.ActionTypes.SEARCH).skip(1); return this.googleBooks.searchBooks(query) .takeUntil(nextSearch$) .map(books => new book.SearchCompleteAction(books)) .catch(() => of(new book.SearchCompleteAction([]))); }); constructor(private actions$: Actions, private googleBooks: GoogleBooksService) { } }
collection.ts
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/startWith'; import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/toArray'; import { Injectable } from '@angular/core'; import { Action } from '@ngrx/store'; import { Effect, Actions } from '@ngrx/effects'; import { Database } from '@ngrx/db'; import { Observable } from 'rxjs/Observable'; import { defer } from 'rxjs/observable/defer'; import { of } from 'rxjs/observable/of'; import * as collection from '../actions/collection'; import { Book } from '../models/book'; @Injectable() export class CollectionEffects { /** * This effect does not yield any actions back to the store. Set * `dispatch` to false to hint to @ngrx/effects that it should * ignore any elements of this effect stream. * * The `defer` observable accepts an observable factory function * that is called when the observable is subscribed to. * Wrapping the database open call in `defer` makes * effect easier to test. */ @Effect({ dispatch: false }) openDB$: Observable<any> = defer(() => { return this.db.open('books_app'); }); /** * This effect makes use of the `startWith` operator to trigger * the effect immediately on startup. */ @Effect() loadCollection$: Observable<Action> = this.actions$ .ofType(collection.ActionTypes.LOAD) .startWith(new collection.LoadAction()) .switchMap(() => this.db.query('books') .toArray() .map((books: Book[]) => new collection.LoadSuccessAction(books)) .catch(error => of(new collection.LoadFailAction(error))) ); @Effect() addBookToCollection$: Observable<Action> = this.actions$ .ofType(collection.ActionTypes.ADD_BOOK) .map((action: collection.AddBookAction) => action.payload) .mergeMap(book => this.db.insert('books', [ book ]) .map(() => new collection.AddBookSuccessAction(book)) .catch(() => of(new collection.AddBookFailAction(book))) ); @Effect() removeBookFromCollection$: Observable<Action> = this.actions$ .ofType(collection.ActionTypes.REMOVE_BOOK) .map((action: collection.RemoveBookAction) => action.payload) .mergeMap(book => this.db.executeWrite('books', 'delete', [ book.id ]) .map(() => new collection.RemoveBookSuccessAction(book)) .catch(() => of(new collection.RemoveBookFailAction(book))) ); constructor(private actions$: Actions, private db: Database) { } }
See also: