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 管理的状态也会越来越多
-
此时,有两种方式来处理状态的更新:
- 使用一个 reducer:处理项目中所有状态的更新
- 使用多个 reducer:按照项目功能划分,每个功能使用一个 reducer 来处理该功能的状态更新
-
推荐:👍使用第二种方案(多个 reducer) ,每个 reducer 处理的状态更单一,职责更明确
-
此时,项目中会有多个 reducer,但是 store 只能接收一个 reducer,因此,需要将多个 reducer 合并为一根 reducer,才能传递给 store
-
合并方式:使用 Redux 中的
combineReducers
函数 -
注意:合并后,Redux 的状态会变为一个对象,对象的结构与 combineReducers 函数的参数结构相同
- 比如,此时 Redux 状态为:
{ a: aReducer 处理的状态, b: bReducer 处理的状态 }
- 比如,此时 Redux 状态为:
-
注意:虽然在使用
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 的一致性
处理方式:
-
在 store 目录中创建
actionTypes
目录或者constants
目录,集中处理 -
使用常量来存储 action type
-
action type 的值采用:
'domain/action'(功能/动作)形式
,进行分类处理,比如,- 计数器:
'counter/increment'
表示 Counter 功能中的 increment 动作 - TodoMVC:
'todos/add'
表示 TodoMVC 案例中 add 动作等 - 登录:
login/getCode
表示登录获取验证码的动作;login/submit
表示登录功能 - 个人信息:
profile/get
表示获取个人资料;profile/updateName
表示修改昵称
- 计数器:
-
将项目中用到 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 中间件用来处理 状态 更新,也就是在 状态 更新的过程中,执行一系列的相应操作
中间件的触发时机
-
Redux 中间件执行时机:在 dispatching action 和 到达 reducer 之间。
- 没有中间件:
dispatch(action) => reducer
- 使用中间件:
dispatch(action) => 执行中间件代码 => reducer
- 没有中间件:
-
原理:封装了 redux 自己的 dispatch 方法
- 没有中间件:
store.dispatch()
就是 Redux 库自己提供的 dispatch 方法,用来发起状态更新 - 使用中间件:
dispatch()
就是 中间件 封装处理后的 dispatch,但是,最终一定会调用 Redux 库自己提供的 dispatch 方法
- 没有中间件:
-
没有中间件:
- 有中间件:
logger 中间件
- 安装:
yarn add redux-logger
- 导入
import logger from 'redux-logger
- 从导入
applyMiddleware
函数 - 将
applyMiddleware()
调用作为 createStore 函数的第二个参数 - 应用 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 中就可以执行异步操作代码,完成异步操作
- 安装:
yarn add redux-thunk
- 导入
import thunk from 'redux-thunk'
- 将
thunk
添加到中间件列表中 - 修改 action creator,返回一个异步函数
-
说明:
- 在函数形式的 action 中执行异步操作
- 在异步操作成功后,分发 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 中间件前后对比
- 不使用 redux-thunk 中间件:
// 1 dispatch 分发动作
dispatch({ type: 'INCREMENT', payload: 2 })
// 2 action 达到 reducer,调用 reducer
reducer(10, { type: 'INCREMENT', payload: 2 })// 得到新的状态值:12
// 3 因为 Redux 中的状态更新了,所以,导致了组件重新渲染,组件中渲染出来的值:12
- 使用 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状态
步骤
:
- 通过包管理器在项目中安装
yarn add redux-devtools-extension
- 在
store/index.js
中进行配置和导入 - 安装chrome浏览器插件 Redux_DevTools
- 启动react项目,打开 chrome 开发者工具,测试
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-极客园案例-获取新闻列表
复用组件逻辑的三种方式
- HOC
- 自定义hooks
- 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>
);
}
\