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的菜鸟阅读和学习,希望能帮助到有需要的同学。