Redux你的Angular 2应用--ngRx使用体验 | 掘金技术征文

第一节:初识Angular-CLI
第二节:登陆组件的构建
第三节:创建一个待办事项应用
第四节:进化!模块化你的应用
第五节:多用户版本的待办事项应用
第六节:使用第三方样式库及模块优化用
第七节:给组件带来活力
Rx--隐藏在Angular 2.x中利剑
Redux你的Angular 2应用
第八节:查缺补漏大合集(上)
第九节:查缺补漏大合集(下)javascript

标题写错了吧,是React吧?没错,你没看错,就是Angular2。若是说RxJS是Angular2开发中的倚天剑,那么Redux就是屠龙刀了。并且这两种神兵利器都是不依赖于平台的,左手倚天右手屠龙......算了,先不YY了,回到正题。css

Redux目前愈来愈火,已经成了React开发中的事实标准。火到什么程度,Github上超过26000星。html

Redux的Github项目页面,超过26000星

那么什么到底Redux作了什么?这件事又和Angular2有几毛钱关系?别着急,咱们下面就来说一下。前端

什么是Redux?

Redux是为了解决应用状态(State)管理而提出的一种解决方案。那么什么是状态呢?简单来讲对于应用开发来说,UI上显示的数据、控件状态、登录状态等等所有能够看做状态。java

咱们在开发中常常会碰到,这个界面的按钮须要在某种状况下变灰;那个界面上须要根据不一样状况显示不一样数量的Tab;这个界面的某个值的设定会影响另外一个界面的某种展示等等。应该说应用开发中最复杂的部分就在于这些状态的管理。不少项目随着需求的迭代,代码规模逐渐扩大、团队人员水平良莠不齐就会遇到各类状态管理极其混乱,致使代码的可维护性和扩展性下降。git

那么Redux怎么解决这个问题呢?它提出了几个概念:Reducer、Action、Store。github

Store

能够把Store想象成一个数据库,就像咱们在移动应用开发中使用的SQLite同样,Store是一个你应用内的数据(状态)中心。Store在Redux中有一个基本原则:它是一个惟一的、状态不可修改的树,状态的更新只能经过显性定义的Action发送后触发。web

Store中通常负责:保存应用状态、提供访问状态的方法、派发Action的方法以及对于状态订阅者的注册和取消等。chrome

遵照这个约定的话,任什么时候间点的Store的快照均可以提供一个完整当时的应用状态。这在调试应用时会变得很是方便,有没有想过在调试时能够任意的返回前面的某一时间点?Redux的TimeMachine调试器会带咱们进行这种时光旅行,后面咱们会一块儿体验!数据库

Reducer

我在有一段时间一直以为Reducer这个东西很差理解,主要缘由有两个:

其一是这个英语单词有多个含义,在词典上给出的最靠前的意思是渐缩管和减压阀。我以前一直望文生义的以为这个Reducer应该有减速做用,感受是否是和Rx的zip有点像(这个理解是错的,只是当时看到这个词的感受)。

其二是我看了Redux的做者的一段视频,里面他用数组的reduce方法来作类比,而我以前对reduce的理解是reduce就是对数组元素进行累加计算成为一个值。

数组的reduce方法定义

其实做者也没有说错,由于数组的reduce操做就是给出不断的用序列中的值通过累加器计算获得新的值,这和旧状态进入reducer经处理返回新状态是同样的。只不过打的这个比方我比较无感。

这两个因素致使我当时没理解正确reducer的含义,如今我比较喜欢把reducer的英文解释成是“异形接头”(见下图)。Reducer的做用是接收一个状态和对应的处理(Action),进行处理后返回一个新状态。

不少网上的文章说能够把Reducer想象成数据库中的表,也就是Store是数据库,而一个reducer就是其中一张表。我其实以为Reducer不太像表,仍是以为这个“异形接头”的概念比较适合我。

异形接头

Reducer是一个纯javascript函数,接收2个参数:第一个是处理以前的状态,第二个是一个可能携带数据的动做(Action)。就是相似下面给出的接口定义,这个是TypeScript的定义,因为JavaScript中没有强类型,因此用TypeScript来理解一下。

export interface Reducer<T> {
  (state: T, action: Action): T;
}复制代码

那么纯函数是意味着什么呢?意味着咱们理论上能够把reducer移植到全部支持Redux的框架上,不用作改动。下面咱们来看一段简单的代码:

export const counter: Reducer<number> = (state = 0, action) => {
    switch(action.type){
        case 'INCREMENT':
            return state + action.payload;
        case 'DECREMENT':
            return state - action.payload;
        default:
            return state;
    }
};复制代码

上面的代码定义了一个计数器的Reducer,一开始的状态初始值为0((state = 0, action) 中的 state=0 给state赋了一个初始状态值)根据Action类型的不一样返回不一样的状态。这段代码就是很是简单的javascript,不依赖任何框架,能够在React中使用,也能够在接下来的咱们要学习的Angular2中使用。

Action

Store中存储了咱们的应用状态,Reducer接收以前的状态并输出新状态,可是咱们如何让Reducer和Store之间通讯呢?这就是Action的职责所在。在Redux规范中,全部的会引起状态更新的交互行为都必须经过一个显性定义的Action来进行。

下面的示意图描述了若是使用上面代码的Reducer,显性定义一个Action {type: 'INCREMENT', payload: 2} 而且 dispatch 这个Action后的流程。

显性定义的Action触发Reducer产生新的状态

好比说以前的计数器状态是1,咱们派送这个Action后,reducer接收到以前的状态1做为第一个参数,这个Action做为第二个参数。在Switch分支中走的是INCRMENT这个流程,也就是state+action.payload,输出的新状态为3.这个状态保存到Store中。

值得注意的一点是payload并非一个必选项,看一下Action的TypeScript定义,注意到 payload?: any 那个 ? 没有,那个就是说这个值能够没有。

export interface Action {
  type: string;
  payload?: any;
}复制代码

为何要在Angular2中使用?

首先,正如C#当初在主流强类型语言中率先引入Lamda以后,如今Java8也引入了这个特性同样,全部的好的模式、好的特性最终会在各个平台框架上有体现。Redux自己在React社区中的大量使用自己已经证实这种状态管理机制是很是健壮的。

再有咱们能够来看一下在Angular中现有的状态管理机制是什么样子的。目前的管理机制就是...嗯...没有统一的状态管理机制。

遍地开花的Angular状态管理

这种没有统一管理机制的状况在一个大团队是很恐怖的事情,状态管理的代码质量彻底看我的水平,这样会致使功能愈来愈多的应用中的状态几乎是没法测试的。

仍是用代码来讲话吧,下面咱们看一下一个不用Redux管理的Angular应用是怎样的。咱们就拿最多见的Todo应用来解析(题外话:这个应用已经变成web框架的标准对标项目了,就像上个10年的PetStore是第一代web框架的对标项目同样。)

第一种状态管理:咱们在组件中管理。在组件中能够声明一个数组,这个数组做为todo的内存存储。每次操做好比新增(addTodo)或切换状态(toggleTodo)首先调用服务中的方法,而后手动操做数组来更新状态。

export class TodoComponent implements OnInit {
  desc: string = '';
  todos : Todo[] = [];//在组件中创建一个内存TodoList数组

  constructor(
    @Inject('todoService') private service,
    private route: ActivatedRoute,
    private router: Router) {}
  ngOnInit() {
    this.route.params.forEach((params: Params) => {
      let filter = params['filter'];
      this.filterTodos(filter);
    });
  }
  addTodo(){
    this.service
      .addTodo(this.desc) //经过服务新增数据到服务器数据库
      .then(todo => {//更新todos的状态
        this.todos.push(todo);//使用了可改变的数组操做方式
      });
  }
  toggleTodo(todo: Todo){
    const i = this.todos.indexOf(todo);
    this.service
      .toggleTodo(todo)//经过服务更新数据到服务器数据库
      .then(t => {//更新todos的状态
        const i = todos.indexOf(todo);
        todos[i].completed = todo.completed; //使用了可改变的数组操做方式
      });
  }
  ...复制代码

第二种方式呢,咱们在服务中作相似的事情。在服务中定义一个内存存储(dataStore),而后一样是在更新服务器数据后手动更新内存存储。这个版本当中咱们使用了RxJS,但大致逻辑是差很少的。固然使用Rx的好处比较明显,组件只需访问todos属性方法便可,组件内的逻辑会比较简单。

...
export class TodoService {

  private api_url = 'http://localhost:3000/todos';
  private headers = new Headers({'Content-Type': 'application/json'});
  private userId: string;
  private _todos: BehaviorSubject<Todo[]>; 
  private dataStore: {  // 咱们本身实现的内存数据存储
    todos: Todo[]
  };
  constructor(private http: Http, @Inject('auth') private authService) {
    this.authService.getAuth()
      .filter(auth => auth.user != null)
      .subscribe(auth => this.userId = auth.user.id);
    this.dataStore = { todos: [] };
    this._todos = new BehaviorSubject<Todo[]>([]);
  }
  get todos(){
    return this._todos.asObservable();
  }
  // POST /todos
  addTodo(desc:string){
    let todoToAdd = {
      id: UUID.UUID(),
      desc: desc,
      completed: false,
      userId: this.userId
    };
    this.http
      .post(this.api_url, JSON.stringify(todoToAdd), {headers: this.headers})
      .map(res => res.json() as Todo) //经过服务新增数据到服务器数据库
      .subscribe(todo => {
        //更新内存存储todos的状态
        //使用了不可改变的数组操做方式
        this.dataStore.todos = [...this.dataStore.todos, todo];
        //推送给订阅者新的内存存储数据
        this._todos.next(Object.assign({}, this.dataStore).todos);
      });
  }
  toggleTodo(todo: Todo) {
    const url = `${this.api_url}/${todo.id}`;
    const i = this.dataStore.todos.indexOf(todo);
    let updatedTodo = Object.assign({}, todo, {completed: !todo.completed});
    this.http
      .patch(url, JSON.stringify({completed: !todo.completed}), {headers: this.headers})//经过服务更新数据到服务器数据库
      .subscribe(_ => {
        //更新内存存储todos的状态
        this.dataStore.todos = [
          ...this.dataStore.todos.slice(0,i),
          updatedTodo,
          ...this.dataStore.todos.slice(i+1)
        ];//使用了不可改变的数组操做方式
        //推送给订阅者新的内存存储数据
        this._todos.next(Object.assign({}, this.dataStore).todos);
      });
  }
...
}复制代码

固然还有不少方式,好比服务中维护一部分,组件中维护一部分;再好比说有的同窗可能使用localStorage作存储,每次读来写去等等。

不是说这些方式很差(若是能够保持项目组内的规范统一,项目较小的状况下也还能够),而是说代码编写的方式太多了,并且状态分散在各个组件和服务中,没有统一管理。一个小项目可能尚未问题,但大项目就会发现内存状态很难统一维护。

更不用说在Angular2中咱们写了不少组件里的EventEmitter只是为了把某个事件弹射到父组件中而已。而这些在Redux的模式下,均可以很方便的解决,咱们一样能够很自由的在服务或组件中引用store。但无论怎样编写,咱们遵照的一样的规则,维护的是应用惟一状态树。

Angular 1.x永久的改变了JQuery类型的web开发,使得咱们能够像写手机客户端App同样来鞋前端代码。Redux也同样改变了状态管理的写法,Redux其实不只仅是一个类库,更是一种设计模式。并且在Angular2 中因为有RxJS,你会发现咱们甚至比在React中使用时更方便更强大。

在Angular 2中使用Redux

ngrx是一套利用RxJS的类库,其中的 @ngrx/store (github.com/ngrx/store) 就是基于Redux规范制定的Angular2框架。接下来咱们一块儿看看如何使用这套框架作一个Todo应用。

打造一个有Http后台的Todo列表应用

对Angular2 不熟悉的童鞋能够去 github.com/wpcfan/awes… 看个人Angular 2: 从0到1系列

简单内存版

固然第一步是安装 npm install @ngrx/core @ngrx/store --save。而后须要在你想要使用的Module里面引入store,我推荐在根模块 AppModule或CoreModule(把只在应用中加载一次的全局性东东单独放到一个Module中而后在AppModule引入) 引入这个包,由于Store是整个应用的状态树。

import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { AuthService } from './auth.service';
import { UserService } from './user.service';
import { AuthGuardService } from './auth-guard.service';

import { HttpModule, JsonpModule } from '@angular/http';
import { StoreModule } from '@ngrx/store';
import { todoReducer, todoFilterReducer } from '../reducers/todo.reducer';
import { authReducer } from '../reducers/auth.reducer';

@NgModule({
  imports:[
    HttpModule
    StoreModule.provideStore({ 
      todos: todoReducer, 
      todoFilter: todoFilterReducer
    })
  ],
  providers: [
    AuthService,
    UserService,
    AuthGuardService
    ]
})
export class CoreModule {
  constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error(
        'CoreModule is already loaded. Import it in the AppModule only');
    }
  }
}复制代码

咱们看到StoreModule提供了一个provideStore方法,在这个方法中咱们声明了一个 { todos: todoReducer, todoFilter: todoFilterReducer }对象,这个就是Store。前面讲过Store能够想象成数据库,Reducer能够想象成表,那么这样一个对象形式告诉咱们数据库是由那些表构成的(这个地方把Reducer想象成表仍是有道理的).

那么能够看到咱们定义了两个Reducer:todoReducer和todoFilterReducer。在看代码以前,咱们来思考一下这个流程,所谓Reducer其实就是接收两个参数:以前的状态和要采起的动做,而后返回新的状态。可能动做更好想一些,先看看有什么动做吧:

  • 新增一个Todo
  • 删除一个Todo
  • 更改Todo的完成状态
  • 所有反转Todo的完成状态
  • 清除已完成的Todo
  • 筛选所有Todo
  • 筛选未完成的Todo
  • 筛选已完成的Todo

可是仔细分析一下发现后三个动做其实和前面的不太同样,由于后面的三个都属于筛选,并未改动数据自己。也不用提交后台服务,只须要对内存数据作简单筛选便可。前面几个都须要不光改变内存数据也须要改变服务器数据。

这里咱们先尝试着写一下前面五个动做对应的Reducer,按前面定义的就叫todoReducer吧,一开始也不知道怎么写好,那就先写个骨架吧:

export const todoReducer = (state = [], {type, payload}) => {
  switch (type) {
    default:
      return state;
  }
}复制代码

即便是个骨架,也有不少有意思的点。

第一个参数是state,就像咱们在组件或服务中本身维护了一个内存数组同样,咱们的Todo状态其实也是一个数组,咱们还赋了一个空数组的初始值(避免出现undefined错误)。

第二个参数是一个有type和payload两个属性的对象,其实就是Action。也就是说咱们其实能够不用定义Action,直接给出构造的对象形式便可。内部的话其实reducer就是一个大的switch语句,根据不一样的Action类型决定返回什么样的状态。默认状态下咱们直接将以前状态返回便可。Reducer就是这么单纯的一个函数。

如今咱们来考虑其中一个动做,增长一个Todo,咱们须要发送一个Action,这个Action的type是 ’ADD_TODO’ ,payload就是新增长的这个Todo。

逻辑其实就是列表数组增长一个元素,用数组的push方法直接作是否是就好了呢?不行,由于Redux的约定是必须返回一个新状态,而不是更新原来的状态。而push方法实际上是更新原来的数组,而咱们须要返回一个新的数组。感谢ES7的Object Spread操做符,它可让咱们很是方便的返回一个新的数组。

export const todoReducer = (state = [], {type, payload}) => {
  switch (type) {
    case 'ADD_TODO':
      return [
          ...state,
          action.payload
          ];
    default:
      return state;
  }
}复制代码

如今咱们已经有了一个能够处理 ADD_TODO 类型的Reducer。可能有的同窗要问这只是改变了内存的数据,咱们怎么处理服务器的数据更改呢?要不要在Reducer中处理?答案是服务器数据处理的逻辑是服务(Service)的职责,Reducer不负责那部分。后面咱们会处理服务器的数据更新的。

接下来工做就很简单了,咱们在TodoComponent中去引入Store而且在适当的时候dispatch ‘ADD_TODO’这个Action就OK了。

...
export class TodoComponent {
  ...
  todos : Observable<Todo[]>;
  constructor(private store$: Store<Todo[]>) {
  ...
    this.todos = this.store$.select('todos');
  }

  addTodo(desc: string) {
    let todoToAdd = {
      id: '1',
      desc: desc,
      completed: false
    }
    this.store$.dispatch({type: 'ADD_TODO', todoToAdd});
  }
  ...
}复制代码

利用Angular提供的依赖性注入(DI),咱们能够很是方便的在构造函数中注入Store。因为Angular2对于RxJS的内建支持以及 @ngrx/store 自己也是基于RxJS来构造的,咱们彻底不用Redux的注册订阅者等行为,访问todos这个状态,只须要写成 this.store$.select('todos')就能够了。这个store后面有个 $ 符号是表示这是一个流(Stream,只是写法上的惯例),也就是Observable。而后在addTodo方法中把action发送出去就完事了,固然这个方法是在按Enter键时触发的。

<div>
  <app-todo-header placeholder="What do you want" (onEnterUp)="addTodo($event)" >
  </app-todo-header>
  <app-todo-list [todos]="todos | async" (onToggleAll)="toggleAll()" (onRemoveTodo)="removeTodo($event)" (onToggleTodo)="toggleTodo($event)" >
  </app-todo-list>
  <app-todo-footer [itemCount]="(todos | async)?.length" (onClear)="clearCompleted()">
  </app-todo-footer>
</div>复制代码

彷佛有点太简单了吧,但真的是这样,比在React中使用还要简便。Angular2中对于Observable类型的变量提供了一个Async Pipe,就是 todos | async ,咱们连在OnDestroy中取消订阅都不用作了。

下面咱们把reducer的其余部分补全吧。除了处理todoReducer中其余的swtich分支,咱们为其添加了强类型,既然是在Angular2中使用TypeScript开发,咱们仍是但愿享受强类型带来的各类便利之处。另外老是对于Action的Type定义了一系列常量。

import { Reducer, Action } from '@ngrx/store';
import { Todo } from '../domain/entities';
import { 
  ADD_TODO, 
  REMOVE_TODO, 
  TOGGLE_TODO,
  TOGGLE_ALL,
  CLEAR_COMPLETED,
  FETCH_FROM_API,
  VisibilityFilters
} from '../actions/todo.action';

export const todoReducer = (state: Todo[] =[], action: Action) => {
  switch (action.type) {
    case ADD_TODO:
      return [
          ...state,
          action.payload
          ];
    case REMOVE_TODO:
      return state.filter(todo => todo.id !== action.payload.id);
    case TOGGLE_TODO:
      return state.map(todo => {
        if(todo.id !== action.payload.id){
          return todo;
        }
        return Object.assign({}, todo, {completed: !todo.completed});
      });
    case TOGGLE_ALL:
      return state.map(todo => {
        return Object.assign({}, todo, {completed: !todo.completed});
      });
    case CLEAR_COMPLETED:
      return state.filter(todo => !todo.completed);
    case FETCH_FROM_API:
      return [
        ...action.payload
      ];
    default:
      return state;
  }
}

export const todoFilterReducer = (state = (todo: Todo) => todo, action: Action) => {
  switch (action.type) {
    case VisibilityFilters.SHOW_ALL:
      return todo => todo;
    case VisibilityFilters.SHOW_ACTIVE:
      return todo => !todo.completed;
    case VisibilityFilters.SHOW_COMPLETED:
      return todo => todo.completed;
    default:
      return state;
  }
}复制代码

上面的todoReducer看起来倒仍是很正常,这个todoFilterReducer却形迹十分可疑,它的state看上去是个函数。是的,你的判断是对的,的确是函数。

为何咱们要这么设计呢?缘由是这几个过滤器,其实只是对内存数组进行筛选操做,那么就能够经过 arr.filter(callback[, thisArg]) 来进行筛选。数组的filter方法的含义是对于数组中每个元素经过callback的测试,而后返回值组成一个新数组。因此这个Reducer中咱们的状态实际上是不一样条件的测试函数,就是那个callback。

好,咱们一块儿把这个没有后台API的版本先完成了吧,要完成的其余部分都很简单,好比toggle、remove什么的,由于只是调用store的dispatch方法把Action发送出去便可。

...
export class TodoComponent {

  todos : Observable<Todo[]>;

  constructor(
    private service: TodoService,
    private route: ActivatedRoute,
    private store$: Store<Todo[]>) {
      const fetchData$ = this.store$.select('todos')
        .startWith([]);
      const filterData$ = this.store$.select('todoFilter');
      this.todos = Observable.combineLatest(
        fetchData$,
        filterData$,
        (todos: Todo[], filter: any) => todos.filter(filter)
      )
    }
  ngInit(){
    this.route.params.pluck('filter')
      .subscribe(value => {
        const filter = value as string;
        this.store$.dispatch({type: filter});
      })
  }
  addTodo(desc: string) {
    let todoToAdd = {
      id: UUID.UUID(),
      desc: desc,
      completed: false
    };
    this.store$.dispatch({
      type: ADD_TODO, 
      payload: todoToAdd
    });
  }
  toggleTodo(todo: Todo) {
    let updatedTodo = Object.assign({}, todo, {completed: !todo.completed});
    this.store$.dispatch({
      type: TOGGLE_TODO, 
      payload: updatedTodo
    });
  }
  removeTodo(todo: Todo) {
    this.store$.dispatch({
      type: REMOVE_TODO,
      payload: todo
    });
  } 
  toggleAll(){
    this.store$.dispatch({
      type: TOGGLE_ALL
    });
  }
  clearCompleted(){
    this.store$.dispatch({
      type: CLEAR_COMPLETED
    });
  }
}复制代码

咱们一块儿看看过滤器部分怎么处理咱们实现的,咱们知道目前有两个和todo有关的Reducer:todoReducer和todoFilterReducer。这两个应该是配合来影响状态的,咱们不能够在没有任何一方的状况下独立返回正常的状态。怎么理解呢?打个比方吧,咱们添加了几个Todo以后,这些Todo确定知足某个过滤器的条件测试,而不可能存在一个Todo在任何一个过滤器中都不知足其条件。

那么如何配合处理这两个状态流呢(在@ngrx/store中,它们都是流)?从新描述一下对这两个流的要求,为方便起见,咱们叫todos流和filter流。咱们想要这样的一个合并流,这个合并流的数据来自于todos流和filter流。并且合并流的每一个数据都来自于一对最新的todos流数据和filter流数据,固然存在一种状况:一个流产生了新数据,但另外一个没有。这种状况下,咱们会使用新产生的这个数据和另外一个流中以前最新的那个配对产生合并流的数据。

这在Rx世界太简单了,combineLatest操做符干的就是这样一件事。因而咱们看到下面这段代码:咱们合并了todos流和filter流,并且在以它们各自的最新数据为参数的一个函数产生了新的合并流的数据 todos.filter(filter)。稍微解释一下,todos流中的数据就是todo数组,咱们在todoReducer中就是这样定义的,而filter流中的数据是一个函数,那么咱们其实就是使用从todos流中的最新数组,调用todos.filter方法而后把filter流中的最新的函数当成todos.filter的参数。

const fetchData$ = this.store$.select('todos').startWith([]);
const filterData$ = this.store$.select('todoFilter');
this.todos = Observable.combineLatest(
  fetchData$,
  filterData$,
  (todos: Todo[], filter: any) => todos.filter(filter)
)复制代码

还有一处须要解释而且优化的代码位于ngInit中的那段,咱们把它分拆出来列在下面。咱们在Todo里面实现过滤器时使用的是Angular2的路由参数,也就是 todo/:filter 这种形式(咱们定义在 todo-routing.module.ts 中了 ),好比若是过滤器是 ALL,那么这个表现形式就是 todo/ALL。下面代码中的 this.route.params.pluck('filter') 就是取得这个filter路由参数的值。而后咱们dispatch了要进行过滤的action。

ngInit(){
  this.route.params.pluck('filter')
    .subscribe(value => {
      const filter = value as string;
      this.store$.dispatch({type: filter});
    })
  }复制代码

虽然说如今的形式已经能够正常工做了,但总以为这个路由参数的获取单独放在这里有点别扭,由于逻辑上这个路由参数流和filter流是有前后顺序的,并且后者依赖前者,但这种逻辑关系没有体现出来。

嗯,来优化一下,Rx的一个优势就是能够把一系列操做串(chain)起来。从时间序列上看这个路由参数的获取是先发生的,而后获取到这个参数filter流才会有做用,那么咱们优化的点就在于怎么样把这个路由参数流和filter流串起来。

const filterData$ = this.route.params.pluck('filter')
  .do(value => {
    const filter = value as string;
    this.store$.dispatch({type: filter});
  })
  .flatMap(_ => this.store$.select('todoFilter'));复制代码

上面的代码把原来独立的两个流串了起来,逻辑关系有两层:

首先时间顺序要保证,也就是说路由参数的先有数据后 this.store$.select('todoFilter') 才能够工做。 do 至关于在语句中间临时subscribe一下,咱们在此时发送了Action。

再有咱们并不关心路由参数流的数据,咱们只是关心它何时有数据,因此咱们在 flatMap 语句中把参数写成了 _

到这里,咱们的内存版redux化的Angular2 Todo应用就搞定了。

时光旅行调试器 -- Redux TimeMachine Debugge

在介绍HTTP后台版本以前,咱们要隆重推出大名鼎鼎的Redux时光旅行调试器。首先须要下载Redux DevTools for Chrome,在Chrome商店中搜索 Redux DevTools便可。

image_1b4oekl1o18829t616cv1jd7u3jm.png-232.7kB

安装好插件以后,咱们须要在为 @ngrx/store 安装一个dev-tools的npm包: npm install @ngrx/store-devtools --save

而后在AppModule或CoreModule的Module元数据中加上 StoreDevtoolsModule.instrumentOnlyWithExtension()

...
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@NgModule({
  imports:[
    ...
    StoreModule.provideStore({ 
      todos: todoReducer, 
      todoFilter: todoFilterReducer
    }),
    StoreDevtoolsModule.instrumentOnlyWithExtension()
  ],
  ...
})复制代码

这样就配置好了,让咱们先看看它长什么样吧,打开浏览器进入todo应用。对了,别忘打开chrome的开发者工具,你应该能够看到Redux那个Tab,切换过去就好。

右侧的就是Redux DevTools

为何叫它时光旅行调试器呢?由于传统的Debugger只能单向的往前走,不能回退。还记得咱们有多少时间浪费在不断从新调试,一步步跟踪,不断添加watch的变量吗?这一切在Redux中都不存在,咱们能够时光穿梭到任何一个已发生的步骤。并且咱们能够选择看看若是没有某个步骤会是什么样子。

咱们来试验一下,对于显示的某个todo作切换完成状态,而后咱们会发现右侧的Inspector随即出现了TOGGLE_TODO的Action。你若是点一下这个Action,会发现出现了一个Skip按钮,点一下这个按钮吧,刚才那个Item的状态又恢复成以前的样子了。其实点任何一个步骤都没问题。

点击某个Action能够体验时光旅行

并且能够随时试验手动编辑一个Action,发射出去会是什么样子。还有不少其余功能,你们本身试验摸索吧。

在调试器中能够随时创建一个Action并发射出去

带HTTP后台版本

在前面铺垫的基础上,作这个版本很容易了。咱们用json-server能够快速创建一套REST的Web API。json-server只须要咱们提供一个json数据样本就能够完成Web API了,咱们的样本json是这样的:

{
  "todos": [
    {
      "id": "6e628423-be05-204f-f075-527cc1bb10d8",
      "desc": "have lunch",
      "completed": false
    },
    {
      "id": "40ab7081-cab9-5900-4048-f4ea905afd2f",
      "desc": "take a break",
      "completed": false
    },
    {
      "id": "6ae06293-23d4-c0ca-ee5b-880365dbd48b",
      "desc": "having fun",
      "completed": false
    },
    {
      "id": "e54f5e86-a781-acd5-1d16-8b878c7cba5d",
      "desc": "have a test",
      "completed": true
    }
  ]
}复制代码

而后把这个数据文件起个名,好比叫 data.json 放在 src/app 下,使用 json-server ./src/app/data.json 启动api服务。

如今咱们再来梳理一下若是使用后台版本的逻辑,咱们的如今的数据源实际上是来自于服务器API的,每次更改Todo后也都要提交到服务器。这个联动关系比较强,也就是说必需要服务器返回成功数据后才能进行内存状态的改变。这种状况下咱们彷佛应该把某些dispatch的动做放到service中。拿addTodo举个例子,咱们post到服务器一个新增todo的请求后在发送了dispatch ADD_TODO的消息,这时内存状态就会根据这个进行状态的迁转。

import { Injectable, Inject } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { UUID } from 'angular2-uuid';

import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import { Todo } from '../domain/entities';

import {
  ADD_TODO,
  TOGGLE_TODO,
  REMOVE_TODO,
  TOGGLE_ALL,
  CLEAR_COMPLETED
} from '../actions/todo.action'

@Injectable()
export class TodoService {

  private api_url = 'http://localhost:3000/todos';
  private headers = new Headers({'Content-Type': 'application/json'});
  private userId: string;

  constructor(
    private http: Http, 
    @Inject('auth') private authService,
    private store$: Store<Todo[]>
    ) {
    this.authService.getAuth()
      .filter(auth => auth.user != null)
      .subscribe(auth => this.userId = auth.user.id);
  }

  // POST /todos
  addTodo(desc:string): void{
    let todoToAdd = {
      id: UUID.UUID(),
      desc: desc,
      completed: false
    };
    this.http
      .post(this.api_url, JSON.stringify(todoToAdd), {headers: this.headers})
      .map(res => res.json() as Todo)
      .subscribe(todo => {
        this.store$.dispatch({type: ADD_TODO, payload: todo});
      });
  }
  // PATCH /todos/:id 
  toggleTodo(todo: Todo): void {
    const url = `${this.api_url}/${todo.id}`;
    let updatedTodo = Object.assign({}, todo, {completed: !todo.completed});
    this.http
      .patch(url, JSON.stringify({completed: !todo.completed}), {headers: this.headers})
      .mapTo(updatedTodo)
      .subscribe(todo => {
        this.store$.dispatch({
          type: TOGGLE_TODO, 
          payload: updatedTodo
        });
      });
  }
  // DELETE /todos/:id
  removeTodo(todo: Todo): void {
    const url = `${this.api_url}/${todo.id}`;
    this.http
      .delete(url, {headers: this.headers})
      .mapTo(Object.assign({}, todo))
      .subscribe(todo => {
        this.store$.dispatch({
          type: REMOVE_TODO,
          payload: todo
        });
      });
  }
  // GET /todos
  getTodos(): Observable<Todo[]> {
    return this.http.get(`${this.api_url}?userId=${this.userId}`)
      .map(res => res.json() as Todo[]);
  }

  toggleAll(): void{
    this.getTodos()
      .flatMap(todos => Observable.from(todos))
      .flatMap(todo=> { 
        const url = `${this.api_url}/${todo.id}`;
        let updatedTodo = Object.assign({}, todo, {completed: !todo.completed});
        return this.http
          .patch(url, JSON.stringify({completed: !todo.completed}), {headers: this.headers})
      })
      .subscribe(()=>{
        this.store$.dispatch({
          type: TOGGLE_ALL
        });
      })
  }

  clearCompleted(): void {
    this.getTodos()
      .flatMap(todos => Observable.from(todos.filter(t => t.completed)))
      .flatMap(todo=> {
        const url = `${this.api_url}/${todo.id}`;
        return this.http
          .delete(url, {headers: this.headers})
      })
      .subscribe(()=>{
        this.store$.dispatch({
          type: CLEAR_COMPLETED
        });
      });
  }
}复制代码

增删改这些操做应该都没有问题了,但此时存在一个新问题:内存状态如何能够经过服务器获得初始值呢?原来的内存版本中,咱们初始化就是一个空数组,但如今不同了,你可能会有上次已经建立好的todo须要在一开始显示出来。

如何改变那个初始值呢?但若是换个角度想,如今引入了服务器以后,咱们从服务器取数据彻底能够定义一个新的Action,好比叫 FETCH_FROM_API 吧。咱们如今只须要从服务器取得新数据后发送这个Action,应用状态就会根据取得的最新服务器数据刷新了。

import { Component, Inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { TodoService } from './todo.service';
import { Todo } from '../domain/entities';
import { UUID } from 'angular2-uuid';
import { Store } from '@ngrx/store';
import {
  FETCH_FROM_API
} from '../actions/todo.action'

import { Observable } from 'rxjs/Observable';

@Component({
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css']
})
export class TodoComponent {

  todos : Observable<Todo[]>;

  constructor(
    private service: TodoService,
    private route: ActivatedRoute,
    private store$: Store<Todo[]>) {
      const fetchData$ = this.service.getTodos()
        .flatMap(todos => {
          this.store$.dispatch({type: FETCH_FROM_API, payload: todos});
          return this.store$.select('todos')
        })
        .startWith([]);
      const filterData$ = this.route.params.pluck('filter')
        .do(value => {
          const filter = value as string;
          this.store$.dispatch({type: filter});
        })
        .flatMap(_ => this.store$.select('todoFilter'));
      this.todos = Observable.combineLatest(
        fetchData$,
        filterData$,
        (todos: Todo[], filter: any) => todos.filter(filter)
      )
    }

  addTodo(desc: string) {
    this.service.addTodo(desc);
  }
  toggleTodo(todo: Todo) {
    this.service.toggleTodo(todo);
  }
  removeTodo(todo: Todo) {
    this.service.removeTodo(todo);
  } 
  toggleAll(){
    this.service.toggleAll();
  }
  clearCompleted(){
    this.service.clearCompleted();
  }
}复制代码

如今服务器版本算是能够工做了,打开浏览器试一试吧。如今咱们的代码很是清晰:组件中不处理事务逻辑,只负责调用服务的方法。服务中只负责提交数据到服务器和发送动做。全部的应用状态都是经过Redux处理的。

服务器版本能够正常工做了

一点小思考

虽然服务器版本能够work了,但为何获取数据和fitler这段不能够放在服务中呢?为何要遗留这部分代码在组件中?这个问题很好,咱们一块儿来试验一下,实践是检验真理的惟一标准。

把组件构造函数中的代码移到Service的构造函数中,固然一样在Service中注入ActiveRoutes。

const fetchData$ = this.getTodos() 
  .do(todos => { 
    this.store$.dispatch({ 
     type: FETCH_FROM_API, 
     payload: todos 
    }) 
  }) 
  .flatMap(this.store$.select('todos')) 
  .startWith([]); 
const filterData$ = this.route.params.pluck('filter') 
  .do(value => { 
    const filter = value as string; 
    this.store$.dispatch({type: filter}); 
  }) 
  .flatMap(_ => this.store$.select('todoFilter')); 
this.todos = Observable.combineLatest( 
  fetchData$, 
  filterData$, 
  (todos: Todo[], filter: any) => todos.filter(filter) 
)复制代码

事实是残酷的,报错了

悲催的是,和咱们想象的彻底不同,报错了。这是因为Service默认状况下是单件形式(Singleton),而ActivatedRoutes并非,因此注入到service的routes并非后来激活的那个。固然也有解决办法,但那个就不是本章的目标。

咱们提出这个问题在于告诉你们@ngrx/store的灵活性,它既能够在Service中使用也能够在组件中使用,也能够混合使用,但都不会影响应用状态的独立性。在现实的编程环境中,咱们常常会遇到本身不可改变的事实,好比已有的代码实现方式、或者第三方类库等没法更改的状况,这时候@ngrx/store的灵活性就能够帮助咱们在项目中无需作大的更改的状况下进行更清晰的状态管理了。

Store便可以在Service中使用也能够在Component中使用

我实现的Todo实际上是多用户版本,比这个例子里有多了一些东西。你们能够去
github.com/wpcfan/awes… 查看代码

纸书出版了,比网上内容丰富充实了,欢迎你们订购!
京东连接:item.m.jd.com/product/120…

Angular从零到一

本文参与了掘金技术征文:gold.xitu.io/post/58522d…

相关文章
相关标签/搜索