02-redux-进阶(中间件)

38 阅读8分钟

Redux 设计

项目 store 设计

├─store
    └─reducer            
    │       article.js
    │       user.js
    │       todo.js
    │       index.js      // 入口文件合并多个 reducer
    ├─action
    │      actionTypes.js // actionType 提示文件
    │      article.js
    │      user.js
    └─index.js           // store 入口文件

Reducer 的分离与合并

  • 随着项目功能变得越来越复杂,需要 Redux 管理的状态也会越来越多

  • 此时,有两种方式来处理状态的更新:

    1. 使用一个 reducer:处理项目中所有状态的更新
    2. 使用多个 reducer:按照项目功能划分,每个功能使用一个 reducer 来处理该功能的状态更新
  • 推荐:👍使用第二种方案(多个 reducer) ,每个 reducer 处理的状态更单一,职责更明确

  • 此时,项目中会有多个 reducer,但是 store 只能接收一个 reducer,因此,需要将多个 reducer 合并为一根 reducer,才能传递给 store

  • 合并方式:使用 Redux 中的 combineReducers 函数

  • 注意:合并后,Redux 的状态会变为一个对象,对象的结构与 combineReducers 函数的参数结构相同

    • 比如,此时 Redux 状态为:{ a: aReducer 处理的状态, b: bReducer 处理的状态 }
  • 注意:虽然在使用 combineReducers 以后,整个 Redux 应用的状态变为了对象,但是,对于每个 reducer 来说,每个 reducer 只负责整个状态中的某一个值。也就是每个 reducer 各司其职,最终,由多个 reducer 合作完成整个应用状态的更新。

    • 也就是:每个reducer只负责整个应用状态中的某一部分,每个 reducer 都很自私只关注自己的数据

    • 举个例子:

      • 登录功能:loginReducer 处理的状态只应该是跟登录相关的状态
      • 个人资料:profileReducer 处理的状态只应该是跟个人资料相关的状态
      • 文章列表、文章详情、文章评论 等
import { combineReducers } from 'redux'
function moneyReducer(state = 1000, action) {
  console.log('reducer执行', action)
  // 处理各种各样的action
  switch (action.type) {
    case 'addOne':
      return state + 1
    case 'subOne':
      return state - 1
    case 'addMore':
      return state + action.payload
    case 'subMore':
      return state - action.payload
    default:
      // 很重要
      return state
  }
}
​
function userReducer(state = { name: 'zs', password: '123456' }, action) {
  if (action.type === 'setName') {
    return {
      ...state,
      name: action.payload
    }
  }
  return state
}
​
// 合并多个reducer
const rootReducer = combineReducers({
  // a 和 b指的就是模块的名字
  money: moneyReducer,
  user: userReducer
})
​
export default rootReducer
​

Action Type 的设计

  • Action Type 指的是:action 对象中 type 属性的值
  • Redux 项目中会多次使用 action type,比如,action 对象、reducer 函数、dispatch(action) 等
  • 目标:集中处理 action type,保持项目中 action type 的一致性

处理方式:

  1. 在 store 目录中创建 actionTypes 目录或者 constants 目录,集中处理

  2. 使用常量来存储 action type

  3. action type 的值采用:'domain/action'(功能/动作)形式,进行分类处理,比如,

    • 计数器:'counter/increment' 表示 Counter 功能中的 increment 动作
    • TodoMVC:'todos/add' 表示 TodoMVC 案例中 add 动作等
    • 登录:login/getCode 表示登录获取验证码的动作;login/submit 表示登录功能
    • 个人信息:profile/get 表示获取个人资料;profile/updateName 表示修改昵称
  4. 将项目中用到 action type 的地方替换为这些常量,从而保持项目中 action type 的一致性

// 👍 通过常量存储字符串,书写时可以有代码提示// todo
export const CHANGE_DONE_TODO = 'todo/changeDoneById'
export const DEL_TODO = 'todo/delById'// user
// ....

Redux 中间件

中间件概述

目标: 能够理解为什么需要 redux 中间件

内容:

默认情况下,Redux 自身只能处理同步数据流。但是在实际项目开发中,状态的更新、获取,通常是使用异步操作来实现。

  • 问题:如何在 Redux 中进行异步操作呢?
  • 回答:通过 Redux 中间件机制来实现。

中间件概念

  • 中间件,可以理解为处理一个功能的中间环节
  • 下图中,自来水从水库到用户家庭中的每一个环节都是一个中间件
  • 中间件的优势:可以串联、组合,在一个项目中使用多个中间件
  • Redux 中间件用来处理 状态 更新,也就是在 状态 更新的过程中,执行一系列的相应操作

image.png

中间件的触发时机

  • Redux 中间件执行时机:在 dispatching action 和 到达 reducer 之间

    • 没有中间件:dispatch(action) => reducer
    • 使用中间件:dispatch(action) => 执行中间件代码 => reducer
  • 原理:封装了 redux 自己的 dispatch 方法

    • 没有中间件:store.dispatch() 就是 Redux 库自己提供的 dispatch 方法,用来发起状态更新
    • 使用中间件:dispatch() 就是 中间件 封装处理后的 dispatch,但是,最终一定会调用 Redux 库自己提供的 dispatch 方法
  • 没有中间件:

image.png

  • 有中间件:

image.png

logger 中间件

  1. 安装:yarn add redux-logger
  2. 导入 import logger from 'redux-logger
  3. 从导入 applyMiddleware 函数
  4. applyMiddleware() 调用作为 createStore 函数的第二个参数
  5. 应用 logger 中间件 applyMiddleware(logger)
  • 调用 store.dispatch() 查看 logger 中间件记录的日志信息
-import { createStore } from 'redux'
+import { createStore, applyMiddleware } from 'redux'
+import logger from 'redux-logger'import rootReducer from './reducer'
​
-const store = createStore(rootReducer)
+const store = createStore(rootReducer, applyMiddleware(logger))

Action处理异步行为报错

  • 静态结构

action/todo.js

export function addTodoCreator() {
  const newTodo = {
    id: Date.now(),
    task: '学习异步Action',
    isDone: false,
  };
  return {
    type: 'todo/add',
    payload: newTodo,
  };
}

reducer/todo.js

 case 'todo/add':
      return {
        ...state,
        list: [{ ...payload }, ...state.list],
      };

Main.js

<button onClick={() => dispatch(addTodoCreator())}>点击异步增加新任务</button>
  • action/todo.js 中使用Promise模拟异步行为
/* 模拟异步API的方法 */
+function loadNewTodoAPI() {
+  const newTodo = {
+    task: '异步返回的任务',
+    id: Date.now(),
+    isDone: false,
+  };
+  console.log('开始请求');
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      console.log('请求成功,2秒后,数据返回');
+      resolve(newTodo);
+    }, 2000);
+  });
+}
​
// 💥💥 默认Action不能有异步行为,React要求是Action只是一个普通的JS对象
// 这行代码会导致报错
export async function addTodoCreator() {
+ const newTodo = await loadNewTodoAPI();
- const newTodo = {id: Date.now(), task: '学习异步Action', isDone: true}
  return {
    type: 'todo/add',
    payload: newTask,
  };
}
​

redux-thunk - 基本使用

redux-thunk 中间件可以处理函数形式的 action。因此,在函数形式的 action 中就可以执行异步操作代码,完成异步操作

  1. 安装:yarn add redux-thunk
  2. 导入 import thunk from 'redux-thunk'
  3. thunk 添加到中间件列表中
  4. 修改 action creator,返回一个异步函数
  • 说明:

    1. 在函数形式的 action 中执行异步操作
    2. 在异步操作成功后,分发 action 更新状态

store/index.js

​
+ import thunk from 'redux-thunk'
// 1. 将 thunk 添加到中间件列表中
- const store = createStore(rootReducer, applyMiddleware( logger))
+ const store = createStore(rootReducer, applyMiddleware(thunk, logger))
export  function addTodoCreator() {     
-   const newTodo = await loadNewTodoAPI();
-   return {
-     type: 'todo/add',
-     payload: newTodo,     
-   }; 
​
    // 🔑 2.1 返回一个异步函数
+   return async (dispatch) => {    
+       const newTodo = await loadNewTodoAPI();    
        // 2.2🔑 函数形参的获取到被拦截的dispatch,再次发起dispatch(action)
+       dispatch({ type: 'todo/add', payload: newTodo });
+   };
}
    
// 其他示例代码不变

使用 redux-thunk 中间件前后对比

  1. 不使用 redux-thunk 中间件:
// 1 dispatch 分发动作
dispatch({ type: 'INCREMENT', payload: 2 })
// 2 action 达到 reducer,调用 reducer
reducer(10, { type: 'INCREMENT', payload: 2 })// 得到新的状态值:12
// 3 因为 Redux 中的状态更新了,所以,导致了组件重新渲染,组件中渲染出来的值:12
  1. 使用 redux-thunk 中间件:
// 函数形式的 action
const incrementAsync = () => {  
    // 注意:此处返回的是函数,而不是一个对象  
    return (dispatch, getState) => {  
        // 执行异步操作的代码了    
        setTimeout(() => {      
            dispatch({ type: 'INCREMENT', payload: 2 })    
        }, 1000)    
        // 1 首先执行异步操作,比如,发请求获取个人资料    
        // 2 在异步操作完成后,继续调用 dispatch 分发状态    
        // 3 然后,redux 中就有了这个状态数据了  
    }
}
​
// 0 处理异步操作
// 目的:仅仅是给你一个地方,可以让你写 异步代码;也可以认为是这个函数形式的 action 帮你消化掉了 异步操作
dispatch(incrementAsync())
// 1 dispatch 分发动作dispatch({ type: 'INCREMENT', payload: 2 })
// 2 action 达到 reducer,调用 reducer// reducer(10, { type: 'INCREMENT', payload: 2 })// 得到新的状态值:12
// 3 因为 Redux 中的状态更新了,所以,导致了组件重新渲染,组件中渲染出来的值:12

redux-thunk - 中间件原理

function createThunkMiddleware(extraArgument) {  
    // Redux 中间件的写法:
    const myMiddleware = store => next => action => { 
        /* 此处写 中间件 的代码 */ 
    }  
    return ({ dispatch, getState }) => (next) => (action) => {    
        // redux-thunk 的核心代码:    
        // 判断 action 的类型是不是函数    
        // 如果是函数,就调用该函数(action),并且传入了 dispatch 和 getState    
        if (typeof action === 'function') {      
             action(dispatch, getState, extraArgument);  
            return
        }        
        // 如果不是函数,就调用下一个中间件(next),将 action 传递过去    
        // 如果没有其他中间件,那么,此处的 next 指的就是:Redux 自己的 dispatch 方法 
        return next(action);  
    };
}

redux-devtools-extension 的使用

目标:开发react项目时,通过chrome开发者工具调试跟踪redux状态

步骤

  1. 通过包管理器在项目中安装 yarn add redux-devtools-extension
  2. store/index.js中进行配置和导入
  3. 安装chrome浏览器插件 Redux_DevTools
  4. 启动react项目,打开 chrome 开发者工具,测试

文档 redux-devtools-exension

import { createStore, applyMiddleware } from 'redux';
+import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(  reducer,
+  composeWithDevTools(    
+       applyMiddleware(...middleware)
+  )
);

案例-极客园首页

安装依赖

yarn add axios redux redux-thunk redux-devtools-extension react-redux

需求列表:

01-极客园案例-首页-函数组件拆分

02-极客园案例-Redux仓库设计

03-极客园案例-thunk中间件和调试工具配置

04-极客园案例-频道获取异-异步Action处理

05-极客园案例-点击频道高亮

06-极客园案例-ActionType封装

07-极客园案例-获取新闻列表

复用组件逻辑的三种方式

  1. HOC
  2. 自定义hooks
  3. renderProps
function Header() {
  const [mouse, setMouse] = useState({ x: 0, y: 0 });
  const handleMouseMove = (e) => {
    setMouse({ x: e.clientX, y: e.clientY });
  };
​
  return (
    <div onMouseMove={handleMouseMove}>
      <h1>
        x: {mouse.x} - y: {mouse.y}
      </h1>
    </div>
  );
}
​
​
function Main({ mouse, handleMouseMove }) {
  return (
    <div onMouseMove={handleMouseMove}>
      我是
      <h1>
        Main- {mouse.x} -{mouse.y}
      </h1>
    </div>
  );
}
​
function Footer({ mouse, handleMouseMove }) {
  return (
    <h1 onMouseMove={handleMouseMove}>
      我是Footer - {mouse.x} - {mouse.y}
    </h1>
  );
}
​
function Header({ mouse, handleMouseMove }) {
  return (
    <div onMouseMove={handleMouseMove}>
      <h1>
        x: {mouse.x} - y: {mouse.y}
      </h1>
    </div>
  );
}
​
​

\