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 (
{/* 容器组件 /}){/ 容器组件 /} {/ 展示组件 */}
} export default AppFooter.jsx
import React from 'react' import FilterLink from '../containers/FilterLink' import { VisibilityFilters } from '../actions' //无状态组件,这种写法初学者可能难以理解,可以先补习下ES6,等价于 //function Footer(){ // return (
XXX) //} const Footer = () => (Show:) export default FooterAll Active Completed 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} ) }
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 (
-
{
props.todos.map((value,index) => {
return <Todo key = {index} {...value} onClick = {() => props.toggleTodo(value.id)} />
})
}
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 }) => (
- {text} )
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} // // )
// 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 (
<form onSubmit={e => { e.preventDefault() if (!input.value.trim()) { return } dispatch(addTodo(input.value)) input.value = '' }} > <input ref={node => input = node} />) }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的菜鸟阅读和学习,希望能帮助到有需要的同学。
6 参考资料
本文同步分享在 博客“CherishTheYouth”(CNBlog)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。