一、常用的 redux 中间件

  • 我们以 Counter 为例;组件代码如下:
  1. import React, { Component } from 'react'
  2. import actions from '../store2/action'
  3. import { connect } from 'react-redux'
  4. class Counter extends Component {
  5. render () {
  6. return (<div className="container">
  7. <button onClick={() => this.props.add(1)}>+</button>
  8. <span>{this.props.num}</span>
  9. <button onClick={() => this.props.minus(1)}>-</button>
  10. </div>)
  11. }
  12. }
  13. export default connect(state => ({...state}), actions)(Counter)

1.1 redux-logger

当 store 中的状态发生变化时,redux-logger 会把改变之前的状态和改变之后的状态输出到控制台;

1.1.1 安装 redux-logger

所有的中间件在使用前都需要安装;

  1. yarn add redux-logger --save

1.1.2 使用中间件

使用中间件,需要借助 redux 中的另一个方法 applyMiddleware,从 redux 中导出它,同时导入中间件

  1. import { createStore, applyMiddleware } from 'redux'
  2. import reduxLogger from 'redux-logger'

然后,在调用 createStore 时传入第二个参数:applyMiddleware(中间件名字)

示例:

  1. import { createStore, applyMiddleware } from 'redux'
  2. import reducer from './reducer'
  3. import reduxLogger from 'redux-logger'
  4. export default createStore(reducer, applyMiddleware(reduxLogger))

接着,在控制台修改状态试试看,控制台会输出修改前的状态和修改后的状态;

1.2 redux-thunk

  • 需求,点击 Counter 的加,2s 后再执行加的操作

我们有时候需要一些异步的操作,例如定时器、ajax 等,这些异步的操作无法直接在 actionCreator 中进行,但是这个 redux-thunk 函数,可以让我们的 actionCreator 返回一个 function ,这个函数的参数是 dispatch 和 getState就是 store.dispatch、store.getState;
它把 dispatch 的控制权交给了我们 actionCreator 返回的函数,在这个函数中,我们可以在任意位置 dispatch,例如定时器中;

1.2.1 安装 redux-thunk

  1. yarn add redux-thunk --save

1.2.2 导入,并且作为第二个参数传入 createStore 时的 applyMiddleware

  1. import { createStore, applyMiddleware } from 'redux'
  2. import reducer from './reducer'
  3. import reduxLogger from 'redux-logger'
  4. import reduxThunk from 'redux-thunk'
  5. export default createStore(reducer, applyMiddleware(reduxLogger, reduxThunk))

1.2.3 修改后的 actionCreator 函数

  1. import * as Types from '../action-types'
  2. export default {
  3. add (amount) {
  4. return (dispatch, getState) => {
  5. // 返回一个 函数,在我们可以在任意位置 dispatch action,异步的也可以
  6. setTimeout(() => {
  7. dispatch({
  8. type: Types.ADD,
  9. amount
  10. })
  11. }, 2000)
  12. }
  13. },
  14. minus (amount) {
  15. return {
  16. type: Types.MINUS,
  17. amount
  18. }
  19. }
  20. }

1.3 redux-promise

我们在日常开发中,尤其是处理异步的数据时,常用到 Promise;对此 redux-promise 中间件专门用来处理 redux 中的 Promise;

1.3.1 安装 redux-promise

  1. yarn add redux-promise --save

1.3.2 导入并使用

  1. import { createStore, applyMiddleware } from 'redux'
  2. import reducer from './reducer'
  3. import reduxLogger from 'redux-logger'
  4. import reduxThunk from 'redux-thunk'
  5. import reduxPromise from 'redux-promise'
  6. export default createStore(reducer, applyMiddleware(reduxLogger, reduxThunk, reduxPromise))

1.3.3 使用 redux-promise 后,在 actionCreator 中有两种用法:

  1. 直接在某个 actionCreator 中返回一个 Promise 实例,但是这种用法,只能处理 resolve,并在 resolve 时传入一个 action 对象;如果 reject 直接抛出错误
  • 示例:
  1. import * as Types from '../action-types'
  2. export default {
  3. add (amount) {
  4. return (dispatch, getState) => {
  5. setTimeout(() => {
  6. dispatch({
  7. type: Types.ADD,
  8. amount
  9. })
  10. }, 2000)
  11. }
  12. },
  13. minus (amount) {
  14. return new Promise((resolve, reject) => {
  15. setTimeout(() => {
  16. resolve({
  17. type: Types.MINUS,
  18. amount
  19. })
  20. }, 2000)
  21. })
  22. }
  23. }
  1. 如果失败成功都要处理,我们不能直接在 actionCreator 函数中返回 Promise 实例,而是在返回的 action 对象中增加 payload 属性,payload 的属性值是一个 Promise,如果 Promise resolve 那么 payload 代表的就是 resolve 时传入的值,如果 reject 时,payload 代表的就是 reject 时传入的值,同时 reducer 函数也需要修改;

示例:

  • action/index.js 【actionCreator】
  1. import * as Types from '../action-types'
  2. export default {
  3. add (amount) {
  4. return (dispatch, getState) => {
  5. setTimeout(() => {
  6. dispatch({
  7. type: Types.ADD,
  8. amount
  9. })
  10. }, 2000)
  11. }
  12. },
  13. minus (amount) {
  14. return {
  15. type: Types.MINUS,
  16. payload: new Promise((resolve, reject) => {
  17. // resolve(1)
  18. reject(10)
  19. })
  20. }
  21. }
  22. }
  • reducer/index.js
  1. import * as Types from '../action-types'
  2. export default function (state = {num: 0}, action) {
  3. switch (action.type) {
  4. case Types.ADD:
  5. return {
  6. num: state.num + action.amount
  7. }
  8. case Types.MINUS:
  9. return {
  10. num: state.num - action.payload // actionCreator 函数使用 Promise 对应修改为减去 action.payload
  11. }
  12. }
  13. return state
  14. }

二、TodoList 案例

效果如图:

2.1 项目结构如下

  1. -src
  2. index.css
  3. index.js
  4. ├─components
  5. App.js
  6. TodoFooter.js
  7. TodoHeader.js
  8. TodoItem.js
  9. TodoList.js
  10. ├─store
  11. action-types.js
  12. index.js
  13. ├─action
  14. index.js
  15. └─reducer
  16. index.js

2.2 代码如下:

2.2.1 src/index.js

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import './index.css';
  4. import { Provider } from 'react-redux'
  5. import App from './components/App';
  6. import store from './store'
  7. ReactDOM.render(<Provider store={store}>
  8. <App />
  9. </Provider>, document.getElementById('root'));

2.2.2 sr/store/index.js

  1. import { createStore } from 'redux'
  2. import reducer from './reducer'
  3. export default createStore(reducer)

2.2.3 src/store/action-types.js

  1. export const ADD_TODO = 'ADD_TODO'
  2. export const CHANGE_SELECTED = 'CHANGE_SELECTED'
  3. export const DELETE_TODO = 'DELETE_TODO'
  4. export const CHANGE_CURRENT_TYPE = 'CHANGE_CURRENT_TYPE'

2.2.4 src/store/action/index.js 【actionCreator】

  1. import * as Types from '../action-types'
  2. // 在组件中使用 connect 的时候我们需要传递一个 actionCreator 对象,所以这里我们直接导出一个 actionCreator 对象
  3. export default {
  4. addTodo (todo) { // 添加 todo 内容
  5. return {
  6. type: Types.ADD_TODO,
  7. todo
  8. }
  9. },
  10. changeSelected (id) {
  11. return {
  12. type: Types.CHANGE_SELECTED,
  13. id
  14. }
  15. },
  16. deleteTodo (id) {
  17. return {
  18. type: Types.DELETE_TODO,
  19. id
  20. }
  21. },
  22. changeType (curType) {
  23. return {
  24. type: Types.CHANGE_CURRENT_TYPE,
  25. curType
  26. }
  27. }
  28. }

2.2.5 src/store/reducer/index.js

  1. import * as Types from '../action-types'
  2. let initState = {
  3. type: 'all',
  4. todos: [
  5. {
  6. isSelected: false,
  7. title: '今天吃药了吗',
  8. id: 1
  9. },
  10. {
  11. isSelected: true,
  12. title: '今天吃饭了吗',
  13. id: 2
  14. }
  15. ]
  16. }
  17. export default function todo(state= initState, action) {
  18. switch (action.type) {
  19. case Types.ADD_TODO:
  20. return {
  21. ...state,
  22. todos: [
  23. ...state.todos,
  24. action.todo
  25. ]
  26. }
  27. case Types.CHANGE_SELECTED:
  28. return {
  29. ...state,
  30. todos: state.todos.map(item => {
  31. if (item.id === action.id) {
  32. item.isSelected = !item.isSelected
  33. return item
  34. }
  35. return item
  36. })
  37. }
  38. case Types.DELETE_TODO:
  39. return {
  40. ...state,
  41. todos: state.todos.filter(item => item.id !== action.id)
  42. }
  43. case Types.CHANGE_CURRENT_TYPE:
  44. return {
  45. ...state,
  46. type: action.curType
  47. }
  48. }
  49. // 写 reducer 第一件事返回默认状态
  50. return state
  51. }

2.2.5 src/components/App.js

  1. import React, { Component } from 'react'
  2. import TodoHeader from './TodoHeader'
  3. import TodoFooter from './TodoFooter'
  4. import TodoList from './TodoList'
  5. import 'bootstrap/dist/css/bootstrap.min.css'
  6. export default class App extends Component {
  7. render () {
  8. return (<div className="container">
  9. <div className="col-md-6 col-md-offset-3">
  10. <div className="panel panel-danger">
  11. <div className="panel-heading">
  12. <TodoHeader />
  13. </div>
  14. <div className="panel-body">
  15. <TodoList />
  16. </div>
  17. <div className="panel-footer">
  18. <TodoFooter />
  19. </div>
  20. </div>
  21. </div>
  22. </div>)
  23. }
  24. }

2.2.5 src/components/TodoHeader.js

  1. import React, {Component} from 'react'
  2. import {connect} from 'react-redux'
  3. import actions from '../store/action'
  4. class TodoHeader extends Component {
  5. getUnfinishedCount = () => this.props.todos.filter(i => !i.isSelected).length
  6. render () {
  7. return (<div>
  8. <h3>亲,你有 {this.getUnfinishedCount()} 件事没有完成</h3>
  9. <input type="text" className="form-control" onKeyDown={(e) => {
  10. if (e.keyCode === 13) {
  11. this.props.addTodo({
  12. id: this.props.todos[this.props.todos.length - 1].id + 1,
  13. title: e.target.value,
  14. isSelected: false
  15. })
  16. e.target.value = ''
  17. }
  18. }}/>
  19. </div>)
  20. }
  21. }
  22. export default connect(state => ({...state}), actions)(TodoHeader)

2.2.7 src/components/TodoList.js

  1. import React, {Component} from 'react'
  2. import actions from '../store/action'
  3. import TodoItem from './TodoItem'
  4. import { connect } from 'react-redux'
  5. class TodoList extends Component {
  6. filterData = () => {
  7. switch (this.props.type) {
  8. case 'all':
  9. return this.props.todos
  10. case 'unfinished':
  11. return this.props.todos.filter(i => !i.isSelected)
  12. case 'finished':
  13. return this.props.todos.filter(i => i.isSelected)
  14. }
  15. }
  16. render() {
  17. return (<ul className="list-group">
  18. {
  19. this.filterData().map((item, index) => {
  20. return <TodoItem item={item}
  21. key={index}
  22. changeSelected={this.props.changeSelected}
  23. deleteTodo={this.props.deleteTodo} />
  24. })
  25. }
  26. </ul>)
  27. }
  28. }
  29. export default connect(state => ({...state}), actions)(TodoList)

2.2.8 src/components/TodoItem.js

  1. import React, { Component } from 'react'
  2. export default class TodoItem extends Component {
  3. render () {
  4. let { item, changeSelected, deleteTodo } = this.props
  5. return (<li className="list-group-item">
  6. <input type="checkbox"
  7. checked={item.isSelected}
  8. onChange={() => changeSelected(item.id)} />
  9. {item.title}
  10. <button className="btn btn-xs pull-right" onClick={() => deleteTodo(item.id)}>&times;</button>
  11. </li>)
  12. }
  13. }

2.2.9 src/components/TodoFooter.js

  1. import React, { Component } from 'react'
  2. import actions from '../store/action'
  3. import { connect } from 'react-redux'
  4. class TodoFooter extends Component {
  5. render () {
  6. let type = this.props.type
  7. return (<div>
  8. <nav className="nav nav-pills" onClick={(e) => {
  9. // h5 新增的属性,元素对象有一个 dataset 属性,值是对象,其中包含了以 data- 开头的行内属性及对应的值;注意取值时不带 data-
  10. this.props.changeType(e.target.dataset.type)
  11. }}>
  12. <li className={type === 'all' ? 'active' : ''}><a data-type="all">全部</a></li>
  13. <li className={type === 'unfinished' ? 'active' : ''}><a data-type="unfinished">未完成</a></li>
  14. <li className={type === 'finished' ? 'active' : ''}><a data-type="finished">已完成</a></li>
  15. </nav>
  16. </div>)
  17. }
  18. }
  19. export default connect(state => ({...state}), actions)(TodoFooter)