06-Redux

Redux 是 React 最常用的集中状态管理工具,类似于 Vue 中的 Pinia(Vuex),可以独立于框架运行

作用:通过集中管理的方式管理应用的状态

ZXMzaGtkR3pEeXdSeE5MR3JXZUppUGowTk44RnhubGZNVVhwZ1cwPQ==

为什么要使用 Redux?

  1. 独立于组件,无视组件之间的层级关系,简化通信问题
  2. 单项数据流清晰,易于定位 bug
  3. 调试工具配套良好,方便调试

1. 快速体验

1. 实现计数器

需求:不和任何框架绑定,不使用任何构建工具,使用纯 Redux 实现计数器

VmRZeTBISmhPWmt2RkR6M0Z1YkxpZmowTk44RnhubGZNVVhwZ1czL1RBPT0=
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>
</body>
<script src="https://cdn.jsdelivr.net/npm/redux@4.2.0/dist/redux.min.js"></script>
<script>
  // 1. 定义 reducer 函数
  // 内部主要的工作是根据不同的 action 返回不同的 state
  function counterReducer(state = {count: 0}, action) {
    switch (action.type) {
      case 'INCREMENT':
        return {count: state.count + 1}
      case 'DECREMENT':
        return {count: state.count - 1}
      default:
        return state
    }
  }

  // 2. 使用 createStore 函数生成 store 实例
  const store = Redux.createStore(counterReducer)

  // 3. 订阅数据变化(一旦变化,可以得到通知)
  store.subscribe(() => {
    console.log(store.getState())
    document.getElementById('count').innerText = store.getState().count
  })
  // 增
  const inBtn = document.getElementById('increment')
  inBtn.addEventListener('click', () => {
    store.dispatch({
      type: 'INCREMENT'
    })
  })
  // 减
  const dBtn = document.getElementById('decrement')
  dBtn.addEventListener('click', () => {
    store.dispatch({
      type: 'DECREMENT'
    })
  })
</script>
</html>











 



 











 


 





 






 






2. Redux数据流架构

Redux 难点:理解它对于数据修改的规则,下图动态展示了在整个数据的修改中,数据的流向

ek92ZEY4YVlwc3k2V1VaOTRPRVJ5UGowTk44RnhubGZNVVhwZ1czL1RBPT0=

为了职责清晰,Redux 代码被分为三个核心的概念:

  1. state:一个对象,存放着需要管理的数据
  2. action:一个对象,用来描述怎样改数据
  3. reducer:一个函数,根据 action 的描述更新 state

2. 环境准备

Redux 虽然是一个框架无关可以独立运行的插件,但是社区通常把它与 React 绑定在一起使用。计数器案例(Redux + React)

1. 配套工具

在 React 中使用 Redux,官方要求安装两个其他插件:Redux Toolkitreact-redux

  1. Redux Toolkit(RTK):官方推荐编写 Redux 逻辑的方式,是一套工具的集合集,简化书写方式
  2. react-redux:用来链接 Redux 和 React 组件的中间件
eThLWng2R1lqTWkzK0xOM2k4d3NvL2owTk44RnhubGZNVVhwZ1cwPQ==

2. 基础环境

# 1. 使用 CRA 快速创建 React 项目
npx create-react-app react-redux

# 2. 安装配套工具
npm i @reduxjs/toolkit react-redux

# 3. 启动项目
npm run start

3. store目录结构设计

  1. 通常集中状态管理部分,都会单独创建一个单独的 store 目录
  2. 应用通常会有很多个子 store 模块,创建 modules 目录,在内部编写业务分类的子 store
  3. store 中的入口文件 index.js
    • 作用:是组合 modules 中所有的子模块,并导出 store
d1RRNEtiZm1Pa2trQ3grTkZRRm5zZmowTk44RnhubGZNVVhwZ1czL1RBPT0=

3. 实现counter

1. 整体路径熟悉

QUdnMFg3b2lrL0o1eEczcVdyM0ozZmowTk44RnhubGZNVVhwZ1czL1RBPT0=

2. counterStore.js

import {createSlice} from '@reduxjs/toolkit'

const counterStore = createSlice({
  // 模块名称独一无二
  name: 'counter',
  // 初始数据
  initialState: {
    count: 1
  },

  // 修改数据的同步方法
  reducers: {
    increment(state) {
      state.count++
    },
    decrement(state) {
      state.count--
    }
  }
})

// 结构出 actionCreater
const {increment, decrement} = counterStore.actions

// 获取 reducer 函数
const counterReducer = counterStore.reducer

// 导出
export {increment, decrement}
export default counterReducer


 



















 


 




import {configureStore} from '@reduxjs/toolkit'

import counterReducer from './modules/counterStore'

export default configureStore({
  reducer: {
    // 注册子模块
    counter: counterReducer
  }
})


 

 





3. 为React注入store

react-redux 负责把 Redux 和 React 链接起来,内置 Provider 组件,通过 store 参数把创建好的 store 实例注入到应用中,链接正式建立

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

// 导入 store
import store from './store'
// 导入 store 提供组件 Provider
import {Provider} from 'react-redux'

ReactDOM.createRoot(document.getElementById('root')).render(
    // 提供 store 数据
    <Provider store={store}>
      <App/>
    </Provider>
)

// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(
//   <React.StrictMode>
//     <App />
//   </React.StrictMode>
// );

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();







 

 



 















4. 组件中使用store中数据

在 React 组件中使用 store 中的数据,需要用到一个钩子函数 useSelector

  • 作用:把 store 中的数据映射到组件中

5. 组件中修改store中数据

React 组件中修改 store 中的数据,需要借助另外一个 hook 函数 useDispatch()

  • 作用:生成提交 action 对象的 dispatch 函数
import './App.scss'

import {useDispatch, useSelector} from 'react-redux';
// 导入创建 action 对象的方法
import {decrement, increment} from './store/modules/counterStore';

function App() {
  const {count} = useSelector(state => state.counter);
  // 得到 dispatch 函数
  const dispatch = useDispatch();
  return (
      <div className="App">
        {/* 调用 dispatch 提交 action 对象 */}
        <button onClick={() => dispatch(decrement())}>-</button>
        <span>{count}</span>
        <button onClick={() => dispatch(increment())}>+</button>
      </div>
  );
}

export default App







 

 



 

 





4. 提交action传参

需求:组件中两个按钮 add to 10add to 20 直接把 count 值修改到对应的数字,count 值在组件中传递过去的,需要在提交 action 时传递参数

NmRhWVY4My9ZeXlHUElXVHN3WnRodmowTk44RnhubGZNVVhwZ1czL1RBPT0=

实现方式:在 reducers 的同步修改方法中添加 action 对象参数,在调用 actionCreater 时传递参数,参数会被传递到 action 对象 payload 属性上

import {createSlice} from "@reduxjs/toolkit"

const counterStore = createSlice({
  name: 'counter',
  // 初始化state
  initialState: {
    count: 0
  },
  // 修改状态的方法,同步方法,支持直接修改
  reducers: {
    inscrement(state) {
      state.count++
    },
    decrement(state) {
      state.count--
    },
    addToNum(state, action) {
      state.count = action.payload
    }
  }
})

// 解构出来 actionCreater 函数
const {inscrement, decrement, addToNum} = counterStore.actions
// 获取 reducer
const reducer = counterStore.reducer

// 以按需导出的方式导出 actionCreater
export {inscrement, decrement, addToNum}
// 以默认导出的方式导出 reducer
export default reducer
















 
 
 




 







import './App.scss'

import {useDispatch, useSelector} from 'react-redux';
// 导入创建 action 对象的方法
import {addToNum, decrement, inscrement} from './store/modules/counterStore';

function App() {
  const {count} = useSelector(state => state.counter)
  const dispatch = useDispatch()
  // 使用useEffect触发异步请求执行
  return (
      <div className="App">
        <button onClick={() => dispatch(decrement())}>-</button>
        {count}
        <button onClick={() => dispatch(inscrement())}>+</button>
        <button onClick={() => dispatch(addToNum(10))}>add To 10</button>
        <button onClick={() => dispatch(addToNum(20))}>add To 20</button>
      </div>
  )
}

export default App















 
 





5. 异步action处理

需求:

V3ZnTXFlc2NuVFhxa3hqd0trZ25sdmowTk44RnhubGZNVVhwZ1cwPQ==
  1. 创建 store 的写法保持不变,配置好同步修改状态的方法
  2. 单独封装一个函数,在函数内部 return 一个新函数,在新函数中
    1. 封装异步请求获取数据
    2. 调用同步 actionCreater 传入异步数据生成一个 action 对象,并使用 dispatch 提交
  3. 组件中 dispatch 的写法保持不变
import {createSlice} from "@reduxjs/toolkit"
import axios from "axios"

const channelStore = createSlice({
  name: 'channel',
  initialState: {
    channelList: []
  },
  reducers: {
    setChannels(state, action) {
      state.channelList = action.payload
    }
  }
})

// 异步请求部分
const {setChannels} = channelStore.actions

const fetchChannlList = () => {
  return async (dispatch) => {
    // const res = await axios.get('http://geek.itheima.net/v1_0/channels')
    const res = {
      "data": {
        "channels": [
          {"id": 0, "name": "推荐"}, {"id": 1, "name": "html"}, {"id": 2, "name": "开发者资讯"},
          {"id": 4, "name": "c++"}, {"id": 6, "name": "css"}, {"id": 7, "name": "数据库"},
          {"id": 8, "name": "区块链"}, {"id": 9, "name": "go"}, {"id": 10, "name": "产品"},
          {"id": 11, "name": "后端"}, {"id": 12, "name": "linux"}, {"id": 13, "name": "人工智能"},
          {"id": 14, "name": "php"}, {"id": 15, "name": "javascript"}, {"id": 16, "name": "架构"},
          {"id": 17, "name": "前端"}, {"id": 18, "name": "python"}, {"id": 19, "name": "java"},
          {"id": 20, "name": "算法"}, {"id": 21, "name": "面试"}, {"id": 22, "name": "科技动态"},
          {"id": 23, "name": "js"}, {"id": 24, "name": "设计"}, {"id": 25, "name": "数码产品"},
          {"id": 26, "name": "软件测试"}
        ]
      }, "message": "OK"
    }
    dispatch(setChannels(res.data.channels))
  }
}

export {fetchChannlList}

const reducer = channelStore.reducer

export default reducer



















 
















 








import {configureStore} from '@reduxjs/toolkit'

import channelReducer from './modules/channelStore'

export default configureStore({
  reducer: {
    // 注册子模块
    channel: channelReducer
  }
})







 


import {createContext, useContext, useEffect, useRef, useState} from 'react'
import './App.scss'

import {useDispatch, useSelector} from 'react-redux';
import {fetchChannlList} from './store/modules/channelStore'

function App() {
  const {channelList} = useSelector(state => state.channel)
  const dispatch = useDispatch()

  // 使用 useEffect 触发异步请求执行
  useEffect(() => {
    dispatch(fetchChannlList())
  }, [dispatch])

  return (
      <div className="App">
        <ul>
          {channelList.map(item => <li key={item.id}>{item.name}</li>)}
        </ul>
      </div>
  )
}

export default App












 












6. Redux调试devtools

Redux 官方提供了针对于 Redux 的调试工具,支持实时 state 信息展示,action 提交信息查看等

Vnp2MVdRTnE4cGg3VHk1aWU0azFvZmowTk44RnhubGZNVVhwZ1czL1RBPT0=

7. 美团案例

1. 案例演示

L1B4ZjM3Q1Y3Q0JQcGtZbFRVdGJ3L2owTk44RnhubGZNVVhwZ1czL1RBPT0=

基本开发思路:使用 RTK(Redux Toolkit)来管理应用状态,组件负责数据渲染和 dispatch action

2. 环境

# 1. 克隆项目到本地(内置了基础静态组件和模版)
git clone http://git.itcast.cn/heimaqianduan/redux-meituan.git

# 2. 安装所有依赖
npm i

# 3. 启动 mock 服务(内置了 json-server)
npm run serve

# 4. 启动前端服务
npm run start

3. 分类和商品列表渲染

Slk4bGdNL2ZRTklEUHVIUzFQbTI0UGowTk44RnhubGZNVVhwZ1czL1RBPT0=
  1. 编写 store 逻辑
// 编写 store
import { createSlice } from "@reduxjs/toolkit"
import axios from "axios"

const foodsStore = createSlice({
  name: 'foods',
  initialState: {
    // 商品列表
    foodsList: []
  },
  reducers: {
    // 更改商品列表
    setFoodsList (state, action) {
      state.foodsList = action.payload
    }
  }
})

// 异步获取部分
const { setFoodsList } = foodsStore.actions
const fetchFoodsList = () => {
  return async (dispatch) => {
    // 编写异步逻辑
    const res = await axios.get('http://localhost:3004/takeaway')
    // 调用 dispatch 函数提交 action
    dispatch(setFoodsList(res.data))
  }
}

export { fetchFoodsList }

const reducer = foodsStore.reducer

export default reducer
  1. 组件使用 store 数据
// 省略部分代码
import { useDispatch, useSelector } from 'react-redux'
import { fetchFoodsList } from './store/modules/takeaway'
import { useEffect } from 'react'

const App = () => {
  // 触发 action 执行
  // 1. useDispatch -> dispatch 2. actionCreater导入进来 3.useEffect
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch(fetchFoodsList())
  }, [dispatch])

  return (
    <div className="home">
      {/* 导航 */}
      <NavBar />

      {/* 内容 */}
      <div className="content-wrap">
        <div className="content">
          <Menu />
          <div className="list-content">
            <div className="goods-list">
              {/* 外卖商品列表 */}
              {foodsList.map(item => {
                return (
                  <FoodsCategory
                    key={item.tag}
                    // 列表标题
                    name={item.name}
                    // 列表商品
                    foods={item.foods}
                  />
                )
              })}
            </div>
          </div>
        </div>
      </div>
      {/* 购物车 */}
      <Cart />
    </div>
  )
}

export default App

4. 点击分类激活交互

c0RscVB2T1NSeXZMcjVIc2RDY2V4ZmowTk44RnhubGZNVVhwZ1czL1RBPT0=
  1. 编写 store 逻辑
// 编写 store
import { createSlice } from "@reduxjs/toolkit"
import axios from "axios"

const foodsStore = createSlice({
  name: 'foods',
  initialState: {
    // 菜单激活下标值
    activeIndex: 0
  },
  reducers: {
    // 更改 activeIndex
    changeActiveIndex (state, action) {
      state.activeIndex = action.payload
    }
  }
})

// 导出
const { changeActiveIndex } = foodsStore.actions

export { changeActiveIndex }

const reducer = foodsStore.reducer

export default reducer
  1. 编写组件逻辑
const Menu = () => {
  const { foodsList, activeIndex } = useSelector(state => state.foods)
  const dispatch = useDispatch()
  const menus = foodsList.map(item => ({ tag: item.tag, name: item.name }))
  return (
    <nav className="list-menu">
      {/* 添加 active 类名会变成激活状态 */}
      {menus.map((item, index) => {
      return (
        <div
          // 提交 action 切换激活index
          onClick={() => dispatch(changeActiveIndex(index))}
          key={item.tag}
          // 动态控制 active 显示
          className={classNames(
            'list-menu-item',
            activeIndex === index && 'active'
          )}
          >
          {item.name}
        </div>
      )
    })}
    </nav>
  )
}

5. 商品列表切换显示

dnpaWDhLd1JWb0hZR2hGU2svd3NpdmowTk44RnhubGZNVVhwZ1czL1RBPT0=
<div className="list-content">
  <div className="goods-list">
    {/* 外卖商品列表 */}
    {foodsList.map((item, index) => {
      return (
        activeIndex === index && <FoodsCategory
          key={item.tag}
          // 列表标题
          name={item.name}
          // 列表商品
          foods={item.foods}
        />
      )
    })}
  </div>
</div>

6. 添加购物车

V3lyRll5QXZGeTJxWXpDNS9kdWJOdmowTk44RnhubGZNVVhwZ1cwPQ==
  1. 编写 store 逻辑
import { createSlice } from "@reduxjs/toolkit"
import axios from "axios"

const foodsStore = createSlice({
  name: 'foods',
  reducers: {
    // 添加购物车
    addCart (state, action) {
      // 是否添加过?以 action.payload.id 去 cartList 中匹配,匹配到了,添加过
      const item = state.cartList.find(item => item.id === action.payload.id)
      if (item) {
        item.count++
      } else {
        state.cartList.push(action.payload)
      }
    }
  }
})

// 导出 actionCreater
const { addCart } = foodsStore.actions

export { addCart }

const reducer = foodsStore.reducer

export default reducer
  1. 编写组件逻辑
<div className="goods-count">
  {/* 添加商品 */}
  <span
    className="plus"
    onClick={() => dispatch(addCart({
    id,
    picture,
    name,
    unit,
    description,
    food_tag_list,
    month_saled,
    like_ratio_desc,
    price,
    tag,
    count
  }))}></span>
</div>

7. 统计区域

N2x5dndUQ2JtamhzOU4yK2J0ZmZQdmowTk44RnhubGZNVVhwZ1czL1RBPT0=
  1. 基于 store 中的 cartList 的 length 渲染数量
  2. 基于 store 中的 cartList 累加 price * count
  3. 购物车 cartList 的 length 不为零则高亮
// 计算总价
const totalPrice = cartList.reduce((a, c) => a + c.price * c.count, 0)

{/* fill 添加 fill 类名购物车高亮*/}
{/* 购物车数量 */}
<div onClick={onShow} className={classNames('icon', cartList.length > 0 && 'fill')}>
  {cartList.length > 0 && <div className="cartCornerMark">{cartList.length}</div>}
</div>

8. 购物车列表功能

R21TWjFPamlMVHdocDhUS2pvdGVLL2owTk44RnhubGZNVVhwZ1czL1RBPT0=
  1. 控制列表渲染
const Cart = () => {
  return (
    <div className="cartContainer">
      {/* 添加 visible 类名,div 会显示出来 */}
      <div className={classNames('cartPanel', 'visible')}>
        {/* 购物车列表 */}
        <div className="scrollArea">
          {cartList.map(item => {
            return (
              <div className="cartItem" key={item.id}>
                <img className="shopPic" src={item.picture} alt="" />
                <div className="main">
                  <div className="skuInfo">
                    <div className="name">{item.name}</div>
                  </div>
                  <div className="payableAmount">
                    <span className="yuan">¥</span>
                    <span className="price">{item.price}</span>
                  </div>
                </div>
                <div className="skuBtnWrapper btnGroup">
                  {/* 数量组件 */}
                  <Count
                    count={item.count}
                  />
                </div>
              </div>
            )
          })}
        </div>
      </div>
    </div>
  )
}

export default Cart
  1. 购物车增减逻辑实现
// count 增
increCount (state, action) {
  // 关键点:找到当前要修改谁的 count id
  const item = state.cartList.find(item => item.id === action.payload.id)
  item.count++
},
// count 减
decreCount (state, action) {
  // 关键点:找到当前要修改谁的 count id
  const item = state.cartList.find(item => item.id === action.payload.id)
  if (item.count === 0) {
    return
  }
  item.count--
}
<div className="skuBtnWrapper btnGroup">
  {/* 数量组件 */}
  <Count
    count={item.count}
    onPlus={() => dispatch(increCount({ id: item.id }))}
    onMinus={() => dispatch(decreCount({ id: item.id }))}
    />
</div>
  1. 清空购物车实现
// 清除购物车
clearCart (state) {
  state.cartList = []
}
<div className="header">
  <span className="text">购物车</span>
  <span
    className="clearCart"
    onClick={() => dispatch(clearCart())}>
    清空购物车
  </span>
</div>

9. 购物车显示和隐藏

dzFEZC9pQ0J6US9oZG9jOGozblUrdmowTk44RnhubGZNVVhwZ1czL1RBPT0=
// 控制购物车打开关闭的状态
const [visible, setVisible] = useState(false)

const onShow = () => {
  if (cartList.length > 0) {
    setVisible(true)
  }
}


{/* 遮罩层,添加 visible 类名可以显示出来 */}
<div
	className={
     classNames('cartOverlay', visible && 'visible')
  }
	onClick={() => setVisible(false)}
/>