干货 | React技术栈耕耘 —— Redux

前端之家收集整理的这篇文章主要介绍了干货 | React技术栈耕耘 —— Redux前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

作者:小boy (沪江web前端开发工程师)
本文为原创文章,有不当之处欢迎指出。转载请注明出处。

Redux 是近年来提出的 Flux 思想的一种实践方案,在它之前也有 reflux 、 fluxxor 等高质量的作品,但短短几个月就在 GitHub 上获近万 star 的成绩让这个后起之秀逐渐成为 Flux 的主流实践方案。

正如 Redux 官方所称,React 禁止在视图层直接操作 DOM 和异步行为 ( removing both asynchrony and direct DOM manipulation ),来拆开异步和变化这一对冤家。但它依然把状态的管理交到了我们手中。Redux 就是我们的状态管理小管家。

安利的话先暂时说到这,本期的技术周刊将为你带来 React-Redux 在沪江前端团队中的实践。

0. 放弃

你没有看错,在开始之前我们首先谈论一下什么情况下不应该用 Redux。
所谓杀鸡焉用宰牛刀,任何技术方案都有其适用场景。作为一个思想的实践方案,Redux 必然会为实现思想立规矩、铺基础,放在复杂的 React 应用里,它会是“金科玉律”,而放在结构不算复杂的应用中,它只会是“繁文缛节”。

如果我们将要构建的应用无需多层组件嵌套,状态变化简单,数据单一,那么就应放弃 Redux ,选用单纯的 React 库 或其他 MV* 库。毕竟,没有人愿意雇佣一个收费比自己收入还高的财务顾问。

1. 思路

首先,我们回顾一下 Redux 的基本思路

用户与界面交互时,交互事件的回调函数会触发 ActionCreators ,它是一个函数,返回一个对象,该对象携带了用户的动作类型和修改 Model 必需的数据,这个对象也被我们称作 Action 。

以 TodoList 为例,添加一个 Todo 项的 ActionCreator 函数如下所示:

在上例中,addTodo 就是 ActionCreator 函数,该函数返回的对象就是 Action 。

其中 type 为 Redux 中约定的必填属性,它的作用稍后我们会讲到。而 text 则是执行 “添加 Todo 项“ 这个动作必需的数据。

当然,不同动作所需要的数据也不尽相同,如 “删除Todo” 动作,我们就需要知道 todo 项的 id,“拉取已有的Todo项” 动作,我们就需要传入一个元素为 Todo 项对象的数组( todos )。形如 text 、 id 、 todos 这类属性,我们习惯称呼其为 “ payload ” 。

现在,我们得到了一个 “栩栩如生” 的动作。它足够简洁,但担任 Model 的 store 暂时还不知道如何感知这个动作从而改变数据结构。

为了处理这个关键问题,Reducer 巧然登场。它仍然是一个函数,而且是没有副作用的纯函数。它只接收两个参数:state 和 action ,返回一个 newState 。

没错,state 就是你在 React 中熟知的 state,但根据 Redux 三原则 之一的 “单一数据源” 原则,Reducer 幽幽地说:“你的 state 被我承包了。”

于是,单一数据源规则实施起来,是规定用 React 的顶层容器组件( Container Components )的 state 来存储单一对象树,同时交给 Redux store 来管理。

这里区分一下 state 和 Redux store:state 是真正储存数据的对象树,而 Redux store 是协调 Reducer、state、Action 三者的调度中心。

而如此前所说,Reducer 此时手握两个关键信息:旧的数据结构(state),还有改变它所需要的信息 (action),然后聪明的 Reducer 算盘一敲,就能给出一个新的 state ,从而更新数据,响应用户。下面依然拿 TodoList 举例:

当接收到一个 action 时,Reducer 从 action.type 识别出该动作是要添加 Todo 项,然后路由到相应的处理方案,接着根据 action.text 完成了处理,返回一个 newState 。过程之间,整个应用的 state 就从 state => newState 完成了状态的变更。

这个过程让我们很自然地联想到去银行存取钱的经历,显然我们应该告诉柜台操作员要存取钱,而不是遥望着银行的金库自言自语。

Reducer 为我们梳理了所有变更 state 的方式,那么 Redux store 从无到有,从有到变都应该与 Reducer 强关联。

因此,Redux 提供了 createStore 函数,他的第一个参数就是 Reducer ,用以描绘 state 的更改方式。第二个是可选参数 initialState ,此前我们知道,这个 initialState 参数也可以传给 Reducer 函数。放在这里做可选参数的原因是为同构应用提供便捷。

createStore 函数最终返回一个对象,也就是我们所说的 store 对象。主要提供三个方法 getStatedispatch subscribe。 其中 getState() 获得 state 对象树。dispatch(actionCreator) 用以执行 actionCreators,建起从 action 到 store 的桥梁。

仅仅完成状态的变更可不算完,我们还得让视图层跟上 store 的变化,于是 Redux 还为 store 设计了 subscribe 方法。顾名思义,当 store 更新时,store.subscribe() 的回调函数会更新视图层,以达到 “订阅” 的效果

在 React 中,有 react-redux 这样的桥接库为 Redux 的融入铺平道路。所以,我们只需为顶层容器组件外包一层 Provider 组件、再配合 connect 函数处理从 store 变更到 view 渲染的相关过程。

而顶层容器组件往下的子组件只需凭借 props 就能一层层地拿到 store 数据结构的数据了。就像这样:

至此,我们走了一圈完整的数据流。然而,在实际项目中,我们面临的需求更为复杂,与此同时,redux 和 react 又是具有强大扩展性的库,接下来我们将结合以上的主体思路,谈谈我们在实际开发中会遇到的一些细节问题。

2. 细节

应用目录

清晰的思路须辅以分工明确的文件模块,才能让我们的应用达到更佳的实践效果,同时,统一的结构也便于脚手架生成模板,提高开发效率。

以下的目录结构为团队伙伴多次探讨和改进而来(限于篇幅,这里只关注 React 应用的目录。):

入口文件 app.js 与顶层组件 react/container.js

这块我们基本上保持和之前思路上的一致,用 react-redux 桥接库提供的 Provider 与函数 connect 完成 Redux store 到 React state 的转变。

细心的你会在 Provider 的源码中发现,它最终返回的还是子组件(本例中就是顶层容器组件 “Container“ )。星星还是那个星星,Container 还是那个 Container,只是多了一个 Redux store 对象。

而 Contaier 作为 业务组件 Wrapper 的 高阶组件 ,负责把 Provider 赋予它的 store 通过 store.getState() 获取数据,转而赋值给 state 。然后又根据我们定义的 mapStateToProps 函数按一定的结构将 state 对接到 props 上。 mapStateToProps 函数我们稍后详说。如下所见,这一步主要是 connect 函数干的活儿。

业务组件 component/Wrapper.js 与 mapStateToProps

这两个模块是整个应用很重要的业务模块。作为一个复杂应用,将 state 上的数据和 actionCreator 合理地分发到各个业务组件中,同时要易于维护,是开发的关键。

首先,我们设计 mapStateToProps 函数。需要谨记一点: 拿到的参数是 connect 函数交给我们的根 state,返回的对象是最终 this.props 的结构。

和 Redux 官方示例不同的是,我们为了可读性,将分发 action 的函数也囊括进这个结构中。这也是得益于 bindActions 模块,稍后我们会讲到。

这样,我们这个函数就准备好履行它分发数据和组件行为的职责了。那么,它又该如何 “服役” 呢?

敏锐的你一定察觉到刚才我们设计的结构中,以 “ params ” 开头的属性既没起到给组件展示数据的作用,又没有为组件发送 action 的功能。它们便是我们分发以上两种功能属性的关键。

我们先来看看业务组件 Wrapper :

现在,param 属性们为我们展示了它扮演的角色:在组件中实际分发数据和方法的快递小哥。这样,即使项目越变越大,组件嵌套越来越多,我们也能在 param.js 模块中,清晰地看到我们的组件结构。需求更改的时候,我们也能快速地定位和修改,而不用对着堆积如山的组件模块梳理父子关系。

相信你应该能猜到剩下的子组件们怎么取到数据了,这里限于篇幅就不贴出它们的代码了。

Action 模块: react/action.js、react/actionType.js 和 react/bindActions.js

在前面的介绍中,我们提到:一个 ActionCreator 长这样:

而在 Redux 中,真正让其分发一个 action ,并让 store 响应该 action,依靠的是 dispatch 方法,即:

交互动作一多,就会变成:

而容易想到:抽象出一个公用函数来分发 action (这里粗略写一下我的思路,简化方式并不唯一)

而细心的 Redux 已经为我们提供了这个方法 —— bindActionCreator
所以,我们的 bindActions.js 模块就借用了 bindActionCreator 来简化 action 的分发:

不难想象,action 模块里就是一个个 actionCreator :

为了更好地合作,我们单独为 action 的 type 划分了一个模块

react/reducers/ 和 react/store.js

前面我们说到,reducer 的作用就是区别 action type 然后更新 state ,这里不再赘述。可上手实际项目的时候,你会发现 action 类型和对应处理方式多起来会让单个 reducer 迅速庞大。

为此,我们就得想方设法将其按业务逻辑拆分,以免难以维护。但是如何把拆分后的 Reducer 组合起来呢 Redux 再次为我们提供便捷 —— combineReducers 。

只有单一 Reducer 时,想必代码结构你也了然:

我们最终得到的 state 结构是:

  • state

    • demoAPP

当有多个 reducer 时:

我们最终得到的 state 结构是:

  • state

    • demoAPP

    • reducerB

想必你已经想到更进一步,把这些 Reducer 拆分到相应的文件模块下:

接着,我们来看 store 模块:

怎么和想象的不一样?不应该是这样吗:

这里引入 redux 中间件的概念,你只需知道 redux 中间件的作用就是 在 action 发出以后,给我们一个再加工 action 的机会 就可以了。

为什么要引入 redux-thunk 这个中间件呢?

要知道,我们此前所讨论的都是同步过程。实际项目中,只要遇到请求接口的场景(当然不只有这种场景)就要去处理异步过程。

前面我们知道,dispatch 一个 ActionCreator 会立即返回一个 action 对象,用以更新数据,而中间件赋予我们再处理 action 的机会。

试想一下,如果我们在这个过程中,发现 ActionCreator 返回的并不是一个 action 对象,而是一个函数,然后通过这个函数请求接口,响应就绪后,我们再 dispatch 一个 ActionCreator ,这次我们真的返回一个 action ,然后携带接口返回的数据去更新 state 。 这样一来不就解决了我们的问题吗?

当然,这只是基本思路,关于 redux 的中间件设计,又是一个有趣的话题,有兴趣我们可以再开一篇专门讨论,这里点到为止。

回到我们的话题,经过

这样包装一遍 store 后,我们就可以愉快地使用异步 action 了:

这里我们用 promise 方式来处理请求,model.js 模块如你所想是一些接口请求 promise,就像这样:

你也可以参阅我们往期介绍的其他方式。

最后,我们再来完善一下之前的流程:

3.结语

Redux 的 API 一只手都能数得完,源码更是精炼,加起来不超过500行。但它给我们带来的,不啻是一套复杂应用解决方案,更是 Flux 思想的精简表达。此外,你还可以从中体会到函数式编程的乐趣。

一千个观众心中有一千个哈姆莱特,你脑海里的又是哪一个呢?

参考

《Redux 官方文档》
《深入 React 技术栈》


iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。

原文链接:https://www.f2er.com/react/302995.html

猜你在找的React相关文章