1.前言

在之前的博客中,我写了一篇关于todo-list实现的博客,一步一步详细的记录了如何使用基础的React知识实现一个React单页面应用,通过该篇文章,能够对React入门开发有一个直观的认识和粗浅的理解。

近期,个人学习了一下Redux,又将该项目使用 React+Redux的方式进行了实现。本片内容记录以下实践的过程。通过本实例,可以学习到:

  • Redux的核心思想;
  • Redux的三大概念;
  • React+Redux的开发方法和流程;

下面将从以下几个方面展开讲解和记录。

2.项目演示!

基于React Redux的todo-list小案例 - 图1

3.Redux基础知识

3.1 认识

3.1.1 动机

随着 JavaScript 单页面应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态),管理不断变化的 state 非常困难,state 在什么时候,由于什么原因,如何变化已然不受控制。当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。

因此,需要一种更可控的方式来管理系统的state,让系统的state变得可预测,redux就是用来管理系统state的工具。

3.1.2 三大原则

  • 单一数据源
    整个应用的状态都保存在一个对象中,一个应用只有一个唯一的state,保存在store中,通过store统一管理。
  • 状态是只读的
    唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
    redux不会直接修改state,而是在状态发生更改时,返回一个全新的状态,旧的状态并没有进行更改,得以保留。可以使用 redux-devtools-extension 工具进行可视化查看。
  • 状态修改由纯函数完成
    Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。

3.2 基础

3.2.1 Store

Redux的核心是 Store ,Store 由 createStore方法创建,

  1. createStore(reducer, [initState])//reducer表示一个根reducer,initState是一个初始化状态

store提供方法来操作state

3.2.2 Action

action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。通过 store.dispatch() 将 action 传到 store。如果有数据需要添加,在action中一并传过来。

action需要action创建函数进行创建,如下是一个action创建函数:

  1. /*
  2. * action 类型
  3. */
  4. export const ADD_TODO = 'ADD_TODO';
  5. export const TOGGLE_TODO = 'TOGGLE_TODO'
  6. export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'
  7. /*
  8. * 其它的常量
  9. */
  10. export const VisibilityFilters = {
  11. SHOW_ALL: 'SHOW_ALL',
  12. SHOW_COMPLETED: 'SHOW_COMPLETED',
  13. SHOW_ACTIVE: 'SHOW_ACTIVE'
  14. }
  15. /*
  16. * action 创建函数
  17. */
  18. export function addTodo(text) {
  19. return { type: ADD_TODO, text }
  20. }
  21. export function toggleTodo(index) {
  22. return { type: TOGGLE_TODO, index }
  23. }
  24. export function setVisibilityFilter(filter) {
  25. return { type: SET_VISIBILITY_FILTER, filter }
  26. }

返回一个对象,改对象由reducer获取,根据 action 类型进行相应操作。

3.2.3 Reducer

store通过 store.dispatch(某action(参数)) 来给reducer安排任务。

简单理解,一个reducer就是一个函数,这个函数接受两个参数 当前state 和 action,然后根据 action 来对当前 state 进行操作,如果有需要更改的地方,就返回一个 新的 state ,而不会对旧的 state进行操作,任何一个阶段的 state 都可以进行查看和监测,这让 state 的管理变得可控,可以实时追踪 state的变化。

React中使用Redux时,需要有一个根 Reducer,这个根 Reducer 通过 conbineReducer() 将多个子 Reducer 组合起来。

根reducer:

  1. import { combineReducers } from 'redux'
  2. import todos from './todos'
  3. import visibilityFilter from './visibilityFilter'
  4. //根reducer
  5. // rootReducer 根reducer,把子reducer组合在一起
  6. export default combineReducers({
  7. todos, //子state
  8. visibilityFilter //子state
  9. })

子reducer:

  1. //这里的state = []为state的当前值
  2. const todos = (state = [], action) => {
  3. switch (action.type) {
  4. case 'ADD_TODO':
  5. return [
  6. ...state, // Object.assign() 新建了一个副本
  7. {
  8. id: action.id,
  9. text: action.text,
  10. completed: false
  11. }
  12. ]
  13. case 'TOGGLE_TODO':
  14. // console.log(state);
  15. return state.map((value,index) => {
  16. return (value.id === action.id) ? {...value,completed:!value.completed} : value;
  17. })
  18. default:
  19. return state;
  20. }
  21. }
  22. export default todos;

3.2.4 数据流

基于React Redux的todo-list小案例 - 图2

3.3 展示组件和容器组件

3.3.1 展示组件和容器组件分离

本部分在笔者尚未深入研究,在此给出redux作者写的深度解析文章链接及网上的译文链接,读者可自行查看。

原文链接:展示组件和容器组件相分离 译文链接:展示组件和容器组件相分离

3.3.2 展示组件和容器组件比较

展示组件 容器组件
作用 描述如何展示骨架、样式 描述如何运行(数据获取、状态更新)
直接使用Redux 否 是
数据来源 props 监听Redux state
数据修改 从props调用回调函数 向Redux派发action
调用方式 手动 通常由React Redux生成

大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和 Redux store 连接起来。

React Redux 的使用 connect() 方法来生成容器组件。

  1. import { connect } from 'react-redux'
  2. import { setVisibilityFilter } from '../actions'
  3. import Link from '../components/Link'
  4. //mapStateToProps参数中的state是store的state.
  5. // 在容器组件中,通过mapStateToProps方法,在展示组件和store中间传递数据和执行action
  6. // ownProps表示的是组件自身的属性,即父组件传过来的属性
  7. const mapStateToProps = (state, ownProps) => {
  8. return {
  9. active: ownProps.filter === state.setVisibilityFilter
  10. }
  11. }
  12. // ownProps表示的是组件自身的属性,即父组件传过来的属性
  13. const mapDispatchToProps = (dispatch, ownProps) => {
  14. return {
  15. // 这里写方法名,在展示组件中通过这个方法名来执行里面的action派遣函数
  16. onClick: () => {
  17. // 执行setVisibilityFilter这个action
  18. dispatch(setVisibilityFilter(ownProps.filter))
  19. }
  20. }
  21. }
  22. //通过connect让Link组件得以连接store,从store中取得active数据和onClick方法的执行体。
  23. export default connect(
  24. mapStateToProps,
  25. mapDispatchToProps
  26. )(Link)

connect() 中最核心的两个方法是:mapActionToProps 和 mapDispatchToProps ,通过容器组件,可以在 展示组件和 store之间传递数据和执行 action。

4.基于Redux的React项目实战

4.1 目录结构

根据Redux的几大组成部分,在进行开发时,将在之前基础的React开发模式下,增加几个文件夹,形成新的开发目录结构,具体目录结构如下图:

  1. │ App.css
  2. │ App.js
  3. │ App.test.js
  4. │ index.css
  5. │ index.js
  6. │ logo.svg
  7. │ readme.txt
  8. │ serviceWorker.js
  9. │ setupTests.js
  10. ├─actions
  11. ├─components
  12. ├─containers
  13. └─reducers

基于React Redux的todo-list小案例 - 图3

如图,在之前的结构下,新增了 actions 、reducers 、containers 这三个文件夹。

4.2 配置React-Redux开发环境

4.2.1 步骤

在建好文件目录后就可以开始进行开发了,由于是基于Redux做React开发,所以首先一步当然需要把Redux的开发环境配置一下。

  • 安装 react-redux 包
  1. npm install --save react-redux
  • 编写入口文件 index.js

前文讲到,redux使用一个唯一的 store 来对项目进行状态管理,那么首先我们需要创建这个 store ,并将这个 store 作为一个属性,传递给下级子组件。

具体代码如下:

  1. import React from 'react';
  2. import ReactDOM, { render } from 'react-dom';
  3. //redux ----------------------------------------------------
  4. import { Provider } from 'react-redux';
  5. import { createStore } from 'redux';
  6. import { rootReducer } from './reducers';
  7. //引入项目根组件App.jsx
  8. import App from './App';
  9. //创建store,将根Reducer传入store中。redux应用只有一个单一的store
  10. const store = createStore(rootReducer);
  11. render(
  12. <Provider store = {store}>
  13. <App />
  14. </Provider>,
  15. document.getElementById('id')
  16. )

如上代码所示,使用Redux,需要引入的文件有:

  • Provider 组件
  • createStore 方法
  • 根reducer
  • 项目根组件App.jsx

createStore:createStore 方法可接受两个参数,第一个是项目的根 reducer ,是必选的参数,另一个是可选的参数,可输入项目的初始 state 值。通过该方法创建一个 store 实例,即为项目唯一的 store。

Provider组件:Provider组件包裹在跟组件App.jsx外层,将项目的 store作为属性传递给 Provider。使用Provider 可以实现所有子组件直接对 store 进行访问。在下文将深入讲一下 Provider 的实现和工作原理。

根reducer:随之项目的不断增大,程序state的越来越复杂,只用一个 reducer 是很难满足实际需求的,redux中采用将 reducer 进行拆分,最终在状态改变之前通过 根 reducer 将 各个拆分的子 reducer 进行合并方式来进行处理。

App.jsx:项目的跟组件,将一级子组件写在App.jsx中。

4.2.2 Provider

provider 包裹在根组件外层,使所有的子组件都可以拿到state。它接受store作为props,然后通过context往下传,这样react中任何组件都可以通过context获取store。

Provider 原理:

原理是React组件的context属性

组件源码如下:

原理是React组件的context属性

  1. export default class Provider extends Component {
  2. getChildContext() {
  3. //返回一个对象,这个对象就是context
  4. return { store: this.store }
  5. }
  6. constructor(props, context) {
  7. super(props, context)
  8. this.store = props.store
  9. }
  10. render() {
  11. return Children.only(this.props.children)
  12. }
  13. }
  14. Provider.propTypes = {
  15. store: storeShape.isRequired,
  16. children: PropTypes.element.isRequired
  17. }
  18. Provider.childContextTypes = {
  19. store: storeShape.isRequired
  20. }

4.3 src目录文件列表

文件夹 文件
src index.js
src/actions index.js
src/components(展示组件) App.jsx
TodoList.jsx
Footer.jsx
Todo.jsx
Link.jsx
src/containers(容器组件) AddTodo.js
FilterLink.js
VisibleTodoList.js
src/reducers index.js
todo.jsx
visibilityFilter.js

4.4 项目代码

注意:

  • 代码说明大部分写在项目代码中,读者在查看时,建议对代码也要进行仔细阅读。
  • 本项目功能较简单,因此代码直接按照文件目录给出,而不按照功能模块陈列。

4.4.1 入口文件 index.js

  1. import React from 'react';
  2. import ReactDOM, { render } from 'react-dom';
  3. import './index.css';
  4. import App from './components/App';
  5. //redux
  6. import { Provider } from 'react-redux';
  7. import { createStore } from 'redux';
  8. import rootReducer from './reducers';
  9. //创建store,createStore()第一个参数是项目的根reducer,第二个参数是可选的,用于设置state的初始状态
  10. const store = createStore(rootReducer);
  11. render(
  12. // Provider组件包裹在跟组件的外层,使所有的子组件都可以拿到state.
  13. // 它接受store作为props,然后通过context往下传,这样react中任何组件
  14. // 都可以通过context获取store.
  15. <Provider store = {store}>
  16. {/* App 根组件 */}
  17. <App />
  18. </Provider>,
  19. document.getElementById('root')
  20. )

4.4.2 actions文件

  • index.js
  1. let nextTodoId = 0;
  2. // 定义action 常量 对于小型项目,可以将action常量和action创建函数写在一起,对于复杂的项目,可将action常量和其他的常量抽取出来,放到单独的某个常量文件夹中
  3. const ADD_TODO = 'ADD_TODO';
  4. const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
  5. const TOGGLE_TODO = 'TOGGLE_TODO';
  6. //这里是几个action创建函数,函数里面的对象才是action,返回一个action
  7. // text是跟随action传递的数据
  8. // 调用 dispatch(addTodo(text)),即代表派遣action,交给reducer处理
  9. //action生成函数
  10. // 大部分情况下,他简单的从参数中收集信息,组装成一个action对象并返回,
  11. // 但对于较为复杂的行为,他往往会容纳较多的业务逻辑与副作用,包括与后端的交互等等。
  12. export const addTodo = (text) => {
  13. return {
  14. type: ADD_TODO,
  15. id: nextTodoId ++,
  16. text
  17. }
  18. }
  19. export const setVisibilityFilter = (filter) => {
  20. return {
  21. type: SET_VISIBILITY_FILTER,
  22. filter
  23. }
  24. }
  25. export const toggleTodo = (id) => {
  26. return {
  27. type: TOGGLE_TODO,
  28. id
  29. }
  30. }
  31. //三个常量
  32. export const VisibilityFilters = {
  33. SHOW_ALL: 'SHOW_ALL',
  34. SHOW_COMPLETED: 'SHOW_COMPLETED',
  35. SHOW_ACTIVE: 'SHOW_ACTIVE'
  36. }

4.4.3 components文件(展示组件)

  • App.jsx
  1. import React from 'react'
  2. import Footer from './Footer'
  3. import AddTodo from '../containers/AddTodo'
  4. import VisibleTodoList from '../containers/VisibleTodoList'
  5. //应用的根组件
  6. const App = () => {
  7. return (
  8. <div>
  9. {/* 容器组件 */}
  10. <AddTodo />
  11. {/* 容器组件 */}
  12. <VisibleTodoList />
  13. {/* 展示组件 */}
  14. <Footer />
  15. </div>
  16. )
  17. }
  18. export default App
  • Footer.jsx
  1. import React from 'react'
  2. import FilterLink from '../containers/FilterLink'
  3. import { VisibilityFilters } from '../actions'
  4. //无状态组件,这种写法初学者可能难以理解,可以先补习下ES6,等价于
  5. //function Footer(){
  6. // return (<div>XXX</div>)
  7. //}
  8. const Footer = () => (
  9. <div>
  10. <span>Show: </span>
  11. <FilterLink filter={VisibilityFilters.SHOW_ALL}>
  12. All
  13. </FilterLink>
  14. <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>
  15. Active
  16. </FilterLink>
  17. <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
  18. Completed
  19. </FilterLink>
  20. </div>
  21. )
  22. export default Footer
  • Link.jsx
  1. import React from 'react'
  2. import PropTypes from 'prop-types'
  3. //prop-types是一个组件属性校验包,导入这个包可以数据进行格式等方面的校验
  4. const Link = (props) => {
  5. return (
  6. <button onClick={props.onClick} disabled={props.active} style={{marginLeft:'4px'}}>
  7. {props.children}
  8. </button>
  9. )
  10. }
  11. Link.propTypes = {
  12. active: PropTypes.bool.isRequired,
  13. children: PropTypes.node.isRequired,
  14. onClick: PropTypes.func.isRequired
  15. }
  16. export default Link
  • TodoList.jsx
  1. import React, { createFactory } from 'react'
  2. import PropTypes from 'prop-types'
  3. import Todo from './Todo'
  4. const TodoList = (props) => {
  5. return (
  6. <ul>
  7. {
  8. props.todos.map((value,index) => {
  9. return <Todo key = {index} {...value} onClick = {() => props.toggleTodo(value.id)} />
  10. })
  11. }
  12. </ul>
  13. )
  14. }
  15. TodoList.propTypes = {
  16. todos: PropTypes.arrayOf(
  17. PropTypes.shape({
  18. id: PropTypes.number.isRequired,
  19. completed: PropTypes.bool.isRequired,
  20. text: PropTypes.string.isRequired
  21. }).isRequired
  22. ).isRequired,
  23. toggleTodo: PropTypes.func.isRequired
  24. }
  25. export default TodoList
  • Todo.jsx
  1. import React from 'react'
  2. import PropTypes from 'prop-types'
  3. const Todo = ({ onClick, completed, text }) => (
  4. <li
  5. onClick={onClick}
  6. style={ {
  7. textDecoration: completed ? 'line-through' : 'none'
  8. }}
  9. >
  10. {text}
  11. </li>
  12. )
  13. Todo.propTypes = {
  14. onClick: PropTypes.func.isRequired,
  15. completed: PropTypes.bool.isRequired,
  16. text: PropTypes.string.isRequired
  17. }
  18. export default Todo

4.4.4 containers文件(容器组件)

注意:本部分涉及 connect() 方法,代码注释中有重要知识点,建议仔细查看。对于connect()本文不做深入探讨,后续会单独成文分析。

  • FilterLink.js
  1. import { connect } from 'react-redux'
  2. import { setVisibilityFilter } from '../actions'
  3. import Link from '../components/Link'
  4. import { createFactory } from 'react'
  5. //mapStateToProps参数中的state是store的state.
  6. // 在容器组件中,通过mapStateToProps方法,在展示组件和store中间传递数据和执行action
  7. // ownProps表示的是组件自身的属性,即父组件传过来的属性
  8. const mapStateToProps = (state, ownProps) => {
  9. return {
  10. active: ownProps.filter === state.setVisibilityFilter
  11. }
  12. }
  13. // ownProps表示的是组件自身的属性,即父组件传过来的属性
  14. const mapDispatchToProps = (dispatch, ownProps) => {
  15. return {
  16. // 这里写方法名,在展示组件中通过这个方法名来执行里面的action派遣函数
  17. onClick: () => {
  18. // 执行setVisibilityFilter这个action
  19. dispatch(setVisibilityFilter(ownProps.filter))
  20. }
  21. }
  22. }
  23. //通过connect让Link组件得以连接store,从store中取得active数据和onClick方法的执行体。
  24. export default connect(
  25. mapStateToProps,
  26. mapDispatchToProps
  27. )(Link)
  28. // //将Link组件的内容放到本页面来结合起来理解,以下代码不是本组件的功能代码
  29. // const Link = ({ active, children, onClick }) => (
  30. // <button
  31. // onClick={onClick}
  32. // disabled={active}
  33. // style={{
  34. // marginLeft: '4px',
  35. // }}
  36. // >
  37. // {children}
  38. // </button>
  39. // )
  40. // Link.propTypes = {
  41. // active: PropTypes.bool.isRequired,
  42. // children: PropTypes.node.isRequired,
  43. // onClick: PropTypes.func.isRequired
  44. // }

建议将容器组件和它对应的展示组件紧密结合起来理解。

  • AddTodo.js
  1. import React from 'react'
  2. import { connect } from 'react-redux'
  3. import { addTodo } from '../actions'
  4. const AddTodo = ({ dispatch }) => {
  5. let input
  6. return (
  7. <div>
  8. <form
  9. onSubmit={e => {
  10. e.preventDefault()
  11. if (!input.value.trim()) {
  12. return
  13. }
  14. dispatch(addTodo(input.value))
  15. input.value = ''
  16. }}
  17. >
  18. <input ref={node => input = node} />
  19. <button type="submit">
  20. Add Todo
  21. </button>
  22. </form>
  23. </div>
  24. )
  25. }
  26. export default connect()(AddTodo);
  • VisibleTodoList.js
  1. import { connect } from 'react-redux'
  2. import { toggleTodo } from '../actions'
  3. import TodoList from '../components/TodoList'
  4. //获取符合条件的todo,
  5. // todos state中的todo数据
  6. // filter state中的过滤条件
  7. const getVisibleTodos = (todos, filter) => {
  8. switch (filter) {
  9. case 'SHOW_COMPLETED':
  10. return todos.filter(t => t.completed)
  11. case 'SHOW_ACTIVE':
  12. return todos.filter(t => !t.completed)
  13. case 'SHOW_ALL':
  14. default:
  15. return todos
  16. }
  17. }
  18. const mapStateToProps = (state) => {
  19. return {
  20. todos: getVisibleTodos(state.todos, state.visibilityFilter)
  21. }
  22. }
  23. const mapDispatchToProps = (dispatch) => {
  24. return {
  25. toggleTodo: (id) => {
  26. dispatch(toggleTodo(id))
  27. }
  28. }
  29. }
  30. export default connect(
  31. mapStateToProps,
  32. mapDispatchToProps
  33. )(TodoList)

4.4.5 reducer文件夹

  • 根reducer/index.js
  1. import { combineReducers } from 'redux'
  2. import todos from './todos'
  3. import visibilityFilter from './visibilityFilter'
  4. // rootReducer 根reducer,把子reducer组合在一起
  5. export default combineReducers({
  6. todos, //子state
  7. visibilityFilter //子state
  8. })
  • todo.js
  1. //这里的state = []为state的当前值
  2. const todos = (state = [], action) => {
  3. switch (action.type) {
  4. case 'ADD_TODO':
  5. return [
  6. ...state, // Object.assign() 新建了一个副本
  7. {
  8. id: action.id,
  9. text: action.text,
  10. completed: false
  11. }
  12. ]
  13. case 'TOGGLE_TODO':
  14. // console.log(state);
  15. return state.map((value,index) => {
  16. return (value.id === action.id) ? {...value,completed:!value.completed} : value;
  17. })
  18. default:
  19. return state;
  20. }
  21. }
  22. export default todos;
  • visibilityFilter.js
  1. const visibilityFilter = (state = 'SHOW_ALL', action) => {
  2. switch (action.type) {
  3. case 'SET_VISIBILITY_FILTER':
  4. return action.filter
  5. default:
  6. return state
  7. }
  8. }
  9. export default visibilityFilter

5.总结

本文,菜鸡本鸡通过一个todo-list实例相对系统的介绍了redux的一些基础概念,基本用法和如何如react进行结合,实现react的功能开发,主要内容包括redux基础,redux于react结合,实例完成步骤,完整代码,项目演示等,比较适合刚接触redux的菜鸟阅读和学习,希望能帮助到有需要的同学。

6 参考资料