目录
里边有用到的代码:
- https://github.com/redux-utilities/redux-promise/blob/master/src/index.js
- 自己照着 http://todomvc.com/写的 todo-list 代码在 https://github.com/zhoushaokun/to-do-list 上边
3.2 使用redux-promise-middleware
1 react-redux 的思想——将所有组件分为两大类
(1)UI 组件
- 对应用的其他部分没有依赖关系
- 只负UI 的呈现,不带有任何的逻辑
- 通过 props 接受数据
- 不使用任何的 Redux 的API
- 一般保存在 components 文件夹下
具体见 https://www.cnblogs.com/pengshuo/p/6645573.html
(2)容器组件
- 负责管理数据和业务逻辑,不负责 UI 呈现
- 使用 Redux 的API
- 一般保存在 containers 下
- 容器组件常常不是自己手动写成的,而是通过 UI 组件 connect(mapStateToProps, mapDispatchToProps)(UiComponent) 而成为高级组件
高阶组件:高阶组件(Hoc, Higher-order components),也称为enhancer。接受一个已有组件组件作为参数,并返回一个新的组件,后者将前者封装于内部。一般使用高阶组件是为了对已有组件进行某些能力上的增强。
来自 《React 全栈》---张轩,杨寒星
因此,react项目结构 设计的一般思路是:每个路由 Route 对应的组件一般是容器组件,容器组件一般是 UI 组件的包装。容器组件负责接触 Store, 而 state 树应该是在引入 Provider 目录下定义。
问题1:如果一个组件即有业务逻辑,又有UI,怎么办?
回答是,将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。
React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它
-----转自 阮一峰 Redux 入门教程(三):React-Redux 的用法
我的理解,这里当一个组价既有业务逻辑,又有UI,应该就是指的是 上文所说的 容器组件,容器组件本就是 UI 组件的升级,也不就是一个组件既有逻辑(与store的直接访问),又有UI呈现。
2 容器组件的实现
2.1 redux 库下项目的数据流向
基本 react 项目(仅适用redux 组件)下,数据的流向如下,
但是仅仅使用结构组织项目的代码,会发现UI界面的代码与交互联系的代码中有 组织action 的代码,而要获取 store,就要一级级从祖先的 store 中通过属性传递得到,这样做在项目复杂时,必然会很繁琐而且难以维护。
而react-redux 解决的就是将业务逻辑这部分代码从 react项目UI部分的内容 中剥离出来,从而实现UI界面代码的解耦。具体做法,是将 state 保存在一个 store 的Object 前提下,新增 容器组件 ,它负责收集并发送 actions给 reducers ,再将reducer处理后的结果(state)返回给UI组件,UI组件只需要借助属性(props)即可去 dispatch(action) 并获得更新后的 state。总之,react-redux 引入了容器组件这个中介,去帮助UI组件实现代码解耦,示意图
2.2 react 下使用 redux 插件
首先需要安装的两个插件 redux 、react-redux ,但是理解和react他们三者的区别还是很重要的,这样便于我们以后理解记忆各个库的接口函数。
React
:负责组件的UI界面渲染;Redux
:数据处理中心;React-Redux
:连接组件和数据中心,也就是把React和Redux联系起来。
因此,redux 中有 createStore 的API,而 react-redux 中有 Provider 和 connect 的常用API。所以 需要npm 安装这两个库。
npm install --save redux
npm install --save redux
这里需要提的一点:
action 是负责将数据从应用传送到 store 的有效载荷,action 本质是一个有 两个属性 的对象:
{
type: 'go to school',
data: {
needs: ['书包', '课本'],
howToGo: 'Bus',
...
}
}
实际上,事件的处理大抵也是这种思路,这两天学习了的 AngularJS 中 $on/$emit/$broadcast 指令,它们的用法也是类似。也就是说,你要将行为逻辑从项目中剥离出来,就要弄清楚行为响应的原理,而这里 redux 就是采用这种方式实现的。
注意:这里 dispatch 的action 都是对象格式(有 type 和 content 两个属性),但是在也可以是一个函数,这里先埋一个伏笔,具体见下文(异步流解决方案内容)。
2.3 组织程序
我们可以按照数据流向去组织程序,也可以按照先从 createStore 入手,我这里以前者组织。
- 由原型图,设计 UI 界面,
- 划分页面的内容,整理各个部分可能出现的 actions ,实现 action creator,
- 设计 store state,实现 reducer,
- 通过容器组件,连接 store 和 展示组件,
2.4 action creator
这是我 todo-list 练习中的 action creator 部分的代码:
import * as actionsType from "./actionsType";
export function handleChange (event) {
return {
type: actionsType.CHANGE,
text: event.target.value
};
}
export function add (event) {
return {
type: actionsType.ADD_ONE,
};
}
export function modify (id) {
return {
type: actionsType.MODIFY_ONE,
text: id
};
}
export function onToggle (id) {
return {
type: actionsType.DONE_ONE,
text: id
};
}
export function toggleAll () {
return {
type: actionsType.DONE_ALL
};
}
export function clearCompleted () {
return {
type: actionsType.CLEAR_COMPLETED
};
}
与上文提到的 action 对象的类型是一致,也就是说你要通过一定操作,将从 UI 组件中得来的数据(也可以没有数据如 clearCompleted 的action creator)封装为一个对象。
2.5 mapDispatchToProps
既然要从数据源说起,那么首先应该是容器组件的 mapDispatchToProps 的接口,它建立的是 从 UI组件 到 store.dispatch 的映射。其中 mapDispatchToProps 可以是对象也可以是函数。
- 当 mapDispathToProps 作为对象时,对象键名会作为对应UI组件的 props 下的方法,键值
An object with the same function names, but with every action creator wrapped into a
dispatch
call so they may be invoked directly, will be merged into the component’s props.——官方文档每个 action creator 会被包装成 dispatch(actionCreator()) 调用的形式,从而能在组件的 props 中直接触发 dispatch.
原理:
如果你传递了一个由 action creators 构成的对象,而不是函数,
connect
会在内部自动为你调用bindActionCreators。
bindActionCreators
接收两个参数:
- 一个函数(action creator)或一个对象(每个属性为一个action creator)
- dispatch
因此,可以直接引用 actioncreator 中产生的 action 并借助 ES6 对象解构赋值,可以像下边这样写。具体可以
import { add, modify, handleChange } from './actions';
const mapDispatchToProps = {
add,
modify,
handleChange,
};
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp);
// 等价于
const mapDipatchToProps = {
add: (...args) => dispatch(add(...args)),
modify: (...args) => dispatch(modify(...args));
handleChange: (...args) => dispatch(handleChange(...args));
};
但是如果直接将键值设置为对象(而不是函数),就不能向为 action 添加数据了。
注意:当你为 mapDispatchToProps 什么都不指定时(对象为null),你的 UI 组件会默认获得 dispatch 这个方法,并添加在props上。
- 当 mapDispatchToProps 作为函数时,接收 dispatch ownProps(可选) 参数。函数需要返回一个对象,对象结构与上边对象类似。
const mapDispatchToProps = (dispatch, ownProps) => {
return {
handleChange: (event) => {
dispatch(Actions.handleChange(event));
},
add: (event) => {
if (event.keyCode !== Constants.ENTER_KEY) {
return;
}
event.preventDefault();
dispatch(Actions.add(event));
},
modify: (id) => {
dispatch(Actions.modify(id));
},
onToggle: (id) => {
dispatch(Actions.onToggle(id));
},
toggleAll: () => {
dispatch(Actions.toggleAll());
},
clearCompleted: () => {
dispatch(Actions.clearCompleted());
}
};
};
这样,通过传入 dispatch 的参数,可以在自己决定在什么时候dispatch 什么样的 action ,比对象形式的 mapDispatchToProps 更加灵活。
但是仔细观察2.4、2.5中内容会发现,有些 action creator 函数其实都是只需要一步就可以完成的,所以,
我们建议适中使用这种“对象简写”形式的
mapDispatchToProps
,除非你有特殊理由需要自定义dispatching
行为。
2.6 组织 reducers 函数
reducer 函数的过程,我的理解就是,
reducer 函数接受两个参数,当前 reducer 函数执行前的 state (我习惯称他 old_state) 和 action 对象。
import {
Action
} from 'path';
const defaultState = {
prop1,
prop2
};
const Reducer = (state = defaultState, action = {}) => {
switch (action.type) {
case type:
return {
...state,
prop2: action.content
};
default:
return state;
}
};
export default Reducer;
注意:
- 必须要给出 state 默认值,
以为初次页面渲染提供数据源,上边代码使用了 ES6 中的解构赋值,当等号右边是 undefined 时,取默认值。
- reducer 必须是纯函数
关于 reducer 函数,最重要的一点是要保证是纯函数,传入的参数 state 与 action 都不能改变,所以上边代码用到了 浅拷贝 的形式返回 新的state。
return {
...state,
prop2: action.content
};
//除此之外,还可以借助 assign 来实现浅拷贝
return Object.assign({}, state, {prop2: action.content});
- 关于 reducer 的拆分
这我会另外总结关于目前遇到了提升 react 性能的方法汇总。
2.7 store和Provider
有了 reducer 当然就可以创造 store 对象了,有了 store 对象,我们就可以使用 Provider 组件来为容器组件提供 store 对象了。
创造 store 对象,前边讲到 redux 库管理数据,那么store的构建函数——createStore就是有redux库来提供。得到如下的代码。
import { createStore } from "redux";
import { composeWithDevTools } from 'redux-devtools-extension';
import reducer from "../reducers/reducer";
let store = createStore(reducer, composeWithDevTools());
//composeWithDevTools() 方便浏览器使用redux 插件所引入的库,与上边 'redux-devtools-extension'对应
export default store;
使用 Provider
import React from 'react';
import './App.css';
import TodoApp_Can from "../containers/TodoApp_Can.jsx";
import Test from "./test"
import store from "../containers/store";
import { Provider } from "react-redux";
class App extends React.Component {
render() {
return (
<div className="App">
<Provider store={store}>
<TodoApp_Can className="todoapp" ></TodoApp_Can>
<Test></Test>
</Provider>
<footer className="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="https://github.com/zhoushaokun/">ZSK</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</div>
);
}
}
export default App;
2.8 mapStateToProps
这是最后一步,也是最关键一步,只有通过这里才能将新的state 渲染到UI上,用户才能感受到与机器的交互。
mapStateToProps 与 mapDispatchToProps 类似,都可以是一个函数,都可以接受 ownProps 这个可选参数,但是它建立的是从(外部的)state对象到UI组件的props的映射,而且用法更简单。
你可以简单的将mapStateToProps写成
const mapStateToProps = ( state ) => ({ state });
当 mapStateToProps 的函数中有 state 参数时,表示UI组件会订阅store,每当store更新,就会触发UI组件的重新渲染。当然可以接受第二个可选参数 ownProps, 表示容器组件自身的props对象,使用该参数后,若容器组件的参数发生改变,也会引起UI组件的重新渲染。
ownProps 指的是传递给容器组件,需要被透传给展示组件的属性。
当然这样写,当项目的结构越来越来复杂时,页面中一个小部分的组价更新会引起所有组件的更新,这样势必会影响用户体验。具体的性能提升方法是:计算记忆结果,使用 select 库。
他的实现原理是修改数据尽可能复用原来的数据,在整体状态的局部发生变化时,那些依赖未变更部分数据的组件所接触的数据保持不变,这在一定程度上会减少重复渲染。
关于记忆计算结构,原理还好理解,但具体怎么用我还没研究透彻。
3 react 中异步流的解决方案
3.1 使用redux-thunk
redux-thunk 允许 dispatch 的action是一个函数,而不是一个普通的对象。如果接受的action是一个函数,那么就会直接调用这个函数,并将 dispatch 与 getState(可选) 作为参数传入,这样可以在 action creator 返回的函数中,获取dispatch、getState或者根据条件选择性地dispatch、多次dispatch乃至实现异步dispatch。
使用 middleware 中间件
//store.js下
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(
reducers,
applyMiddleware(thunk)
);
如下,fetchFriends() 是一个返回 匿名函数 的函数,这个匿名函数 接受两个参数(dispatch、getState)。因此,dispatch(fetchFriends ) 会直接导致 fetchFriends 函数执行,剩余的事情就是等待获取到 url 下的资源并 dispatch 给 reducer,reducer 按 type 判断并作用于 store,最终渲染新的页面。
//actionCreators.js下文件
//fetchFriends是一个Action Creator(动作生成器),返回一个函数。
export function fetchFriends() {
return (dispatch, getState) => {
return fetchHttpRequest("*****url****")
.then(data => {
dispatch({type:"error"})
})
.then(dataJson => {
dispatch({type:"success", dataJson});
});
}
}
// redux的connect作用文件下
import * as actionCreators from "./store/actionCreators";
mapDispatchToProps = (dispatch) => {
return {
getFriends: () => {
dispatch(actionCreators.fetchFriends());
}
};
}
并且对于异步的行为,为了安全起见,应该定义 3 个actionType (发起异步,异步成功,异步失败),并分别dispatch 三种 action,另外一种做法是,仅定义一个类型的action,只不过通过对象 data 中的 error 字段标识本次行为是否成功。
项目中使用的是 promise 对象,这样就可以很方便的指定在哪里去 dispatch 错误类型的action,在哪里去 dispatch 成功的 action 。
在 UI 组件中,logout 函数返回一个 Promise 对象,然后就可以指定该 Promise 对象中的 resolve 函数的具体内容(即异步成功要执行的下一步),data_resolve 通过上边 resolve 处传入的实参。
this.props.logout({
data
})
.then((data_resolve) => {
//....
})
.catch(data_error => {
//....
});
3.2 使用redux-promise-middleware
如下,
const mapDispatchToProps = (dispatch) => {
return {
login: (userInfo) => {
return {
type: "async",
payload: new Promise(resolve, reject => {
//在这里执行异步操作,并指定何时 resolve ,何时 reject
});
})
},
}
}
从 redux-promise 的源码可以看出,传过来的action,要检查 action 的payload,如果是promise对象,就再次 dispatch 一个普通的(与同步相同的)action,这样就可以解决 异步流了。
import isPromise from 'is-promise';
import { isFSA } from 'flux-standard-action';
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action) ? action.then(dispatch) : next(action);
}
return isPromise(action.payload)
? action.payload
.then(result => dispatch({ ...action, payload: result }))
.catch(error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
})
: next(action);
};
}
注意:
- resolve、reject 都是指 接下来应该要做的工作,我们可以给它们传入异步操作后的数据结果,供以后继续操作。
- 其实可以看出,异步action 可以通过同步的action去实现。事实上,很多同步的action也可以通过其他的action帮助实现。因此, 容器的 dispatch 并非和 action type 一一对应的,可能有些交互行为需要组合好几个 dispatch(action) 去实现。