React技术栈耕耘 —— Redux

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 函数如下所示(如果不熟悉 ES6 箭头函数请移步这里):

const addTodo = text => ({type: 'ADD_TODO',text
});

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

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

当然,不同动作所需要的数据也不尽相同,如 “删除Todo” 动作,我们就需要知道 todo 项的 id,“拉取已有的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
举例(不熟悉 “…” ES6 rest/spread 语法请先看这里):

//整个 todoList 最原始的数据结构。
const initState = {filter: 'ALL',todos: []
};
//Reducer 识别动作类型为 ADD_TODO 时的处理函数
const handleAddTodo = (state, text) => {const todos = state.todos;const newState = {...state, {todos: [...todos, {text,completed: false}]}};return newState;
};
//Reducer 函数
const todoList = (state = initState, action) => {switch (action.type) {case 'ADD_TODO':return handleAddTodo(state, action.text);default:return state;}
}

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

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

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

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

//store.js
import reducer from './reducer';
import { createStore } from 'redux';
export default createStore(reducer);

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

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

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

import store from './store';
import {connect, Provider} from 'react-redux';
import React from 'react';
import ReactDOM from 'react-dom';
import Page from '../components/page'; //业务组件
// 把 state 映射到 Container 组件的 props 上的函数
const mapStateToProps = state => { return {...state}
}
const Container = connect(mapStateToProps)(Page); //顶层容器组件
ReactDOM.render(<Provider store={store}><Container /></Provider>,document.getElementById("root")
);

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

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

2. 细节

应用目录

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

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

appPage
├── components
│   └── wrapper
│       ├── component-a
│       │   ├── images
│       │   ├── index.js
│       │   └── index.scss
│       ├── component-a-a
│       ├── component-a-b
│       ├── component-b
│       └── component-b-a
├── react
│   ├── reducer
│   │   ├── index.js
│   │   ├── reducerA.js
│   │   └── reducerB.js
│   ├── action.js
│   ├── actionTypes.js
│   ├── bindActions.js
│   ├── container.js
│   ├── model.js
│   ├── param.js
│   └── store.js
└── app.js

入口文件 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 函数干的活儿。

//入口文件:app.js
import store from './react/store';
import Container from './react/container';
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
ReactDOM.render(<Provider store={store}><Container /></Provider>,document.getElementById("root")
);
//顶层容器组件:react/container.js
import mapStateToProps from './param';
import {connect} from 'react-redux';
import Wrapper from '../components/wrapper';
export default connect(mapStateToProps)(Wrapper);

业务组件 component/Wrapper.js 与 mapStateToProps

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

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

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

//mapStateToProps:react/param.js
import bindActions from './bindActions';
const mapStateToProps = state => {let {demoAPP} = state; // demoAPP 也是 reducer 中的同名函数// 分发 action 的函数let {initDemoAPP, setDemoAPP} = bindActions;// 分发 state 上的数据let {isLoading, dataForA, dataForB} = demoAPP;let {dataForAA1, dataForAA2, dataForAB} = dataForA; // 返回的对象即为 Wrapper 组件的 this.propsreturn {initDemoAPP, // Wrapper 组件需要发送一个 action 初始化页面数据isLoading, //  Wrapper 组件需要 isLoading 用于展示paramsComponentA: {dataForA, // 组件 A 需要 dataForA 用于展示paramsComponentAA: {setDemoAPP, // 组件 AA 需要发送一个 action 修改数据dataForAA1,dataForAA2},paramsComponentAB: {dataForAB}},paramsComponentB: {dataForB,paramsComponentBA: {}}}
}
export default mapStateToProps;

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

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

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

//业务组件组件:components/wrapper.js
import React, { Component } from 'react';
import ComponentA from '../component-a';
import ComponentB from '../component-b';
export default class Example extends Component {constructor(props) {super(props);}componentDidMount() {this.props.initDemoAPP(); //拉取业务数据}render() {let {paramsComponentA, paramsComponentB, isLoading} = this.props;if (isLoading) {return (<span>App is loading ...</span>);}return (<div>{/* 为组件分发参数 */}<ComponentA {...paramsComponentA}/><ComponentB {...paramsComponentB}/></div>);}
}

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

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

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

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

const addTodo = text => ({type: 'ADD_TODO',text
});

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

store.dispatch(addTodo('new todo item'));

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

store.dispatch(addTodo('new todo item1'));
store.dispatch(deleteTodo(0));
store.dispatch(compeleteTodo(1));
store.dispatch(clearTodos());
//...

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

const {dispatch} = store;
const dispatcher = (actionCreators, dispatch) => {// ...校验参数let bounds = {};let keys = Object.keys(actionCreators);for (let key of keys) {bounds[key] = (...rest) => {dispatch(actionCreators[key].apply(null, rest));}}return bounds;
}
//简化后的使用方式
const disp = dispatcher({addTodo,deleteTodo,compeleteTodo//...
}, dispatch);
disp.addTodo('new todo item1');
disp.deleteTodo(0);
//...

而细心的 Redux 已经为我们提供了这个方法 —— bindActionCreator

所以,我们的 bindActions.js 模块就借用了 bindActionCreator 来简化 action 的分发:

// react/bindActions.js
import store from './store.js';
import {bindActionCreators} from 'redux';
import * as actionCreators from './action';
let {dispatch} = store;
export default bindActionCreators({ ...actionCreators}, dispatch);

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

// react/action.js
import * as types from '/actionType.js';
export const setDemoAPP = payload => ({type: types.SET_DEMO_APP,payload
});
// 其他 actionCreators ...

为了更好地合作,我们单独为 action 的 type 划分了一个模块 —— actionTypes.js 里面看起来会比较无聊:

// react/actionTypes.js
export const SET_DEMO_APP = "SET_DEMO_APP";
// 其他 types ...

react/reducers/ 和 react/store.js

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

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

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

import * as actionTypes from '../actionTypes';
let initState = {isLoading: true
};
// 对应 state.demoAPP
const demoAPP = (state = initState, action) => {switch (action.type) {case actionTypes.SET_DEMO_APP:return {isLoading: false,...action.payload};default:return state;}
}
export default demoAPP; // 把它转交给 createStore 函数

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

  • state

    • demoAPP

当有多个 reducer 时:

import * as actionTypes from '../actionTypes';
import { combineReducers } from 'redux';
let initState = {isLoading: true
};
// 对应 state.demoAPP
const demoAPP = (state = initState, action) => {switch (action.type) {case actionTypes.SET_DEMO_APP:return {isLoading: false,...action.payload};default:return state;}
}
// 对应 state.reducerB
const reducerB = (state = {}, action) => {switch (action.type) {case actionTypes.SET_REDUCER_B:return {isLoading: false,...action.payload};default:return state;}
}
const rootReducer = combineReducers({demoAPP,reducerB});
export default rootReducer;

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

  • state

    • demoAPP
    • reducerB

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

// react/reducers/index.js
import demoAPP from './demoAPP.js';
import reducerB from './reducerB.js';
const rootReducer = combineReducers({demoAPP,reducerB});
export default rootReducer;

接着,我们来看 store 模块:

// react/store.js
import rootReducer from './reducers';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
const initialState = {};
const finalCreateStore = compose(applyMiddleware(thunk)
)(createStore);
export default finalCreateStore(rootReducer, initialState);

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

// react/store.js
import rootReducer from './reducers';
import { createStore } from 'redux';
export default createStore(rootReducer);

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

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

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

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

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

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

回到我们的话题,经过

const finalCreateStore = compose(applyMiddleware(thunk)
)(createStore);
export default finalCreateStore(rootReducer, initialState);

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

// react/action.js
import * as types from './actionType.js';
import * as model from './model.js';
// 同步 actionCreator
export const setDemoAPP = payload => ({type: types.SET_DEMO_APP,payload
});
// 异步 actionCreator
export const initDemoAPP = () => dispatch => {model.getBaseData().then(response => {let {status, data} = response;if (status === 0) {//请求成功且返回数据正常dispatch(setDemoAPP(data));}}, error => {// 处理请求异常的情况});
}

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

export const getBaseData () => {return $.getJSON('/someAPI');
}

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

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

3.结语

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

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

参考

http://redux.js.org/“>《Redux 官方文档》
《深入 React 技术栈》

React技术栈探究-Redux相关推荐

  1. 如何从零学习 React 技术栈

    为什么要学习 React? 首先,React 相较于其他框架,其生态圈发展最为完整成熟,有非常多现成的.完整的解决方案. 其次,它适用于大中型应用的开发,便于团队中多人之间协作,很多大厂都在正式的项目 ...

  2. React 技术栈在蚂蚁金服的实践

    在2017在线技术峰会"阿里开源项目最佳实践"上,蚂蚁金服前端工程师崔晓斌为大家带来了"React 技术栈在蚂蚁金服的实践"的演讲.主要从研发的模式变迁开始说起 ...

  3. react 技术栈项目轻量化方案调研

    react 技术栈项目轻量化方案调研 团队的新项目,无论是pc端的还是移动端的,都已全面转移到了 react 的技术栈. 然而,对移动端来说,react 框架脚本的体量还是有些偏大. 在后续项目比较成 ...

  4. 实战react技术栈+express前后端博客项目(8)-- 前端管理界面标签管理+后端对应接口开发...

    项目地址:https://github.com/Nealyang/R... 本想等项目做完再连载一波系列博客,随着开发的进行,也是的确遇到了不少坑,请教了不少人.遂想,何不一边记录踩坑,一边分享收获呢 ...

  5. 深入react技术栈(12):组件内通信

    我是歌谣 放弃很容易 但是坚持一定很酷 微信公众号关注前端小歌谣获取前后端知识 父组件向子组件传值 子组件向父组件传值 跨级组件通信 没有嵌套关系的组件通信 文章参考深入React技术栈

  6. 深入react技术栈(11):样式处理

    我是歌谣 放弃很容易 但是坚持一定很酷 微信公众号关注前端小歌谣 基本样式设置 样式中的像素值 使用className库 Css Modules css模块化遇到了哪些问题 css模块化方案 样式默认 ...

  7. 深入react技术栈(10):受控组件和非受控组件

    我是歌谣 放弃很容易 但是坚持一定很酷 微信公众号关注前端小歌谣 受控组件 非受控组件 受控组件和非受控组件的区别 文章参考深入React技术栈

  8. 深入react技术栈(9):表单

    我是歌谣 放弃很容易 但是坚持一定很酷 微信公众号搜索前端小歌谣 获取前端知识 应用表单组件 文本框 单选按钮和复选框 Select组件 文章参考深入react技术栈

  9. 深入react技术栈(8):事件系统

    我是歌谣 放弃很容易 但是坚持一定很酷 微信公众号搜索前端小歌谣获取前端知识 1合成事件的绑定方式 2合成事件的实现机制 3在React中使用原生事件 4合成事件和原生事件混用 5对比react与原生 ...

最新文章

  1. C++ explicit关键字
  2. 【计算理论】正则语言 ( 正则表达式原子定义 | 正则表达式递归定义 | 正则表达式语言原子定义 | 正则表达式语言结构归纳 | 正则表达式语言示例 | 根据正则表达式构造自动机 )
  3. 003_SpringBoot整合Filter
  4. sqlserver数据库中清空日志文件
  5. 如何在centos中找到安装mysql_centos上如何安装mysql
  6. C语言-字符串处理函数strcmp
  7. 克罗地亚第二狂想曲难度_黄海保级难度增加,将送强力前锋去富力,与建业竞争半个降级名额...
  8. 英文学术论文写作有哪些经验心得?
  9. 手机输入法带拼音声调_五笔已经淘汰,拼音到达瓶颈,百度重拳出击,全新输入方式来袭!...
  10. 如何建立数据指标体系
  11. python 栅格转矢量_python 矢量数据转栅格数据代码实例
  12. 社交产品分析:共同看片,微光
  13. SpringBoot整合ElasticaSearch
  14. 星星评分系统代码模板。
  15. 【滤波】基于最近邻算法实现多目标航迹关联附matlab代码
  16. 30万手表推荐_港剧演员的逼格有多高?30万的手表随便戴!
  17. 安装打印机时提示请反回上一步,使用USB数据线重新连接电脑和打印机
  18. web前端面试--浏览器兼容性问题
  19. 基于 LwIP 协议栈实现 tun2sys-socket,网络游戏加速器(一类)
  20. 数据结构与算法之算法基础公式

热门文章

  1. 好友列表页面java_怎样制作QQ好友列表的界面?
  2. java中文转换数字_Java 中文数字转换为阿拉伯数字
  3. 【BlockingQueue】BlockingQueue接口方法说明和区别
  4. 加多芬科技讲解目前趋势“不带钱包出门”已成趋势 无现金支付你准备好了吗?
  5. AI公开课之NLP:19.04.04李航—字节跳动AILab总监《深度学习与自然语言处理:评析与展望》课堂笔记以及个人感悟
  6. 迅为RK3568开发板Debian系统安装ToDesk
  7. 三星note20u计算机功能,三星Note20Ultra隐藏功能有哪些,三星Note20Ultra使用教程
  8. js中php遍历数组,vue.js如何遍历数组
  9. C++ char和uchar区别
  10. 12-监听器实现统计网站当前在线人数