- A+
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, //子state visibilityFilter //子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这个action dispatch(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.jsx import App from './App'; //创建store,将根Reducer传入store中。redux应用只有一个单一的store const 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() { //返回一个对象,这个对象就是context return { 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'; //redux import { 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 }) => ( <li onClick={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这个action dispatch(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 input return ( <div> <form onSubmit={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, //子state visibilityFilter //子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.filter default: return state } } export default visibilityFilter
5.总结
本文,菜鸡本鸡通过一个todo-list实例相对系统的介绍了redux的一些基础概念,基本用法和如何如react进行结合,实现react的功能开发,主要内容包括redux基础,redux于react结合,实例完成步骤,完整代码,项目演示等,比较适合刚接触redux的菜鸟阅读和学习,希望能帮助到有需要的同学。