1.前言
在之前的博客中,我写了一篇关于todo-list实现的博客,一步一步详细的记录了如何使用基础的React知识实现一个React单页面应用,通过该篇文章,能够对React入门开发有一个直观的认识和粗浅的理解。
近期,个人学习了一下Redux,又将该项目使用 React+Redux的方式进行了实现。本片内容记录以下实践的过程。通过本实例,可以学习到:
- Redux的核心思想;
- Redux的三大概念;
- React+Redux的开发方法和流程;
2.项目演示!

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方法创建,
createStore(reducer, [initState])//reducer表示一个根reducer,initState是一个初始化状态
store提供方法来操作state
- 维持应用的 state;
- 提供
getState()方法获取 state; - 提供
dispatch(action)方法更新 state; - 通过
subscribe(listener)注册监听器,在state状体发生变化后会被调用。 - 通过
subscribe(listener)返回的函数注销监听器。
3.2.2 Action
action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。通过 store.dispatch() 将 action 传到 store。如果有数据需要添加,在action中一并传过来。
action需要action创建函数进行创建,如下是一个action创建函数:
/** action 类型*/export const ADD_TODO = 'ADD_TODO';export const TOGGLE_TODO = 'TOGGLE_TODO'export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'/** 其它的常量*/export const VisibilityFilters = {SHOW_ALL: 'SHOW_ALL',SHOW_COMPLETED: 'SHOW_COMPLETED',SHOW_ACTIVE: 'SHOW_ACTIVE'}/** action 创建函数*/export function addTodo(text) {return { type: ADD_TODO, text }}export function toggleTodo(index) {return { type: TOGGLE_TODO, index }}export function setVisibilityFilter(filter) {return { type: SET_VISIBILITY_FILTER, filter }}
返回一个对象,改对象由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:
import { combineReducers } from 'redux'import todos from './todos'import visibilityFilter from './visibilityFilter'//根reducer// rootReducer 根reducer,把子reducer组合在一起export default combineReducers({todos, //子statevisibilityFilter //子state})
子reducer:
//这里的state = []为state的当前值const todos = (state = [], action) => {switch (action.type) {case 'ADD_TODO':return [...state, // Object.assign() 新建了一个副本{id: action.id,text: action.text,completed: false}]case 'TOGGLE_TODO':// console.log(state);return state.map((value,index) => {return (value.id === action.id) ? {...value,completed:!value.completed} : value;})default:return state;}}export default todos;
3.2.4 数据流

3.3 展示组件和容器组件
3.3.1 展示组件和容器组件分离
本部分在笔者尚未深入研究,在此给出redux作者写的深度解析文章链接及网上的译文链接,读者可自行查看。
原文链接:展示组件和容器组件相分离 译文链接:展示组件和容器组件相分离
3.3.2 展示组件和容器组件比较
| 展示组件 | 容器组件 | |
|---|---|---|
| 作用 | 描述如何展示骨架、样式 | 描述如何运行(数据获取、状态更新) |
| 直接使用Redux | 否 | 是 |
| 数据来源 | props | 监听Redux state |
| 数据修改 | 从props调用回调函数 | 向Redux派发action |
| 调用方式 | 手动 | 通常由React Redux生成 |
大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和 Redux store 连接起来。
React Redux 的使用 connect() 方法来生成容器组件。
import { connect } from 'react-redux'import { setVisibilityFilter } from '../actions'import Link from '../components/Link'//mapStateToProps参数中的state是store的state.// 在容器组件中,通过mapStateToProps方法,在展示组件和store中间传递数据和执行action// ownProps表示的是组件自身的属性,即父组件传过来的属性const mapStateToProps = (state, ownProps) => {return {active: ownProps.filter === state.setVisibilityFilter}}// ownProps表示的是组件自身的属性,即父组件传过来的属性const mapDispatchToProps = (dispatch, ownProps) => {return {// 这里写方法名,在展示组件中通过这个方法名来执行里面的action派遣函数onClick: () => {// 执行setVisibilityFilter这个actiondispatch(setVisibilityFilter(ownProps.filter))}}}//通过connect让Link组件得以连接store,从store中取得active数据和onClick方法的执行体。export default connect(mapStateToProps,mapDispatchToProps)(Link)
connect() 中最核心的两个方法是:mapActionToProps 和 mapDispatchToProps ,通过容器组件,可以在 展示组件和 store之间传递数据和执行 action。
4.基于Redux的React项目实战
4.1 目录结构
根据Redux的几大组成部分,在进行开发时,将在之前基础的React开发模式下,增加几个文件夹,形成新的开发目录结构,具体目录结构如下图:
│ App.css│ App.js│ App.test.js│ index.css│ index.js│ logo.svg│ readme.txt│ serviceWorker.js│ setupTests.js├─actions├─components├─containers└─reducers

如图,在之前的结构下,新增了 actions 、reducers 、containers 这三个文件夹。
4.2 配置React-Redux开发环境
4.2.1 步骤
在建好文件目录后就可以开始进行开发了,由于是基于Redux做React开发,所以首先一步当然需要把Redux的开发环境配置一下。
- 安装
react-redux包
npm install --save react-redux
- 编写入口文件 index.js
前文讲到,redux使用一个唯一的 store 来对项目进行状态管理,那么首先我们需要创建这个 store ,并将这个 store 作为一个属性,传递给下级子组件。
具体代码如下:
import React from 'react';import ReactDOM, { render } from 'react-dom';//redux ----------------------------------------------------import { Provider } from 'react-redux';import { createStore } from 'redux';import { rootReducer } from './reducers';//引入项目根组件App.jsximport App from './App';//创建store,将根Reducer传入store中。redux应用只有一个单一的storeconst store = createStore(rootReducer);render(<Provider store = {store}><App /></Provider>,document.getElementById('id'))
如上代码所示,使用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属性
export default class Provider extends Component {getChildContext() {//返回一个对象,这个对象就是contextreturn { store: this.store }}constructor(props, context) {super(props, context)this.store = props.store}render() {return Children.only(this.props.children)}}Provider.propTypes = {store: storeShape.isRequired,children: PropTypes.element.isRequired}Provider.childContextTypes = {store: storeShape.isRequired}
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
import React from 'react';import ReactDOM, { render } from 'react-dom';import './index.css';import App from './components/App';//reduximport { Provider } from 'react-redux';import { createStore } from 'redux';import rootReducer from './reducers';//创建store,createStore()第一个参数是项目的根reducer,第二个参数是可选的,用于设置state的初始状态const store = createStore(rootReducer);render(// Provider组件包裹在跟组件的外层,使所有的子组件都可以拿到state.// 它接受store作为props,然后通过context往下传,这样react中任何组件// 都可以通过context获取store.<Provider store = {store}>{/* App 根组件 */}<App /></Provider>,document.getElementById('root'))
4.4.2 actions文件
- index.js
let nextTodoId = 0;// 定义action 常量 对于小型项目,可以将action常量和action创建函数写在一起,对于复杂的项目,可将action常量和其他的常量抽取出来,放到单独的某个常量文件夹中const ADD_TODO = 'ADD_TODO';const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';const TOGGLE_TODO = 'TOGGLE_TODO';//这里是几个action创建函数,函数里面的对象才是action,返回一个action// text是跟随action传递的数据// 调用 dispatch(addTodo(text)),即代表派遣action,交给reducer处理//action生成函数// 大部分情况下,他简单的从参数中收集信息,组装成一个action对象并返回,// 但对于较为复杂的行为,他往往会容纳较多的业务逻辑与副作用,包括与后端的交互等等。export const addTodo = (text) => {return {type: ADD_TODO,id: nextTodoId ++,text}}export const setVisibilityFilter = (filter) => {return {type: SET_VISIBILITY_FILTER,filter}}export const toggleTodo = (id) => {return {type: TOGGLE_TODO,id}}//三个常量export const VisibilityFilters = {SHOW_ALL: 'SHOW_ALL',SHOW_COMPLETED: 'SHOW_COMPLETED',SHOW_ACTIVE: 'SHOW_ACTIVE'}
4.4.3 components文件(展示组件)
- App.jsx
import React from 'react'import Footer from './Footer'import AddTodo from '../containers/AddTodo'import VisibleTodoList from '../containers/VisibleTodoList'//应用的根组件const App = () => {return (<div>{/* 容器组件 */}<AddTodo />{/* 容器组件 */}<VisibleTodoList />{/* 展示组件 */}<Footer /></div>)}export default App
- Footer.jsx
import React from 'react'import FilterLink from '../containers/FilterLink'import { VisibilityFilters } from '../actions'//无状态组件,这种写法初学者可能难以理解,可以先补习下ES6,等价于//function Footer(){// return (<div>XXX</div>)//}const Footer = () => (<div><span>Show: </span><FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink><FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink><FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink></div>)export default Footer
- Link.jsx
import React from 'react'import PropTypes from 'prop-types'//prop-types是一个组件属性校验包,导入这个包可以数据进行格式等方面的校验const Link = (props) => {return (<button onClick={props.onClick} disabled={props.active} style={{marginLeft:'4px'}}>{props.children}</button>)}Link.propTypes = {active: PropTypes.bool.isRequired,children: PropTypes.node.isRequired,onClick: PropTypes.func.isRequired}export default Link
- TodoList.jsx
import React, { createFactory } from 'react'import PropTypes from 'prop-types'import Todo from './Todo'const TodoList = (props) => {return (<ul>{props.todos.map((value,index) => {return <Todo key = {index} {...value} onClick = {() => props.toggleTodo(value.id)} />})}</ul>)}TodoList.propTypes = {todos: PropTypes.arrayOf(PropTypes.shape({id: PropTypes.number.isRequired,completed: PropTypes.bool.isRequired,text: PropTypes.string.isRequired}).isRequired).isRequired,toggleTodo: PropTypes.func.isRequired}export default TodoList
- Todo.jsx
import React from 'react'import PropTypes from 'prop-types'const Todo = ({ onClick, completed, text }) => (<lionClick={onClick}style={ {textDecoration: completed ? 'line-through' : 'none'}}>{text}</li>)Todo.propTypes = {onClick: PropTypes.func.isRequired,completed: PropTypes.bool.isRequired,text: PropTypes.string.isRequired}export default Todo
4.4.4 containers文件(容器组件)
注意:本部分涉及 connect() 方法,代码注释中有重要知识点,建议仔细查看。对于connect()本文不做深入探讨,后续会单独成文分析。
- FilterLink.js
import { connect } from 'react-redux'import { setVisibilityFilter } from '../actions'import Link from '../components/Link'import { createFactory } from 'react'//mapStateToProps参数中的state是store的state.// 在容器组件中,通过mapStateToProps方法,在展示组件和store中间传递数据和执行action// ownProps表示的是组件自身的属性,即父组件传过来的属性const mapStateToProps = (state, ownProps) => {return {active: ownProps.filter === state.setVisibilityFilter}}// ownProps表示的是组件自身的属性,即父组件传过来的属性const mapDispatchToProps = (dispatch, ownProps) => {return {// 这里写方法名,在展示组件中通过这个方法名来执行里面的action派遣函数onClick: () => {// 执行setVisibilityFilter这个actiondispatch(setVisibilityFilter(ownProps.filter))}}}//通过connect让Link组件得以连接store,从store中取得active数据和onClick方法的执行体。export default connect(mapStateToProps,mapDispatchToProps)(Link)// //将Link组件的内容放到本页面来结合起来理解,以下代码不是本组件的功能代码// const Link = ({ active, children, onClick }) => (// <button// onClick={onClick}// disabled={active}// style={{// marginLeft: '4px',// }}// >// {children}// </button>// )// Link.propTypes = {// active: PropTypes.bool.isRequired,// children: PropTypes.node.isRequired,// onClick: PropTypes.func.isRequired// }
建议将容器组件和它对应的展示组件紧密结合起来理解。
- AddTodo.js
import React from 'react'import { connect } from 'react-redux'import { addTodo } from '../actions'const AddTodo = ({ dispatch }) => {let inputreturn (<div><formonSubmit={e => {e.preventDefault()if (!input.value.trim()) {return}dispatch(addTodo(input.value))input.value = ''}}><input ref={node => input = node} /><button type="submit">Add Todo</button></form></div>)}export default connect()(AddTodo);
- VisibleTodoList.js
import { connect } from 'react-redux'import { toggleTodo } from '../actions'import TodoList from '../components/TodoList'//获取符合条件的todo,// todos state中的todo数据// filter state中的过滤条件const getVisibleTodos = (todos, filter) => {switch (filter) {case 'SHOW_COMPLETED':return todos.filter(t => t.completed)case 'SHOW_ACTIVE':return todos.filter(t => !t.completed)case 'SHOW_ALL':default:return todos}}const mapStateToProps = (state) => {return {todos: getVisibleTodos(state.todos, state.visibilityFilter)}}const mapDispatchToProps = (dispatch) => {return {toggleTodo: (id) => {dispatch(toggleTodo(id))}}}export default connect(mapStateToProps,mapDispatchToProps)(TodoList)
4.4.5 reducer文件夹
- 根reducer/index.js
import { combineReducers } from 'redux'import todos from './todos'import visibilityFilter from './visibilityFilter'// rootReducer 根reducer,把子reducer组合在一起export default combineReducers({todos, //子statevisibilityFilter //子state})
- todo.js
//这里的state = []为state的当前值const todos = (state = [], action) => {switch (action.type) {case 'ADD_TODO':return [...state, // Object.assign() 新建了一个副本{id: action.id,text: action.text,completed: false}]case 'TOGGLE_TODO':// console.log(state);return state.map((value,index) => {return (value.id === action.id) ? {...value,completed:!value.completed} : value;})default:return state;}}export default todos;
- visibilityFilter.js
const visibilityFilter = (state = 'SHOW_ALL', action) => {switch (action.type) {case 'SET_VISIBILITY_FILTER':return action.filterdefault:return state}}export default visibilityFilter
5.总结
本文,菜鸡本鸡通过一个todo-list实例相对系统的介绍了redux的一些基础概念,基本用法和如何如react进行结合,实现react的功能开发,主要内容包括redux基础,redux于react结合,实例完成步骤,完整代码,项目演示等,比较适合刚接触redux的菜鸟阅读和学习,希望能帮助到有需要的同学。
