星巴克项目实战第二弹: Redux 初体验, 最简单的项目与最难知识点, 你应该知道的Redux !

840 阅读8分钟

前言

本篇文章在尚未完善的星巴克App开发中使用了redux, 尽管在小项目中使用redux 确实有些牛刀杀鸡的感觉, 但是小项目的结构更为清晰简单, 是最适合我们用来给redux 的学习练手的, 如果你对redux 不太熟悉,这篇文章或许能解答你的一些疑问

这也是笔者写下这篇文章的目的, 一来是为了帮助自己理清redux 的相关知识点, 二来是希望广大读者朋友在看到这篇文章之后能获得一些启发与帮助

项目

项目详情大家可以自行查看我的上一篇文章, 页面详情几乎没有改动, 仅仅使用redux 取代了React 自带的useState 来管理数据, 这里就不再赘述项目了

后续若有更新, 将会有新的文章给大家带来更详细的分享

前期准备

在项目中需要安装一些包 :

  • npm i redux react-redux
  • npm i redux-thunk

浏览器中需要安装扩展 :

  • Redux DevTools

image.png

ps : 尽量使用Chrome

Redux

作用

当然是管理数据 !

在使用redux 之前, 我们通常使用useState 来管理数据, 再通过props 来进行父子组件或爷孙组件之间的数据传递
通过对子组件传入data 以及setData 的方法, 我们对数据进行读取和修改

但是

当数据需要在非父子组件之间传递时, 使用props 将成为不可能, 这个时候Redux 站了出来
它给我们提供了一种数据管理方式, 即为我们搭建了一个数据仓库, 这个数据仓库由一个主仓库和若干个子仓库组成

数据的传递在仓库和仓库之间完成, 需要用的时候就从仓库中取出来, 数据若在其中一个子仓库中被修改, 修改后的结果也会被同步到总仓库和每一个子仓库 !

也就是说在一个组件中被修改的数据, 通过Redux 可以向所有使用过该数据的组件同步数据 ! 不需要再向之前一样一层一层的报告, 大大提高的数据传输的效率和安全性 !

Bildschirmfoto-2017-12-01-um-08.53.32.png

仓库结构

主仓库

src 目录下, 其中包括index.jsreducer.js , 下面将对它们进行详细介绍

1657977746006.png

index.js

  1. createStore : 创建一个新的仓库
  2. compose : 可以用来组合中间件
  3. applyMiddleware : 中间件
  4. thunk : 延迟计算
  5. composeEnhancers : 当浏览器中存在Redux DevTools 时将前者赋值, 否则使用默认compose
import { createStore, compose, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

const store = createStore(reducer, composeEnhancers(
    applyMiddleware(thunk)
))

export default store

store : 传入 reducer 作为数据, 再结合中间件作为管理数据的各项配置
可简单理解为 : 仓库中的货物和处理货物的工具
当有多个中间件需要使用时, 可以修改代码为 :

const store = createStore(reducer,
    compose(
        composeEnhancers(applyMiddleware(thunk)),  
        applyMiddleware(thunk),
        // 其他中间件
    )
)

reducer.js

import { combineReducers } from 'redux'
import { reducer as homeReducer } from '@/pages/Home/store/index'

export default combineReducers({
    home: homeReducer
})

reducer.js 中的代码较为简单, 大致可以理解为 : 将子仓库中的数据传过来, 合并到主仓库中

如果需要添加多个子仓库时, 可以这样修改代码 :

import { combineReducers } from 'redux'
import { reducer as homeReducer } from '@/pages/Home/store/index'
import { reducer as accountReducer } from '@/pages/Account/store/index' // 导入

export default combineReducers({
    home: homeReducer, 
    account: accountReducer, // 添加
})

结合上面, 在index.js 中将store 输入, 这样就将主仓库搭建好了

子仓库

哪里需要数据, 哪里就有子仓库

  • 一般来说, 我们会在每一个独立的页面中使用一个子仓库来管理数据, 页面内的数据交流则通过props 传值进行

下图为src/pages/Home 首页的store :

image.png

可以看到, 子仓库中存在四个js 文件, 下面我们将一个一个分析

  1. index.js
import * as actionCreator from './actionCreator'
import * as actionTypes from './common'
import reducer from './reducer'

export {
    actionCreator,
    actionTypes,
    reducer
}

index.js 中的代码十分简短, 可以理解为 : 将其他三个文件中的内容整合, 然后整体向外输出

  1. reducer.js
import { actionTypes } from './index'

const defaultState = {
    adsList: [],
    goodsList: [],
    cultureList: []
}

export default (state = defaultState, action) => {
    switch (action.type) {
        case actionTypes.SET_ADS_LIST:
            return {
                ...state,
                adsList: action.data
            }
        case actionTypes.SET_GOODS_LIST:
            return {
                ...state,
                goodsList: action.data
            }
        case actionTypes.SET_CULTURE_LIST:
            return {
                ...state,
                cultureList: action.data
            }
        default:
            return state
    }
}

reducer.js 用来储存数据, 并规定传过来的数据按什么方式处理 (这里是将更新数据, 也可以在 case 中进行其他各项操作)
它会向外输出一个reducer 对象, 该对象接受两个参数 state (数据), action (对象), 通过actiontypestate 中相应的数据进行操作

其中, action 对象包含两个属性 : typedata

  1. type : 相当于 Key , 用于标识该 action 是谁的 action, 下面 switch 选择中将会用到, 是谁的数据, 就将该数据给谁
  2. data : 需要在匹配后, 给出的数据

简单的来说, 它的作用为 : 存储和修改数据

  1. actionCreator.js
import {
    getAdsRequest,
    getGoodsRequest,
    getCultureRequest
} from '@/api/request'
import { actionTypes } from './index'

// 1
const changeAdsList = (data) => ({
    type: actionTypes.SET_ADS_LIST,
    data
})
const changeGoodsList = (data) => ({
    type: actionTypes.SET_GOODS_LIST,
    data
})
const changeCultureList = (data) => ({
    type: actionTypes.SET_CULTURE_LIST,
    data
})
// ------------------- 我是分割线 -------------------
// 2
export const getAdsList = () => {
    return (dispatch) => {
        getAdsRequest().then(data => {
            dispatch(changeAdsList(data.ad))
        })
    }
}
export const getGoodsList = () => {
    return (dispatch) => {
        getGoodsRequest().then(data => {
            dispatch(changeGoodsList(data.goods))
        })
    }
}
export const getCultureList = () => {
    return (dispatch) => {
        getCultureRequest().then(data => {
            dispatch(changeCultureList(data.culture))
        })
    }
}

这里通过分割线, 将代码分成上面的第 1 部分和下面的第 2 部分

1 部分为同步代码, 用于创建 action 对象
2 部分为异步代码, 用于请求数据, 并 dispatch 一个 action 对象, 即将 action 对象发送出去

ps : 一般来说 , API 的请求调用都在 actionCreator.js 中进行

  1. common.js
export const SET_ADS_LIST = 'SET_ADS_LIST'
export const SET_GOODS_LIST = 'SET_GOODS_LIST'
export const SET_CULTURE_LIST = 'SET_CULTURE_LIST'

common.js 中的代码也十分简洁, 用于声明常量, 集中到 common 文件中更方便全局的调用和管理

至此, 子仓库的创建也完成了......

Redux 使用

首先, 我们会在全局引入一个 Provider (这里是在 mian.jsx 下) , 并作为最外层组件, 将所有组件包裹起来, 传入参数 store = {store}, 这意味着在全局引入 store, 并使用它

image.png

  1. 紧接着, 因为我们需要在 Home 页面中使用数据(子仓库建立在 Home 目录下), 所以来到pages/Home/index.js 中使用数据
  2. 一般来说, 我们对数据的操作包括读数据和写数据, 下面将会在这两个方面对代码进行分析
  3. 先来看整体 :

image.png

  1. 在头部引入 connect , 用于给当前页面组件传参
  2. connect()() 函数柯里化
  3. connect 的第一个括号内接受要传的参数
  4. mapStateToProps : 将数据传给当前页面组件
  5. mapDispatchToProps : 将操作数据的方法传给当前页面组件
  6. mapStateToPropsmapDispatchToProps 中的属性将会合并到 props 中, 我们只需要在 Home 函数体中解构即可
  • 这里可以对代码进行优化, 最后一行代码在输出 Home 时包裹一个 React.memo(Home) ,像这样 :
export default connect(mapStateToProps, mapDispatchToProps)(React.memo(Home))

在分析 Home 中的代码之前, 我们先来看这两个函数究竟做了什么

const mapStateToProps = (state) => {
  return ({
    ads: state.home.adsList,
    goods: state.home.goodsList,
    culture: state.home.cultureList
  })
}
const mapDispatchToProps = (dispatch) => {
  return {
    getAdsListDispatch() {
      dispatch(actionCreator.getAdsList())
    },
    getGoodsListDispatch() {
      dispatch(actionCreator.getGoodsList())
    },
    getCultureListDispatch() {
      dispatch(actionCreator.getCultureList())
    }
  }
}
  1. mapStateToProps 中 : 我们传入参数 state , 即全部数据, 再将我们所需要的数据从 state 取出来 {ads, goods, culture} , 把它们封装成一个对象, 最后返回到 props
  2. mapDispatchToProps 中 : 我们传入 dispatch 函数, 它将会接收一个 action 对象, 并传出, 根据 action.type 的值来修改对应的 data
  3. 可以看到, 对于不同的数据我们都使用了不同的函数来对 dispatch 进行封装, 并传入对应的 action 对象, 以达到更新数据的效果

分析了数据的读写后, 我们再来看看它们中的属性是如何被使用的吧 !

const { ads, goods, culture } = props
const { getAdsListDispatch, getGoodsListDispatch, getCultureListDispatch } = props
useEffect(() => {
  getAdsListDispatch()
  getGoodsListDispatch()
  getCultureListDispatch()
}, [])

通过 props 将我们需要的属性全部解构出来, 再通过 useEffect 生命周期函数, 等页面挂在后调用 Dispatch 函数, 达到更新数据的目的
在此之后, 我们就可以将获取到的数据分发到不同的组件 :

<Wrapper>
  <Ads ads={ads}/>
  <Card />
  <Club />
  <Goods goods={goods} />
  <Culture culture={culture} />
</Wrapper>

后记 :

  • 看到这里的朋友, 相信你对 redux 的框架已经有了基本的理解, 其实文中还有许多较为复杂的难点, 在这里并没有做出详细的解释 , 如 :
    • dispatch 的具体作用原理
    • store 目录下的文件之间的内在联系
    • 数据完整的请求和传递路径 (它是从哪个文件哪行代码开始, 经过了哪些文件, 最后到达了哪里)

等等这些, 都需要对 redux 有着足够深厚的理解才能弄的明白, 讲得清楚
想要将 redux 完全掌握的读者朋友可以自行进行学习和思考, 这里笔者也需要更多的学习来对其进行更进一步的理解

项目详情可以看笔者的上一篇文章 : juejin.cn/post/711594… (已完成redux 更新)

希望这篇文章能给同在学习 redux 的读者朋友带来一些启发和思考, 如果你有收获的话, 请给我一个大大的赞 ! 谢谢~😊😊