ngrx的总结

官网地址: https://ngrx.io/docs

@ngrx/store

  • Single source of truth(单一状态对象)
    这个原则是整个单页应用的状态通过object tree(对象树)的形式存储在store 里面。这个定义十分抽象其实就是把所有需要共享的数据通过javascript 对象的形式存储下来
安装@ngrx/store
npm install @ngrx/store or yarn add @ngrx/store

配置

创建 state, action, reducer
import { Action } from '@ngrx/store';
export interface IAction extends Action {
    payload?: any; // dispatch数据载体,可有可无,继承Action的type属性
}

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';

const initialState = 0;

export function counterReducer(state: number = initialState, action: IAction) {
  switch (action.type) {
    case INCREMENT:
      return state + 1;

    case DECREMENT:
      return state - 1;

    case RESET:
      return 0;

    default:
      return state;
  }
}
注册store
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter';

@NgModule({
  imports: [
    StoreModule.forRoot({ count: counterReducer }), // 注册store
  ],
})
export class AppModule {}
使用store
在组件或服务中注入store进行使用

// 组件级别
import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { INCREMENT, DECREMENT, RESET } from './counter';

interface AppState {
  count: number;
}

@Component({
  selector: 'app-my-counter',
  template: `
    <button (click)="increment()">Increment</button>
    <div>Current Count: {{ count$ | async }}</div>
    <button (click)="decrement()">Decrement</button>

    <button (click)="reset()">Reset Counter</button>
  `,
})
export class MyCounterComponent {
  count$: Observable<number>;

  constructor(private store: Store<AppState>) { // 注入store
    this.count$ = store.pipe(select('count')); // 从app.module.ts中获取count状态流
  }

  increment() {
    this.store.dispatch({ type: INCREMENT });
  }

  decrement() {
    this.store.dispatch({ type: DECREMENT });
  }

  reset() {
    this.store.dispatch({ type: RESET });
  }
}

ngrx/effects

  • 目的:
    为了抽取组件中的多余动作,让组件更单纯,复用性更好
    方法:
    为了达到以上目的,我们要先分辨出,“多余”的动作是什么?—什么是多余的 什么就是effect
    说具体点儿,就是组件中 调取后台service部分的代码。
    因为如果我们将service写在组件中,就会将此代码的复用性降低了,比如你在其他项目还是想利用此组件,但是另一个组件的调用后台的名称或方法并不一致,导致了组件没办法直接复用。
    而有了effect的理念,我们将service方法直接抽离出来,根本不在组件中体现,这样组件就是一个很笨的组件,功能只涉及自己的业务,其他的都不管了。

  • 这种编程思路的根源是把所有的应用(或者组件)的逻辑想象成一个纯粹的对数据进行处理的函数(和外界的读写操作-- 这些读写操作就叫 Effects --都不属于这个函数的职责)以及一系列外部的读、写驱动构成。


function main(){
  // 逻辑部分
  var a = 2;
  var b = 10;
  var result = a * b;
  // 写入 console 的 Effect
  console.log('result is: ' + result);
  // 操作 DOM 的 Effect
  var resultElement = document.getElementById('result');
  resultElement.textContent = result;  
}

上面这段简单代码中前3行是代码的主要逻辑,接下来的几行代码都对外部世界产生了影响,所以他们都是 Effects ( Effect 这个词其实挺头疼,不知道中文那个词能比较形象的对应,“影响”感觉还是不到位)。那么我们接下来按照上面提到原则来改写这部分代码:逻辑部分不涉及任何对外部世界的影响。

    // 程序的主体逻辑完全剥离 Effects,只是对数据做处理
    function main(){
      var a = 2;
      var b = 10;
      var result = a * b;
      return {
        DOM: result,
        log: result
      }; 
     } 
     
    // 对于 Console 的影响写在这里
    function logEffect(result){
      console.log('result is: ' + result);
    }
     
    // 对于 DOM 的影响写在这里
    function domEffect(result){
      var resultElement = document.getElementById('result');
      resultElement.textContent = result;  
    }
     
    // 如何让数据和 effects 连接起来,这是一个粘合剂
    function run(mainFn){
      var sink = mainFn();
      logEffect(sink.log);
      domEffect(sink.DOM);
    }
     
    run(main);


状态、 Action 流 和 Effect

Redux 中的 Reducer 已经是一个纯函数,而且是完全的只对状态数据进行处理的纯函数。那么对于我们前面说的原则,Reducer 已经满足了。在发出某个 Action 之后,Reducer 会对状态数据进行处理然后返回。但一般来说,其实在执行 Action 后我们还是经常会可以称为 Effect 的动作,比如:进行 HTTP 请求,导航,写文件等等。而这些事情恰恰是 Redux 本身无法解决的,所以才有了诸如 Redux-Thunk 等中间件的产生。下面我们一起看看如何使用 @ngrx/effects 解决这个问题。

还是举一个小例子,比如登录注册这种经常用到的鉴权流程,我们一般有如下 Action :LOGIN、LOGIN_SUCCESS、LOGIN_FAIL、REGISTER、REGISTER_SUCCESS、REGISTER_FAIL 和 LOGOUT。

先拿 LOGIN 来说,我们希望流程是这个样子的:发出 LOGIN Action --> 使用登录 service 进行登录鉴权 --> 如果成功,发送 LOGIN_SUCCESS Action,如果失败,发送 LOGIN_FAIL Action。按原来的做法,我们至少需要在组件中的某个位置调用 service 进行 HTTP 请求,组件或者服务在 response 返回后决定发送 LOGIN_SUCCESS 或 LOGIN_FAIL 。

如果应用我们上面提到的 Effect 的概念,其实 Reducer 已经扮演了纯数据处理函数的角色,而 Action 在 @ngrx/effects 中是一个信号流,它扮演的是连接状态和要做的 Effect 中的粘合剂,就像上面代码中的 function run(mainFn) 一样。

    @Injectable()
    export class AuthEffects{
      // 通过构造注入需要的服务和 action 信号流
      constructor(private actions$: Actions, private authService: AuthService) { }
     
      //用 @Effect() 修饰器来标明这是一个 Effect
      @Effect() 
      login$: Observable<Action> = this.actions$ // action 信号流
        .ofType(authActions.ActionTypes.LOGIN) // 如果是 LOGIN Action
        .map(toPayload) // 转换成 action 的 payload 数据流
        .switchMap((val:{username:string, password: string}) => {
          // 调用服务
          return this.authService.login(val.username, val.password);
        })
        // 如果成功发出 LOGIN_SUCCESS Action 交给其它 Effect 或者 Reducer 去处理
        .map(user => new authActions.LoginSuccessAction({user: user})) 
        // 如果失败发出 LOGIN_FAIL Action 交给其它 Effect 或者 Reducer 去处理
        .catch(err => of(new authActions.LoginFailAction(err.json())));
     
    }
你可能会问,如果我们需要登录成功后导航到 /home 呢?导航也是effect,而 actions$ 是一个信号流,所以你完全可以定义一个 effect 监听 LOGIN_SUCCESS ,捕获到后就进行导航即可

  @Effect()
  navigateHome$: Observable<Action> = this.actions$
    .ofType(actions.ActionTypes.LOGIN_SUCCESS)
    .map(() => go(['/home']));

这样的话,其实组件都没有必要调用 Service 了,只需发出信号就好。

      onSubmit({value, valid}){
        if(!valid) return;
        this.store$.dispatch(
          new authActions.LoginAction({
            username: value.username, 
            password: value.password
          }));
      }

那更复杂一些怎么办?比如我们登录后需要取得该登录用户的待办事项列表,那我们照猫画虎但写到 return this.todoService.getTodos(auth.user.id); 发现还需要访问 auth 啊,怎么破?

  @Effect()
  loadTodos$: Observable<Action> = this.actions$
    .ofType(todoActions.ActionTypes.LOAD_TODOS)
    .map(toPayload)
    .switchMap(() => {
      return this.todoService.getTodos(auth.user.id); // 这个auth怎么得到啊?
    })
    .map(todos => new todoActions.LoadTodosSuccessAction(todos))
    .catch(err => of(new todoActions.LoadTodosFailAction(err.json())));


别忘了,ngrx 是基于 rxjs 的,非常善于合并和操作流,而 store 也是一个流,那就非常好办了,我们只需在 store 取得 auth 的最新值,然后合并这两个流就好了:

  @Effect()
  loadTodos$: Observable<Action> = this.actions$
    .ofType(todoActions.ActionTypes.LOAD_TODOS)
    .map(toPayload)
    .withLatestFrom(this.store$.select('auth'))
    .switchMap(([_, auth]) => {
      return this.todoService.getTodos(auth.user.id);
    })
    .map(todos => new todoActions.LoadTodosSuccessAction(todos))
    .catch(err => of(new todoActions.LoadTodosFailAction(err.json())));

这么做的理由
就是组件只是在dispatch action,而具体发生了什么,这个组件根本不关心。
这样做的好处
可以想象,你和队员合作,他可以独立去开发effect,你来写组件,你们的对接口就是action,具体实现已经分离。


RXJS

RxJS 是使用 Observables 的响应式编程的库,它使编写异步或基于回调的代码更容易
中文文档地址: https://cn.rx.js.org/

ramda

一款实用的 JavaScript 函数式编程库。

Ramda 主要特性如下:

Ramda 强调更加纯粹的函数式风格。数据不变性和函数无副作用是其核心设计理念。这可以帮助你使用简洁、优雅的代码来完成工作。

Ramda 函数本身都是自动柯里化的。这可以让你在只提供部分参数的情况下,轻松地在已有函数的基础上创建新函数。

Ramda 函数参数的排列顺序更便于柯里化。要操作的数据通常在最后面。

最后两点一起,使得将多个函数构建为简单的函数序列变得非常容易,每个函数对数据进行变换并将结果传递给下一个函数。Ramda 的设计能很好地支持这种风格的编程

ramda文档地址: http://ramda.cn/docs/#compose

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容