引言
大家可能都知道Redux是用来做全局状态管理的一个库,通常和React放在一起使用。但是为什么需要全局的状态管理呢?
设想一下,现在你的项目非常大,组件有非常多个,每个组件之间需要通讯。如果利用普通的方式,父子之间通讯会显得很麻烦,因为有很多的组件并不是父子的关系,可能他们是祖孙关系,可能是兄弟关系。在这种情况下,如果需要传递状态,则需要传递好几层,用消息订阅发布又显得比较复杂。这时候就可以用到Redux在负责全局的状态管理。
Redux是一个独立于React的库,React本身并没有像Vuex这样的状态管理插件。
本文会从Redux的基础开始,逐步地讲解Redux在日常项目中的使用,本文还会用Redux来实现一个简单的评论小Demo。
基础概念
什么是Redux?
官网上说了 Redux is a predictable state container for JS Apps 也就是为应用程序提供可管理状态容器的一个js库,简单地来说就是“全局状态管理”。
基本原理
在聊Redux的基本原理之前,我们先来理清楚几个概念:
| 概念 | 描述 |
|---|---|
| store | redux的核心,可以理解为电脑的CPU,协调各个硬件工作 |
| view | 我们自己写的组件,也就是我们看到的页面,在页面中会发生各种事件,事件会产生一个dispatch,类似于我们的输入设备:键盘、鼠标等 |
| action creator | 创建action交给reducer运行,他们创建的一个个action就像是一条条指令,通知计算机工作 |
| reducer | 真正执行操作的地方,会返回操作完成的值 |
在知道了上述几个基本概念之后,我们再看一下Redux的基本执行流程
在我们再项目中引入了redux之后,redux会帮助我们管理项目中的种种状态,store这个CPU会将各种信息交给我们的组件,在用户需要用到redux中的存储的信息的时候,就可以通过store.state这个对象来获取想要的信息。但是信息肯定不光光是要展示的,肯定是要操作的,这个使用组件就可以通过调用store.dispatch()这个方法来通知store我想要处理一下store中的state,store.dispatch()这个方法接受一个action类型的参数,action就是一个普通的对象有,有两个属性{type: '', data: ''},store接受到这个dispatch之后就会将这个action和之前的状态交给reducer进行修改,如果是初始化的状态,reducer收到的action就是{type: '@@INIT[随机字符串]', data: undefined},经过reducer处理完后,store就会拿到最新的数据,存到state中。
⚠️注意: Redux只会保存更新状态,并不会主动地渲染页面!
Redux实战
简单的聊了一下Redux的基础原理和执行流程,现在开始就来做一个简单的基于Redux的小项目吧
创建项目
使用create-react-app来创建一个react脚手架项目
$ npx create-react-app redux-comment
处理一下项目的默认文件
正如你想的一样,红色的文件代表需要删除的文件,绿色的是新增的文件,灰色的是进行修改的文件
src/App.jsx
/*
* @Author: Mujey 🦦
* @Date: 2021-08-16 14:20:20
*/
import React, { Component } from 'react'
class App extends Component {
render() {
return (
<div>
<h1>Initial</h1>
</div>
)
}
}
export default App
src/index.js
/*
* @Author: Mujey 🦦
* @Date: 2021-08-16 13:48:30
*/
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
public/index.html
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<title>评论With Redux</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
为了方便代码的书写,我准备使用UI框架来快速地搭建页面,你们也可以使用less或sass来写样式,或者使用其他的UI框架,我这里使用tailwind.css作为该项目的UI框架
在项目中引入tailwind CSS
安装tailwindCSS
$ yarn add tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
安装和配置 CRACO
$ yarn add @craco/craco
将项目改成使用CRACO启动
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},
新建craco.config.js
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 13:51:34
*/
module.exports = {
style: {
postcss: {
plugins: [require('tailwindcss'), require('autoprefixer')],
},
},
}
生成配置文件
$ npx tailwindcss init
修改配置文件
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 13:51:48
*/
module.exports = {
purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
darkMode: 'media', // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
新建文件src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
在index.js中引入
import ReactDOM from 'react-dom'
import App from './App'
import './index.css'
使用纯react实现
在使用redux之前,我们先尝试用原生react来实现组件之间的通信
快速地搭建起页面
新建三个组件
修改App.jsx
import React, { Component } from 'react'
import List from './components/List'
import Publish from './components/Publish'
class App extends Component {
state = {
comments: [],
}
addComment = obj => {
const { comments } = this.state
this.setState({
comments: [obj, ...comments],
})
}
render() {
const { comments } = this.state
return (
<div className="container-lg mx-auto w-8/12 h-screen py-16 antialiased">
<Publish addComment={this.addComment} />
<List comments={comments} />
</div>
)
}
}
export default App
src/components/Publish.jsx
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 15:10:27
*/
import React, { Component } from 'react'
import dateformat from 'dateformat'
import { nanoid } from 'nanoid'
class Publish extends Component {
handlePublish = e => {
if (e.keyCode !== 13) return
const { value } = this.inputNode
const now = new Date()
const obj = {
value,
time: dateformat(now, 'yyyy年MM月dd日 HH:MM:ss'),
key: nanoid(),
}
// 调用props下的addComment添加评论到App组件的state中
this.props.addComment(obj)
this.inputNode.value = ''
}
render() {
return (
<div className="w-full h-28 p-4">
<h1>发布评论</h1>
<div className="flex border-b-2 border-indigo-500 py-1 pl-3">
<input
ref={e => (this.inputNode = e)}
onKeyUp={this.handlePublish}
type="text"
className="flex-1 outline-none "
autoFocus
/>
<button
onClick={this.handlePublish}
className="bg-gradient-to-r from-indigo-300 to-indigo-400 px-9 py-1 text-white text-sm"
>
发布
</button>
</div>
</div>
)
}
}
export default Publish
src/components/List.jsx
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 15:10:37
*/
import React, { Component } from 'react'
import Item from './Item'
class List extends Component {
render() {
const { comments } = this.props
return (
<div>
{comments.map(item => (
<Item comment={item} key={item.key} />
))}
</div>
)
}
}
export default List
最后是src/components/Item.jsx
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 15:10:21
*/
import React, { Component } from 'react'
class Item extends Component {
render() {
const {
comment: { value, time },
} = this.props
return (
<div className="flex items-center my-4 shadow-sm">
<div>{value}</div>
<div className="pl-4 text-sm text-gray-600 font-thin ml-auto">
{time}
</div>
</div>
)
}
}
export default Item
在上面的代码中,Publish组件会接收到一个App传递过来的方法addComment通过调用这个方法,并且传递数据,可以将数据存入App组件的state中,App组件会将数据交给List组件,List组件会做一次循环,然后循环渲染Item组讲将数据渲染出来。
在上述的例子中,交互还是比较单一的,就是子组件调用父组件的方法给父组件传递参数,父组件给子组件传递参数。但是如果层级比较深的情况下,就不是那么容易维护的了。接下来我们就来引入Reudx做全局的状态管理。
Redux版
首先还是安装redux依赖
$ yarn add redux
配置redux
在src目录中创建redux文件夹,用来存放所有关于redux的内容,在redux文件夹中新建一些文件
└── redux
├── actions
│ └── comment.js
├── reducers
│ ├── comment.js
│ └── index.js
└── store.js
src/redux/store.js
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 16:30:42
*/
import { createStore } from 'redux'
import reducers from './reducers'
export default createStore(reducers)
在store.js中通过reudx提供的createStore函数创建的一个store并向外暴露
src/redux/reducers/comment.js
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 16:31:30
*/
const initialState = []
export default function commentReducer(prevState = initialState, action) {
const { type, data } = action
switch (type) {
case 'addComment':
return [data, ...prevState]
default:
return prevState
}
}
这是一个reducer的基本形态,他会接收到store传来的两个参数prevState和action,通过判断action.type决定该方法的返回值
src/redux/reducers/index.js
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 16:35:36
*/
import { combineReducers } from 'redux'
import comment from './comment'
export default combineReducers({
comment,
})
combineReducers是将多个reducers合成一个对象,但是我们现在只有一个reducer,如果你有多个reducer,只需要继续网参数的对象中添加即可。
最后,来写一下action
src/redux/actions/comment.js
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 16:31:22
*/
export const addComment = data => ({ type: 'addComment', data })
是的,你没有看错,所谓的action就是专门用来创建action的方法而已,我们这里用的是同步aciton,后面会提到异步action
这样,reudx就可以投入开发中去使用了,让我们去修改我们的组件,不要让他父子之间再通讯了。
将状态都保存到store中
修改src/App.jsx,去除多余的代码
import React, { Component } from 'react'
import List from './components/List'
import Publish from './components/Publish'
class App extends Component {
render() {
return (
<div className="container-lg mx-auto w-8/12 h-screen py-16 antialiased">
<Publish />
<List />
</div>
)
}
}
export default App
修改src/components/Publis.jsx,引入store和action,使用store.dispatch()方法来发布评论
import store from '../redux/store'
import { addComment } from '../redux/actions/comment'
class Publish extends Component {
handlePublish = e => {
if (e.keyCode !== 13) return
const { value } = this.inputNode
const now = new Date()
const obj = {
value,
time: dateformat(now, 'yyyy年MM月dd日 HH:MM:ss'),
key: nanoid(),
}
// 调用store.dispatch将评论存到store中
store.dispatch(addComment(obj))
this.inputNode.value = ''
}
render() {
//......
最后,修改src/components/List.jsx,从store中取到数据
import store from '../redux/store'
class List extends Component {
render() {
const comments = store.getState().comment
// ........
到这一步,数据就已经存到redux里了,但是页面上应该是看不出任何变化的,原因也很简单,我们之前也提到过,就是redux不会取管页面的渲染。
好在redux提供了subscribe这个api,他可以在每次数据更新后通知我们,我们手动地去渲染页面。
修改src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import store from './redux/store'
import App from './App'
import './index.css'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
store.subscribe(() => {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
})
到目前为止,Redux版的案例已经完成了,Redux可以帮助我们管理全局的状态,很棒。但是在这之上有没有什么可以优化的空间呢?那自然是有的,由于开发人员使用redux的频率太高了,react就集成了一块redux的react版本:react-redux,接下来我们就用react-redux来重构我们的项目。
react-redux版
首先,说一个比较重要的概念,在react-redux中,他不允许我们的组件直接操作redux,而是提供了容易组件来帮助我们操作Redux,那什么是容器组件呢?简单地来说就是react-redux帮助我们和redux建立连接的地方。有点晦涩?那我们就直接开始吧。
还是同样的第一步:安装依赖
$ yarn add react-redux
改造项目目录结构
我们需要创建一个文件夹——containers,并且讲所有将来会操作redux的组件丢进去
├── components
│ └── Item.jsx
├── containers
│ ├── List.jsx
│ └── Publish.jsx
不要忘了修改组件引用的路径哦!
connect
从react-redux中引入connect函数,所有容器组件暴露的不再是之前的类,而是连接过后的容器组件
src/containers/Publis.jsx
import { connect } from 'react-redux'
// ...
class Publish extends Component {/* ... */}
export default connect()(Publish)
来解释一下connect这个函数,这个函数是用来连接UI组件和容器组件的,相比大家现在已经对什么是UI组件,什么事容器组件有了一个概念了吧,UI组件就是我们手写的,布局页面的组件;而容器组件则是使用connect函数自动生成的可以操作redux的组件。
connect函数需要接受两个参数,第一个参数是一个函数,用来向UI组件的porps上挂载state,而第二个参数可以是函数也可以是对象,用来向UI组件的props上挂载dispatch事件,下面是一段伪代码
function mapStateToProps(state) {
return {
// ...
}
}
function mapDispatchToProps(dispatch) {
return {
add() {
dispatch(/* action */)
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ComponentUI)
这就是一个connect函数本来的样子,但是我们可以简写,在connect的api中,它的第二个参数可以简写成一个对象。
import { add } from '../redux/action/...'
export default connect(state => (data: state.data), {add})(ComponentUI)
ok! 这就是connect函数日常使用中的样子了,让我们现在来改写我们的代码
首先我们修改一下src/containers/List.jsx让他成为下面这个样子
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 15:10:37
*/
import React, { Component } from 'react'
import { connect } from 'react-redux'
import Item from '../components/Item'
class List extends Component {
render() {
const { comments } = this.props
return (
<div>
{comments.map(item => (
<Item comment={item} key={item.key} />
))}
</div>
)
}
}
export default connect(state => ({ comments: state.comment }))(List)
接下来是src/containers/Publish.jsx
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 15:10:27
*/
import React, { Component } from 'react'
import dateformat from 'dateformat'
import { nanoid } from 'nanoid'
import { connect } from 'react-redux'
import { addComment } from '../redux/actions/comment'
class PublishUI extends Component {
handlePublish = e => {
if (e.keyCode !== 13) return
const { value } = this.inputNode
const now = new Date()
const obj = {
value,
time: dateformat(now, 'yyyy年MM月dd日 HH:MM:ss'),
key: nanoid(),
}
// new code ⏎ 调用props中的addComment方法
this.props.addComment(obj)
this.inputNode.value = ''
}
render() {
return (
<div className="w-full h-28 p-4">
<h1>发布评论</h1>
<div className="flex border-b-2 border-indigo-500 py-1 pl-3">
<input
ref={e => (this.inputNode = e)}
onKeyUp={this.handlePublish}
type="text"
className="flex-1 outline-none "
autoFocus
/>
<button
onClick={this.handlePublish}
className="bg-gradient-to-r from-indigo-300 to-indigo-400 px-9 py-1 text-white text-sm"
>
发布
</button>
</div>
</div>
)
}
}
export default connect(state => ({ comments: state.comment }), { addComment })(
PublishUI
)
如果此时我们启动项目,就会看到如下报错!
别紧张,这是因为我们还没有注入store,接下来我们修改src/index.js
/*
* @Author: Mujey 🦦
* @Date: 2021-08-16 13:48:30
*/
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import store from './redux/store'
import App from './App'
import './index.css'
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
)
// store.subscribe(() => {
// ReactDOM.render(
// <React.StrictMode>
// <App />
// </React.StrictMode>,
// document.getElementById('root')
// )
// })
还记得我们之前手动渲染的DOM嘛,现在不需要了,react-redux会自动帮助我们渲染,我们只需要借助Provider组件将store提供给任何需要store的地方就行了
目前为止,整个案例已经完成了。
最后我们再来做一些完善,就是之前说的一步action,在创建aciton的时候,如果我们返回一个对象,那么就是一个同步action,如果我们返回的是一个函数,就会转换成为异步action
异步action
安装redux-thunk
$ yarn add redux-thunk
修改src/redux/store.js引入redux-thunk中间件
/*
* @Author: Mujey 🦦
* @Date: 2021-08-17 16:30:42
*/
import { createStore, applyMiddleware } from 'redux'
import reducers from './reducers'
import thunk from 'redux-thunk'
export default createStore(reducers, applyMiddleware(thunk))
在actions中添加一个addCommentAsync方法
export const addCommentAsync = data => {
return dispatch => {
setTimeout(() => {
dispatch({ type: 'addComment', data })
}, 800)
}
}
在这里使用setTimeout造成了异步处理,模拟网络请求,在返回的函数中可以接受到一个dispath函数,通过这个函数出发一个同步的action进行处理数据。
再将Pulish.jsx中的同步方法改为一步方法就可以看到结果了