编程即逻辑 -- 聊聊 Effects 及在 Angular 中的应用

大概去年9月左右,看过 Cycle.js 创作者 André Staltz 的一个视频:讲为什么 React 并不是一个响应式的框架,同时介绍了 Cycle.js。当时就觉得这个思路好牛叉,但一直有点似懂非懂。最近由于工作需要,在 Angular 中使用了 @ngrx/effects (这个是借鉴了 Cycle.js 的思路,把这种思路应用在 Angular 中),对这个模式有了些粗浅认识,这里和大家分享。本文需要您了解 rxjs@ngrx/store (Redux 在 Angular 中的实现)。这些前置知识可以从本人以前写过一些文章获得: Angular 从0到1:Rx -- 隐藏在 Angular 中的利剑Redux你的 Angular 应用

背景知识和术语

这种编程思路的根源是把所有的应用(或者组件)的逻辑想象成一个纯粹的对数据进行处理的函数(和外界的读写操作-- 这些读写操作就叫 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);

大概的意思就这样了,看起来也没啥啊,你可能会想。别着急,它的威力我们到后面就知道了。那么这种思维方式和 Angular 有什么关系呢?

状态、 Action 流 和 Effect

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

还是举一个小例子,比如登录注册这种经常用到的鉴权流程,我们一般有如下 Action :LOGINLOGIN_SUCCESSLOGIN_FAILREGISTERREGISTER_SUCCESSREGISTER_FAILLOGOUT

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

}

你可能会问,如果我们需要登录成功后导航到 /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())));
    });

我感觉这种思路下的编程真正实现了:如果你逻辑想清楚了,你的代码也就基本写完了。

有问题的童鞋可以加入我的小密圈讨论: http://t.xiaomiquan.com/jayRnaQ (该链接7天内(5月14日前)有效)

另外,我的 《Angular 从零到一》出版了,下面是书籍的内容简介:

本书系统介绍Angular的基础知识与开发技巧,可帮助前端开发者快速入门。共有9章,第1章介绍Angular的基本概念,第2~7章从零开始搭建一个待办事项应用,然后逐步增加功能,如增加登录验证、将应用模块化、多用户版本的实现、使用第三方样式库、动态效果制作等。第8章介绍响应式编程的概念和Rx在Angular中的应用。第9章介绍在React中非常流行的Redux状态管理机制,这种机制的引入可以让代码和逻辑隔离得更好,在团队工作中强烈建议采用这种方案。本书不仅讲解Angular的基本概念和最佳实践,而且分享了作者解决问题的过程和逻辑,讲解细腻,风趣幽默,适合有面向对象编程基础的读者阅读。

欢迎大家围观、订购、提出宝贵意见。

慕课网 Angular 视频课上线: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner

京东链接:https://item.m.jd.com/product/12059091.html?from=singlemessage&isappinstalled=0

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

推荐阅读更多精彩内容