翻译| 10 Tips for Better Redux Architecture

Redux对于React程序是可有可无的吗?当你认识到Redux在编程时给你那种可以掌控一切状态能力的时候,你会觉得如果没有这种思考方法,大家到底该怎么来实现其中的逻辑呢?React中对于组件的控制引入了两个东西一个是props,一个是state.如果站在单个组件的角度上,组件本身其实也是一个复杂的复合体,给大家只开放两个能决定组件可以做什么,可以怎么表现的接口,就是这两个东西. 哲学说(呵呵,不是我说的,后面文章里的东西):一切事物皆由外因.外因的来源其实也有很多种组织方法.我曾经踢过很长时间的足球,只是踢踢野球罢了,踢野球可是没有任何章法可言,对阵双方可能是杂合着各种踢法和战术,或者是没有战术,好像也是在踢足球,但是大家都是凭着爱好和从观看比赛时积累的一点不成熟的想法.但是和有专门战术训练,有固定的战术打法的职业队一比没有任何的战斗能力.即便大家看不上的中国足球,随便拿一只大学校队级别的球队和野球队比赛,野球队都差的十万八千里.这里不谈个人的技术素养.在场地上的球员都是按照既定的方案在运行,也即是其实受到教练和战术训练的控制的.这一点非常的高效啊.我都不知道我怎么把React/Redux的思想和足球的思想联系了起来.但是好像能说明一点问题. 还有就是要使用新思想,脑袋要给新思想完全的空间. 中国足球队现在请了世界顶级的教练,成绩依然不是太好,既然请了人家的教练,就完全的配合,不要用什么中国国情特殊来对抗教练的思想.这对于Redux在React中的实施也是很重要的.要用Redux就要完全接受这套思想,不要老想着这怎么和原来的不一样啊!不能这么搞! 重要的还是思想. 脸书在实现React已经给state的管理提供了一个很好的选型构架,Redux只不过这比较好的实现了这个构架.别犹豫了,要使用React,彻底的投入Redux的怀抱吧!**再啰嗦总结一下:Redux通过props完全掌控了React组件的一举一动,大家可以在组件外观察组件能做什么,会发生什么变化 **

下面的这篇文章在meidum中收到了1000过个赞,的确是值得推荐.其实根本的问题Redux文档中的三个原则已经总结的太好了.但是思想的转变真的不是一朝一夕的功夫!需要花更多的时间给你的大脑回路形成的时间.一旦你的大脑再看到Redux的时候,有一个神经元激活了,那就基本成功了(?,题外话,喜欢美剧老友记的可以搜搜镜像神经元和珍妮弗.安妮斯顿.非常有意思的研究).

原文请见

以下为译文内容


当我开始使用React的时候,Redux还没出生呢?仅仅只有Flux构架以及一堆实现这个构架的方案.

现在在React的数据管理方面有两个明显的胜利者:Redux和MobX,MobX甚至都没有使用Flux架构.Redux之所以这么吸引眼球,原因他不在仅仅是为React服务了.你可以发现Redux在其他的框架上也已经有实施方案.包括Angular2,例如ngrx:store.

note1 MobX非常酷,在简单的UI中我可能会使用它而不是Redux,MobX更简单,也不太啰嗦(译注:的确是,你想要既简单又高效的东西那不可能.这种事情是不会发生的).这也就是说Redux有一些特性是MobX不能给你的.在项目中是否使用Redux之前理解这些Redux的独有特性是非常重要的.
note2 Relay和falcor是另外两个状态管理的解决方案.但是他们分别要由GraphQL和Falcor server来提供后台支撑.Relay的state都和服务器端的持久化数据对应着.AFAIK(目前我知道的是),两者都不能提供客户端暂时的
state管理方案.你可能会很喜欢结合Relay或者Falcor和Redu或MobX,结合使用其中在服务端和客户端state管理的能力.底线:在客户端的state管理上没有明显的胜利者.使用手头可以用的最好工具.

Redux的创建者Dan Abramov有一系列关于这个主题的课程:

  • Getting Started with Redux
  • Build Applications with Idiomatic Redux

两者都是循序渐进的先容Redux的基础.但是你会需要对Redux的理解提升到更高的水平.

下面的这些小Tips帮助你构建更好的Redux app

1. Understanding the Benefits of Redux

Redux有两个至关重要的目标,你要牢记于心:

  1. View 渲染的确定性(Deterministic)
  2. State变化的确定性(Deterministic)

确定性对于应用的测试,诊断和修复bugs来说都是非常重要的.如果你的应用视图和状态是不确定的(nondeterministic)的,根本就不可能知道视图和状态是不是有效. 或许nonndeterministic本身就是一个bug.

但是有些事情内在就是nodedeterministic的,例如用户输入和网络的I/O操作.大家怎么知道代码是否正常工作呢? 简单:隔离.(译注:为什么说好测试呢?举个例子:
你要是血糖高,流动在血管里的血是没有办法测糖含量的,大家把它抽出来,才能用试纸来测量.这就是所谓的隔离或者分离啊)

Redux的主要目的就是要从I/O端的异步操作例如视图的渲染和网络的工作中把state management隔离出来.当异步操作隔离出来以后,代码就变得非常的简单,马上和测试业务代码也非常的简单了,因为这些代码不再和网络请求以及DOM操作纠缠在一起.

当你的视图渲染从网络I/O和状态更新隔离开来以后,你就会得到一个外因决定的 视图渲染方法,意思是:只要给定相同的state,视图一定会渲染出相同的结果.这么做消除了诸如异步操作中竞争条件对于视图的影响以及视图在渲染过程中对state的不完整的截取问题.

当一个新手在思考创建一个视图的时候,他可能会想:这里需要一个用户模型,所以我启动一个异步请求,当promises对象的状态变为resloves的时候,我就使用用户的名字来更新用户组件.这么做比todo中的 items需要的任务多一写,大家使用fetch模块来完成他,当promise对象resolves的时候,大家可以遍历他们,添加到屏幕上.

使用这种方法有几个主要的问题:

  1. 在任何时间点,你都没有所有需要渲染视图的数据.直到组件开始加载之前,你实际都不能开始请求数据.
  2. 不同的远程请求任务会在不同时间点到来,这会对视图渲染的队列有些微妙的变化.为了明白渲染队列,你需要一些你不可能预料的常识:每一个异步请求的持续时间.小测试:在上面的场景里,那个视图最先渲染?用户组件还是todo-items?答案是:这是竞争关系.
  3. 有时候事件监听器也会更新视图的状态,可能还会触发另一个渲染,更复杂的队列.

关键的问题是:在视图的状态里存储数据,在视图中添加事件监听器导致了视图状态的突变:

Nondeterminism=并发的处理过程+共享的状态.-Martin Odersky(Scalas设计者)

数据远程获取,数据操作,视图渲染混合在一起构成了时间旅行式的意大利面条代码

我知道这里所说的好像是B级科幻影片的套路,但是请相信我,时间旅行式的意大利面是最坏的菜谱!

flux构架强调严格的隔离和序列,每一次处理都会遵守这些规则.

  1. 首先,大家会知道,固定的state...
  2. 当大家在渲染视图的时候,没有任何事情可以改变state.
  3. 给定同样的state,渲染的视图总是相同.
  4. 事件监听器监听用户的输入和网络请求句柄.当这些请求有了结果以后,actions会被发送到store.
  5. 当一个action被派发,state被更新到一个新的已知的state,队列重复.可以改变state的只有派发actions.

Flux构架是一个果壳,单向的数据流动构架.

Flux Arctiture
Flux Arctiture

在Flux构架中,视图监听用户的输入,把这些输入翻译为action对象,action对象可以被dispatch到Store.Store更新应用的state,并且告知视图再次渲染.当然了,视图也可以只依赖输入和事件,这也没有问题.另外,事件监听器可以像下面这样派发action对象:

重要的是,Flux中的state更新是事务性的.代替在state上简单的调用更新方法,或者支架操作一个值,action会被派发到store.一个Action对象会被事务性的记录下来.你可以认为这有点像银行的事务操作-变化记录的生成.
当你在银行存一点钱,你账户五分钟前的信息不回被删除.新的账户信息会被添加到交易的历史记录中. Action对象在你的应用state中添加一个事务历史记录.

Action对象是这个样子的:

{
 type: ADD_TODO,
 payload: 'Learn Redux'
}

action对象给你的能力是保持所有的state变化的日志资料.这个日志可以根据外部条件重复生成,意思是:

给定相同的初始化state和相同的事务处理,在相同的操作中,你可以获得相同的state.

这一点的潜意义是:

  1. 容易测试
  2. undo/redo很容易实施
  3. 时间旅行deguging
  4. 持久性-即使state被擦写掉了,如果你有每个事务的处理记录,可以很容易的重复他.

试问,谁不想掌控时空的变化?事务性的状态给你了时间旅行的超能力.

Redux devtoos with slide review
Redux devtoos with slide review

2. 某些Apps不需要Redux

如果你的UI的工作流很简单,Redux所做的就有点大材小用了.如果你在做一个tic-tac-toe的游戏,你真的需要undo/redo?这些小游戏很少能玩超过一分钟.如果玩家失败了,你需要做的只是重置游戏,再次开始就行了.

如果:

  • 用户流程很简单
  • 用户之间没有交互
  • 你不需要管理服务端事件(SSE)或者websockets
  • 每个视图从单一数据源获取数据.

这可能是因为你的app 事件流程太简单,不值得在state的事务上花费额外的精力.
或许你不需要在app中使用Fluxify化.还有一个简单的解决方案.看看MobX (译注:我很庆幸没有在Redux学习遇到难点的时候,退缩到安全地带,有的朋友遇到Redux学不下去的时候就退到了MobX了,在我看来Redux是职业足球队的踢法,MobX是业余球队踢法).

然而,随着你的app变得复杂,视图的复杂性和状态管理性都增加了,事务性状态价值增加,MobX不能提供状态的事务性管理方法.

如果:

  • 用户的流程很复杂
  • 你的app有很多的用户工作流
  • 用户有很多交互联系
  • 正在使用web sockets或者SSE
  • 从不同的数据来源获取数据构建单一的视图(译注:这个能力是React比MVC框架更高级的地方,页面中视图中的不同组件可以各自获取自己的数据,互相不受干扰)

如果能从事务模型中获益.Redux对你就非常的合适.

那么关于web sockets和SSE? 当你添加更多的异步I/O来源时,理解app内部的状态管理变得非常的困难.受外部控制的state和state的事务性处理能简化理解过程(译注:redux-logger打印出state的结构的时候,我如释重负).

我的观点是:大多数的SaaS产品包含了最新的复杂UI工作流,应该使用事务性state管理.小型的工具app或者简单原型的app不应该用.用对工具是非常重要的.

3.理解Reducers

Redux=Flux+Functional Programming

Flux使用action对象描述了单向的数据流和事务状管理,但是对于怎么操作action对象,什么也没有讲.这就是Redux的切入点.

构建Redux state管理的首要模块就是reducer函数.那么,什么是reducer 函数?

在函数式编程中,普通的工具reducer()或者fold()用来对values列表中的每一个value实行reducer函数,累加单个输出的value.这里有一个对于JavaScript Array.prototype.reduce()原型的总结.

//这是从codepen拿出来的代码,浏览器console可以看到结果
 const initialState=0;
 const reducer=(state= initialState,data)=>state+data;
 const total=[0,1,2,3].reduce(reducer);
 console.log(total);

在Redux中使用reducers,不是对数组进行操作,而是对系列的action对象应用reducer.记住,action对象是这个样子的:

 {
 type: ADD_TODO,
 payload: 'Learn Redux'
 }

让大家从reducer的总结转到Redux-style的reducer:

 const defaultState = 0;
const reducer = (state = defaultState, action) => {
 switch (action.type) {
   case 'ADD': return state + action.payload;
   default: return state;
 }
};

现在大家可以应用一下测试actions

 const actions = [
 { type: 'ADD', payload: 0 },
 { type: 'ADD', payload: 1 },
 { type: 'ADD', payload: 2 }
];

const total = actions.reduce(reducer, 0); // 3

4.Reducers必须是纯函数

为了获得受控state可重复性,reducers必须是纯函数.没有意外情况,一个纯函数:

  1. 给定一个相同的输入,总是返回相同的输出值
  2. 没有异步操作

在Javascript中非常重要的一点,传递进函数的所有的非原始对象都是传引用赋值(references).换句话说,如果你传递一个对象到函数,这个函数对对象的属性做出了改变, 函数外部的对象也跟着发生变化.这就是副作用(side effect).
如果不知道传递对象的整个历史,你不可能知道函数调用的完整意义.这一点很不利于开发.

Reducers应该返回一个新的对象.例如你可以这样做OBject.assign({},state,{thingToChange}).

数组的参数也是引用赋值的,不能再使用.push()方法把新的items添加到一个数组.因为.push()会突变一个操作,.pop(), .shift(), .unshift(), .reverse(), .splice() 这些方法都不行.
(译注:突变的意思是对源头都改变了,突变以后,源头是什么样子就没有办法看到了)

如果你想在使用数组的时候,安全一点,可以使用 concat()来代替.push(). (译注:在reducer中使用concat可以参见iReading app)

看看chat Reducer中的ADD_CHAT的例子:

 const ADD_CHAT = 'CHAT::ADD_CHAT';

const defaultState = {
 chatLog: [],
 currentChat: {
   id: 0,
   msg: '',
   user: 'Anonymous',
   timeStamp: 1472322852680
 }
};

const chatReducer = (state = defaultState, action = {}) => {
 const { type, payload } = action;
 switch (type) {
   case ADD_CHAT:
     return Object.assign({}, state, {
       chatLog: state.chatLog.concat(payload)
     });
   default: return state;
 }
};

如你所见,新的对象使用Object.assign()来创建,添加项目到数组用concat()代替.push()方法.

就我个人来说,我不喜欢去担心我的State的突变事故,所以之后我会和Redux一起使用immubable data APIs.如果我的state是immutable对象,根本就不用看代码,就知道对象是不会发生突变事故的.之所以得出这个结论是因为和一个小组工作之后,发现了事故性突变的bugs.
(译注:immutable.js的运行原理,或者state的原理,其实参考版本库的管理方法,每次的修改都会有一个唯一的标示来记录增删改查的内容)

还有很多的纯函数.如果你在app编程中使用Redux,你需要很好的掌握纯函数,其他事情你需要放在心上(例如:处理时间,日志和随机数).
了解更多内容请看Master the Javascript Interview:What is Pure Function.

5. 记住:Reducers一定是所有事实的唯一来源

在你的app中,所有的state应该只有唯一的真相来源,意思是说:state存储只存储在一个地方,其他任何需要state的地方,都需要获取state的引用赋值.

不同的事情有不同的来源也可以.例如,URL可以是用户请求路径和请求路径的唯一来源.或许你的app有一个配置服务,有API URLs的所有内容.这也可以,但是...
当你在Redux store中存储state时,接入到state,都需要通过Redux.不遵循这个原则可能会导致脏数据或者某种共享式的state 突变bug.

换句话,如果没有单一来源的原则,你可能会丢失:

  • 受控的视图渲染
  • 受控的state重现
  • 简单易行的undo/redo
  • 时间旅行degugging
  • 容易实施的测试

要么用Redux管理的store,要么不用.如果有的地方用,有的地方不用,可能就会抵消使用Redux的好处.

6.为Action Types使用常量

我习惯于确保当你查看action的历史时,很容易追踪到使用他们的reducer.如果你的actionsde名字比较短的普通名字例如CHANGE_MESSAGE,会变得很难理解他在app中做什么.如果你的action types有更多描述性的名字例如:CHAT::CHANGE_MESSAGE,显然很清楚要做什么.

如果你做了一个错误的输入,派发了一个没有定义的action 常量,app将会抛出一个异常警告你错误.如果会你输错了action的累心字符串,action将不会显示报错信息而失败.

把所有的action type收集在一个文件的顶端可以帮助你:

  • 保持名字的统一
  • 快速理解reducer的API
  • 理解请求中的变化

7.使用Action Creators从派发调用中解耦Action的逻辑

当我告诉其他人他们没有生成IDS或者在reducer中获取当前时间,我看到的是滑稽的表情.如果你现在盯着屏幕感到疑惑:你也不是唯一这么想的.

有没有一个好的地方来处理纯逻辑,不要在需要使用action的地方重复他们?有,请使用action creator.

Action creators有其他的好处:

  • 把action type常量封装在reducer文件中,不能在其他地方导入.
  • 在派发action之前对输入做一些计算
  • Reduce模板

让大家来使用一个action creator 生成一个ADD_CHATaction 对象:

 // Action creators can be impure.
export const addChat = ({
// cuid is safer than random uuids/v4 GUIDs
// see usecuid.org
id = cuid(),
msg = '',
user = 'Anonymous',
timeStamp = Date.now()
} = {}) => ({
type: ADD_CHAT,
payload: { id, msg, user, timeStamp }
});

如你所见,大家使用cuid为每个聊天信息生成随机的ids,使用Date.Now()生成时间戳.两者都是纯操作,在reducer中运行不太安全.但是在action creatros中运行时可以的.

使用Action Creators来减少模板代码使用

一些程序员认为是使用action creators添加模板到项目中.相反的,你将会看到我怎么用他们来大幅度在reducer中减少模板.

提示: 如果你把常量,reducer和action creators放到一个文件中,当你需要从不同的路径导入他们的时候,你就可以减少模板的需求.

想象着,大家需要给聊天用户添加定制他们的用户姓名和可用状态的能力.大家可能会像下面一样天界一列的action type:

 const chatReducer = (state = defaultState, action = {}) => {
 const { type, payload } = action;
 switch (type) {
   case ADD_CHAT:
     return Object.assign({}, state, {
       chatLog: state.chatLog.concat(payload)
     });
   case CHANGE_STATUS:
     return Object.assign({}, state, {
       statusMessage: payload
     });
   case CHANGE_USERNAME:
     return Object.assign({}, state, {
       userName: payload
     });
   default: return state;
 }
};

对于大多数的reducers,这可能会增加一些模板代码.很多我需要构建的reducer比这个更复杂,他们有一些冗余的代码.如果我要把这些简单的属性改变action融合到一起怎么样?

事实是,很容易:

  const chatReducer = (state = defaultState, action =    {}) => {
 const { type, payload } = action;
 switch (type) {
   case ADD_CHAT:
     return Object.assign({}, state, {
       chatLog: state.chatLog.concat(payload)
     });

    // Catch all simple changes
    case CHANGE_STATUS:
    case CHANGE_USERNAME:
     return Object.assign({}, state, payload);

   default: return state;
 }
};

即使使用额外的空格和注释,这个版本也是很短的,这仅仅是两个例子.action越多,代码减少的越多.

switch… case安全吗?我看到飞流直下的瀑布!(译注:琢磨了两天,才明白编辑是这个意思,原文-I see a fall through!)

你可能在其他地方读到过switch声明应该被避免,尤其是要避免偶然出现的瀑布一样的流程,因为cases的列表会变得很臃肿.可能你听说过了,不要刻意的使用瀑布式的代码,因为捕获瀑布流的bug非常难.这是个不错的建议,那就让大家仔细考虑一下上面提到的危险.

  • Reducers是可以组合的,所以case的臃肿不是问题,如果case的列表变的很大,打碎成片段转移到分离的reducers中.
  • 每一个case体都会返回一个对象,如此一来瀑布流就不会出现了.一个瀑布流不应该出现一个以上的异常捕获语句

Redux使用switch..case.只要你遵循简单原则(保持switches语句体积小,目标集中,在每个case都尽早的返回).swith语句是非常好的.

你可能注意到这个版本需要一个不同筹载(payload).这里就是你的action Creator的发源地

 export const changeStatus = (statusMessage = 'Online') => ({
 type: CHANGE_STATUS,
 payload: { statusMessage }
});

export const changeUserName = (userName = 'Anonymous') => ({
 type: CHANGE_USERNAME,
 payload: { userName }
});

如你所见,这些action creators 把参数和state的结构改变联系起来了,他是作为一个翻译的角色.

8.在文档中使用ES6的参数默认值

如果你正在编辑器中使用Tern.js插件,他将会读取这些ES6的默认值,在你的action creators中需要的时候引用他们,所以当你调用他们的时候,可以感知他们和实行自动完成.这会减少程序员的认知负担,因为他们不需要记住所有的载荷雷翔或者检查他们记不起来的源代码.

如果你没有使用类型应用插件例如:Tern,TypeScript或者Flow,是时候使用他们了.

//这一部分实在是不会翻译了,留下来
 Note: I prefer to rely on inference provided by default assignments visible in the function signature as opposed to type annotations, because:
You don’t have to use a Flow or TypeScript to make it work: Instead you use standard JavaScript.
If you are using TypeScript or Flow, annotations are redundant with default assignments, because both TypeScript and Flow infer the type from the default assignment.
I find it a lot more readable when there’s less syntax noise.
You get default settings, which means, even if you’re not stopping the CI build on type errors (you’d be surprised, lots of projects don’t), you’ll never have an accidental `undefined` parameter lurking in your code.

9. 使用Selectors来计算和解耦和State.

设想你正在构建历史上最复杂的聊天app.你已经写了500K的代码,然后产品团队抛出一个需要你必须改变state数据结构的新需求.

不要痛苦,你可以很灵巧的使用selectors来从整个State中解耦和app的其余部分.子弹:躲开

黑客帝国,LEO躲开子弹
黑客帝国,LEO躲开子弹

对于我写过的几乎每一个reducer,我都创建了一个selector简单的输出我需要在视图中构建的所需要的变量.让大家看看简单的chat reducer

   export const getViewState = state =>      Object.assign({}, state);

是的.我知道太简单了,不值得一看.你可能认为我现在疯了,但是记起了大家之前多个的子弹了吗?如果我想添加一下计算state,例如所有会话中我交谈过的用户的完整列表?让大家叫做recentlyActiveUsers.

这个信息已经存储在大家当前的state中-但是不太容易得到.让大家往前看,在getViewState()中获取他.

 export const getViewState = state => Object.assign({}, state, {
// return a list of users active during this session
recentlyActiveUsers: [...new Set(state.chatLog.map(chat => chat.user))]
});

如果你把所有的计算state都放在selector中,你:

  1. 减少了reducers和组件的复杂想
  2. 把你的app从state的结构中解耦出来.
  3. 遵守单一来源原则,甚至是在reducer中也是这样.

10.使用TDD:测试优先

很多研究比较了编写之前测试和编写之后测试的方法以及根本不测试的方法.结论是清除和显著的,在实施编写之前,测试可以减少40-80%的bug.

TDD can effectively cut your shipping bug density in half, and there’s plenty of evidence to back up that claim.

在写这个文章中的示例时,我都以单元测试开始.

为了避免碎片化测试,我创建了如下的工厂来生产expections:

 const createChat = ({
 id = 0,
 msg = '',
 user = 'Anonymous',
 timeStamp = 1472322852680
} = {}) => ({
 id, msg, user, timeStamp
});

const createState = ({
 userName = 'Anonymous',
 chatLog = [],
 statusMessage = 'Online',
 currentChat = createChat()
} = {}) => ({
 userName, chatLog, statusMessage, currentChat
});

** 注意这两个测试我都使用了默认值,意思是我可以越过属性,单独为我感兴趣的测试提供数据**

——

这里是我使用的:

 describe('chatReducer()', ({ test }) => {
 test('with no arguments', ({ same, end }) => {
   const msg = 'should return correct default state';

   const actual = reducer();
   const expected = createState();

   same(actual, expected, msg);
   end();
 });
});

Note: 我使用tape来进项单元测试,因为他够简单.我也有2-3年使用Mocha和Jasmine的经验,以及其他的框架的零散经验.你需要根据这些原则找到合适的测试框架
注意我在测试标书巢式测试时使用的风格.可能由于我使用过Jasmine和Mocha框架的背景,我喜欢由外部代码块开始描述需要测试的组件,接着才是内部的代码块.在测试代码块内部,我使用简单的相等断言,也就是你的测试框架中的deepEqual()或者toEqual()函数.

如你所见,我使用分离的测试声明和工厂函数来代替像beforeEachafterEach()这样的工具,这么工具诱导没有经验的开发者在测试组件中使用共享的state来完成测试(这个做法不太好).

可能你会猜到,我已经为每个reducer准备了三种不同的测试:

  1. 直接的reducer测试,你可以在例子中看到.这些方法简单的测试reducer能否产生预期的默认state.
  2. Action creator测试,通过使用预先设定好的sate作为起始点,对每一个action应用reducer来测试action的功能
  3. Selectors测试,测试每个selectors,确保每个预期的属性的预期值都存在,包括经过计算的属性.

你已经看到了一个reducer测试,让大家看看其他的例子

Action Creators Test:

 describe('addChat()', ({ test }) => {
 test('with no arguments', ({ same, end}) => {
   const msg = 'should add default chat message';

   const actual = pipe(
     () => reducer(undefined, addChat()),
     // make sure the id and timestamp are there,
     // but we don't care about the values
     state => {
       const chat = state.chatLog[0];
       chat.id = !!chat.id;
       chat.timeStamp = !!chat.timeStamp;
       return state;
     }
   )();

   const expected = Object.assign(createState(), {
     chatLog: [{
       id: true,
       user: 'Anonymous',
       msg: '',
       timeStamp: true
     }]
   });

   same(actual, expected, msg);
   end();
 });


 test('with all arguments', ({ same, end}) => {
   const msg = 'should add correct chat message';

   const actual = reducer(undefined, addChat({
     id: 1,
     user: '@JS_Cheerleader',
     msg: 'Yay!',
     timeStamp: 1472322852682
   }));
   const expected = Object.assign(createState(), {
     chatLog: [{
       id: 1,
       user: '@JS_Cheerleader',
       msg: 'Yay!',
       timeStamp: 1472322852682
     }]
   });

   same(actual, expected, msg);
   end();
 });
});

这个例子非常的有意思,原因有几个.addChat() action creator是不纯的.意思是除非你传递值代替原值,否则你就获得不了预期的属性值.为了对付这个问题,我使用了管道.我有时使用管道来避免创建了不需要的附加值.又是使用它来忽略生成的值.我仍然却行他们是存在的,但是我不关心这些值到底是什么.注意我甚至都没有检查type类型.我依靠类型引用和默认值来完成和这个任务.

一个管道(pipe)是一个工具函数,让你通过一系列的函数传递一些输入值,这些系列函数都接受之前函数的输出值,之后做出某种程度的变化.我使用了loadh/fp/pipe,别名是loadsh/flow.有意思的是pipe()也可以在reducer函数中创建.

 const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

const fn1 = s => s.toLowerCase();
const fn2 = s => s.split('').reverse().join('');
const fn3 = s => s + '!'

const newFunc = pipe(fn1, fn2, fn3);
const result = newFunc('Time'); // emit!

我更愿意在reducers文件中使用pipe()来简化state的变化.所有的state的变化最终都从一个数数据流都另个数据流.pipe()很擅长这个工作.

注意,action creator让大家忽略所有的默认值,所以大家可以传递特定的ids和时间戳,可以测试特殊的值.

Selectors测试

最后,大家来测试一下state selectors.确保经过计算的值是正确的.要做的是:

 describe('getViewState', ({ test }) => {
  test('with chats', ({ same, end }) => {
    const msg = 'should return the state needed to render';
    const chats = [
      createChat({
        id: 2,
        user: 'Bender',
        msg: 'Does Barry Manilow know you raid his wardrobe?',
        timeStamp: 451671300000
      }),
      createChat({
        id: 2,
        user: 'Andrew',
        msg: `Hey let's watch the mouth, huh?`,
        timeStamp: 451671480000 }),
      createChat({
        id: 1,
        user: 'Brian',
        msg: `We accept the fact that we had to sacrifice a whole Saturday in
              detention for whatever it was that we did wrong.`,
        timeStamp: 451692000000
      })
    ];

    const state = chats.map(addChat).reduce(reducer, reducer());

    const actual = getViewState(state);
    const expected = Object.assign(createState(), {
      chatLog: chats,
      recentlyActiveUsers: ['Bender', 'Andrew', 'Brian']
    });

    same(actual, expected, msg);
    end();
  });
});

注意,在这个测试中,大家使用了JS数组原型方法reducer()来reducer 累积一些actions addChat()的值.Redux reducer非常好的一个地方是,他们仅调控reducer函数,你可以使用reducer做任何其他reducer能做的事情.

大家的expected值检查了所有日志中的chat对象以及最近的活跃用户的列表是否正确.

没有什么要说的了.

Redux的军规

如果你正确的使用Redux,你会获得很多大好处:

  • 减少时间依赖的bugs
  • 确定性的视图渲染
  • 确定性的state重演
  • 容易实施的undo/redo特性
  • 简单的debug
  • 成为一个时间旅行者

但是为了保证上面的功能可以正常工作,你还要记住以下的规则:

  • Reducer必须是纯函数
  • Reducer必须是state的唯一来源
  • Reducer的state应该总是被序列化
  • Reducer state不能包含有函数

还要牢记于心:

  • 不是所有的app都需要Redux
  • 用常量定义action Types
  • 使用action creators来解耦 action逻辑和dispatch的调用
  • 使用ES6的默认参数方法来描述参数特征
  • 使用selectors来计算state和解耦
  • 一定要使用TDD(译注:马上回考虑的)

祝你愉快!

译文完


妈呀,5000多字,手指都敲掉皮了.没功劳也有苦劳啊,看到这里给个?吧.

Members of “Learn JavaScript with Eric Elliott”, check out the new functional programming and Redux lessons. Be sure to watch the Shotgun series & ride shotgun with me while I build real apps with React and Redux.
Not a member? Join Today!
Eric Elliott is the author of “Programming JavaScript Applications” (O’Reilly), and “Learn JavaScript with Eric Elliott”. He has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.
He spends most of his time in the San Francisco Bay Area with the most beautiful woman in the world.

推荐阅读更多精彩内容