【Redux】实战,冲ovo!

286 阅读8分钟

引言

大家可能都知道Redux是用来做全局状态管理的一个库,通常和React放在一起使用。但是为什么需要全局的状态管理呢?

设想一下,现在你的项目非常大,组件有非常多个,每个组件之间需要通讯。如果利用普通的方式,父子之间通讯会显得很麻烦,因为有很多的组件并不是父子的关系,可能他们是祖孙关系,可能是兄弟关系。在这种情况下,如果需要传递状态,则需要传递好几层,用消息订阅发布又显得比较复杂。这时候就可以用到Redux在负责全局的状态管理。

Redux是一个独立于React的库,React本身并没有像Vuex这样的状态管理插件。

本文会从Redux的基础开始,逐步地讲解Redux在日常项目中的使用,本文还会用Redux来实现一个简单的评论小Demo。

基础概念

什么是Redux?

image.png

官网上说了 Redux is a predictable state container for JS Apps 也就是为应用程序提供可管理状态容器的一个js库,简单地来说就是“全局状态管理”。

基本原理

在聊Redux的基本原理之前,我们先来理清楚几个概念:

概念描述
storeredux的核心,可以理解为电脑的CPU,协调各个硬件工作
view我们自己写的组件,也就是我们看到的页面,在页面中会发生各种事件,事件会产生一个dispatch,类似于我们的输入设备:键盘、鼠标等
action creator创建action交给reducer运行,他们创建的一个个action就像是一条条指令,通知计算机工作
reducer真正执行操作的地方,会返回操作完成的值

在知道了上述几个基本概念之后,我们再看一下Redux的基本执行流程

image.png

在我们再项目中引入了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

处理一下项目的默认文件

image.png

正如你想的一样,红色的文件代表需要删除的文件,绿色的是新增的文件,灰色的是进行修改的文件

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'

官方www.tailwindcss.cn/docs/guides…

使用纯react实现

在使用redux之前,我们先尝试用原生react来实现组件之间的通信

快速地搭建起页面

image.png 新建三个组件

image.png

修改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传来的两个参数prevStateaction,通过判断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
)

如果此时我们启动项目,就会看到如下报错!

image.png

别紧张,这是因为我们还没有注入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中的同步方法改为一步方法就可以看到结果了