当前位置: 代码迷 >> 综合 >> React(五)——Redux
  详细解决方案

React(五)——Redux

热度:48   发布时间:2023-10-01 00:53:50.0

目录

1.知识点

2.状态(数据)管理

3.Redux——核心概念

3.1state 对象

3.1.1state 是只读的

3.1.2通过纯函数修改 state

3.2Reducer 函数

3.3action 对象

3.4Store 对象

3.4.1Redux.createStore 方法

3.4.2getState() 方法

3.4.3dispatch() 方法

3.4.4action 创建函数

3.4.5subscribe() 方法

4.Redux——工作流

5.Redux——Reducers 分拆与融合

5.1combineReducers() 方法

6.react-redux

6.1安装

6.2 Provider 组件

6.3connect 方法

7.Redux DevTools extension

8.异步 action

8.1package.json 配置

8.2./src/setupProxy.js 配置

9.Middleware

9.1redux.applyMiddleware()

10.redux-chunk

10.1安装

10.2redux-chunk下使用redux-devtools-extension


1.知识点

  • 状态管理器
  • state 对象
  • reducer 纯函数
  • store 对象
  • action 对象
  • combineReducers 方法
  • react-redux
  • provider 组件
  • connect 方法
  • Redux DevTools extension 工具
  • 中间件 - middleware
  • redux-chunk

2.状态(数据)管理

各组件之间,父组件与子组件孙组件或者层次更下级的组件之间 以及无嵌套兄弟级间要维护和交互数据,光通过props进行传递非常的麻烦与复杂。

前端应用越来越复杂,要管理和维护的数据也越来越多,为了有效的管理和维护应用中的各种数据,我们必须有一种强大而有效的数据管理机制,也称为状态管理机制,Redux 就是解决该问题的。

3.Redux——核心概念

Redux 是一个独立的 JavaScript 状态管理库,与非 React 内容之一。可以通过独立的中间件将Redux和React联系起来。

引入Redux的js文件,文件地址参考:https://www.bootcdn.cn/redux/。

理解 Redux 核心几个概念与它们之间的关系

  • state对象
  • reducer纯函数
  • store对象
  • action对象

3.1state 对象

通常我们会把应用中的数据存储到一个对象树(Object Tree) 中进行统一管理,我们把这个对象树称为:state

3.1.1state 是只读的

这里需要注意的是,为了保证数据状态的可维护和测试,不推荐直接修改 state 中的原数据

3.1.2通过纯函数修改 state

通过纯函数修改state。

什么是纯函数?

纯函数

  1. 相同的输入永远返回相同的输出
  2. 不修改函数的输入值
  3. 不依赖外部环境状态
  4. 无任何副作用

使用纯函数的好处

  1. 便于测试
  2. 有利重构

3.2Reducer 函数

function todo(state, action) {switch(action.type) {case 'ADD':return [...state, action.payload];break;case 'REMOVE':return state.filter(v=>v!==action.payload);break;default:return state;}
}

上面的 todo 函数就是 Reducer 函数

  1. 第一个参数是原 state 对象
  2. Reducer 函数不能修改原 state,而应该返回一个新的 state(如,[...state])
  3. 第二参数是一个 action 对象,包含要执行的操作和数据
  4. 如果没有操作匹配,则返回原 state 对象

reducer函数的作用:用于接收原始(上一次)的state,并返回一个新的state数据。

3.3action 对象

我们对 state 的修改是通过 reducer 纯函数来进行的,同时通过传入的 action 来执行具体的操作,action 是一个对象

  • type 属性 : 表示要进行操作的动作类型,增删改查……

  • payload属性 : 操作 state 的同时传入的数据

但是这里需要注意的是,我们不直接去调用 Reducer 函数,而是通过 Store 对象提供的 dispatch 方法来调用,store.dispath()方法:用于提交和修改数据。

3.4Store 对象

为了对 stateReduceraction 进行统一管理和维护,我们需要创建一个 Store 对象。

3.4.1Redux.createStore 方法

  • 通过Redux.createStore()方法创建一个Redux仓库,用于生成和维护数据;
  • 通过reducer函数返回值作为数据(state)来创建仓库;
  • Redux.createStore()会初始化调用一次,返回值为初始化数据;
  • Redux.createStore()的第二个参数是仓库中最开始的初始化数据
let store = Redux.createStore((state, action) => {// ...
}, []);

todo

用户操作数据的 reducer 函数

function todo(){return [1,2,3];
}

[]

初始化的 state

我们也可以使用 es6 的函数参数默认值来对 state 进行初始化

let store = Redux.createStore( (state = [], action) => {// ...
} )

3.4.2getState() 方法

通过 getState 方法,可以获取 Store 中的 state

store.getState();

3.4.3dispatch() 方法

通过 dispatch 方法,可以提交更改。这个方法的调用间接的调用了reducer纯函数

每次调用dispath()方法,就会去执行todo纯函数,然后把上一次的state数据作为参数传给todo函数,然后把dispatch()方法中的对象传给todo纯函数的action对象

store.dispatch({type: 'ADD',payload: 'MT'
})

3.4.4action 创建函数

action 是一个对象,用来在 dispatch 的时候传递动作和数据,我们在实际业务中可能会中许多不同的地方进行同样的操作,这个时候,我们可以创建一个函数用来生成(返回)action对象

function add(payload) {return {type: 'ADD',payload}
}store.dispatch(add('MT'));
store.dispatch(add('Reci'));
...

3.4.5subscribe() 方法

可以通过 subscribe 方法注册监听器(类似事件),每次 dispatch action 的时候都会执行监听函数,该方法返回一个函数,通过该函数可以取消当前监听器

let unsubscribe = sotre.subscribe(function() {console.log(store.getState());
});
unsubscribe();

Redux核心概念中各方法使用示例及详细解释:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Redux</title>
</head><body><!-- 引入redux --><script src="https://cdn.bootcss.com/redux/4.0.5/redux.js"></script><script>console.log(Redux);//创建一个纯函数:用来生成和维护数据/*纯函数第一个参数state对象:1,纯函数的第一个参数是原state对象;2,Reducer纯函数不能直接修改原state对象,而需要返回一个新的state;3,第二个参数是一个action对象,包含要执行的操作和数据4,如果没有操作匹配,就返回原state对象*//*纯函数第二个参数action对象:1,Reducer纯函数的state对象不能直接修改,而是通过action来执行具体的操作(增删改查)2,action是一个对象,type属性表示要操作的动作(增删改查),payload属性表示操作state同时传入的数据3,Reducer纯函数不是直接调用的,而是通过store对象的dispatch()方法进行调用4,action对象有初始值: {type: "@@redux/INITv.c.9.6.j.o"}5,Reducer纯函数会根据每次提交(store.dispatch()的调用)过来的action进行不同的操作*/function todo(state, action) {/*没有调用dispatch()方法,会返回action对象的初始化type即{type: "@@redux/INITv.c.9.6.j.o"};调用后会返回dispatch()方法中传入的对象,如{type: "ADD", payload: "MT"}*/console.log(action);switch (action.type) {case 'ADD'://Reducer纯函数不能修改原始state,所以只能返回修改后的state,如[...state]return [...state, action.payload]case 'DELETE':return [...state].filter(v=>v!==action.payload);case 'UPDATE':return;case 'SELECT':return;//没有任何修改,可以直接返回原始数据default:return state;}return state;}/*1,通过Redux.createStore()方法创建一个Redux仓库,用于生成和维护数据2,Redux.createStore()的第一个参数,是一个reducer纯函数;通过reducer纯函数的返回值作为数据(state)来创建仓库;reducer纯函数的作用是用来接受原始(第一次)的state数据,并返回一个新的state数据3,Redux.createStore()第二个参数是仓库的初始化数据*/let store = Redux.createStore(todo, [1, 2, 3]);//store.getState()获取Store中的stateconsole.log(store);//{dispatch: ?, subscribe: ?, getState: ?, replaceReducer: ?, Symbol(observable): ?}// console.log(store.getState());//[1, 2, 3]//subscribe() 方法/*可以通过 subscribe 方法注册监听器(类似事件),每次 dispatch action 的时候都会执行监听函数,该方法返回一个函数,通过该函数可以取消当前监听器如,可以监听store.getState(),只要state发生更改,就会返回更改的动作和提交的数据,即dispatch()中的对象*/let unsubscribe = store.subscribe(() => {console.log(store.getState());});//store.dispatch()用于提交修改数据,/*1,每次调用store.dispatch()方法时,Redux内部就会去执行reducer纯函数(todo),store会将上一次的数据传递给state,然后把dispatch()方法中的对象传递给action;2,type属性表示要操作的动作(增删改查),payload属性表示操作state同时传入的数据;3,type和payload属性名可以更改,只是约定俗成叫type和payload;*/store.dispatch({type: 'ADD',payload: 'MT'});// console.log(store.getState());//[1, 2, 3, "MT"]//多次调用subscribe()方法都能监听到,每次监听返回更改的动作和提交的数据,即dispatch()中的对象{type: "ADD", payload: "MT"}store.dispatch({type: 'ADD',payload: 'MT'});//subscribe()方法返回一个函数,通过该函数可以取消当前监听器// unsubscribe();store.dispatch({type: 'ADD',payload: 'MT'});//action函数:用于生成特定action对象的函数。每次修改state都要在调用dispatch()方法时传入同样的修改对象,所以可以将此对象进行封装,之后直接调用即可function add(payload) {return {type: 'ADD',payload: payload};}function remove(payload) {return {type: 'DELETE',payload: payload};}store.dispatch(add("Mouse"));store.dispatch(add("Baoge"));store.dispatch(remove("MT"));</script>
</body></html>

4.Redux——工作流

React(五)——Redux

工作流解析:

  1. UI中的数据维护不在UI中进行维护,UI对数据的状态state只有使用权;
  2. 状态state由store仓库进行统一管理。管理时有自己的一套方案;
  3. 要操作或修改数据时,提供访问和修改的方式:首先从Store仓库中去取数据state,修改数据时要提交action的动作,能否更改如何更改由仓库决定,此时仓库提供Reducer函数去修改状态,Reducer纯函数不是直接调用的,而是通过store对象的dispatch()方法进行调用;

5.Redux——Reducers 分拆与融合

当一个应用比较复杂的时候,状态数据也会比较多,如果所有状态都是通过一个 Reducer 纯函数来进行修改(增删改查)的话,那么这个 Reducer 就会变得特别复杂。这个时候,我们就会对这个 Reducer 进行必要的拆分

let datas = {user: {},items: []cart: []
}

我们把上面的 usersitemscart 进行分拆

// user reducer
function user(state = {}, action) {// ...
}
// items reducer
function items(state = [], action) {// ...
}
// cart reducer
function cart(state = [], action) {// ...
}

模拟 combineReducers()方式自己实现Reducer()纯函数的拆分与融合:

多种数据同时操作问题重现

  1. 同时操作三个数据users,items,cart,不进行拆分与融合时:
  2. 每个数据都有不同的行为(增删改查)(user只有更改);
  3. 且每个case中返回值也需要同时控制三种数据,如操作users时,Items和cart保存不变;即其中一种数据改变时其他两种数据都保持不变;
  4. 使用action创建函数,用于分别操作各种数据的不同行为(增删改查)

示例,如以下方法,每次操作某种数据,其他数据都必须跟着改变:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>模拟实现combineReducers()拆分融合函数——问题重现</title>
</head><body><script src="https://cdn.bootcss.com/redux/4.0.5/redux.js"></script><script>//初始化数据都为空let initData = {users: {},items: [],carts: []}//纯函数function reducer(state, action) {switch (action.type) {//如以下方法,每次操作某种数据,其他数据都必须跟着改变case 'USER_EDIT':return {//注意此处key-value格式,且只更改user相关数据,其他数据保持不变users: action.payload,items: state.items,carts: state.carts};case 'ITEM_ADD':return {//只更改items相关数据,其他数据保持不变users: state.users,items: [...state.items,action.payload],carts: state.carts};case 'ITEM_REMOVE':return {//只更改items相关数据,其他数据保持不变users: state.users,items: [...state.items].filter((item)=>item.id!==action.payload),carts: state.carts};case 'CART_ADD':return {//注意此处key-value格式,且只更改user相关数据,其他数据保持不变users: state.users,items: state.items,carts: [...state.carts,action.payload]};case 'CART_REMOVE':return {//注意此处key-value格式,且只更改user相关数据,其他数据保持不变users: state.users,items: state.items,carts: [...state.carts].filter(v=>v.id!==action.payload),};}return state;}//创建store仓库let store = Redux.createStore(reducer, initData);//dispatch()方法//users更改方法function updateUser(payload) {return {type: 'USER_EDIT',payload};}// items增加,删除方法function addItem(payload) {return {type: 'ITEM_ADD',payload};}function removeItem(payload) {return {type: 'ITEM_REMOVE',payload};}// carts增加,删除方法function addCart(payload) {return {type: 'CART_ADD',payload};}function removeCart(payload) {return {type: 'CART_REMOVE',payload};}console.log(store.getState());store.subscribe(()=>{console.log(store.getState());});//第一次只更改usersstore.dispatch(updateUser({ id: 1, username: 'MT' }));//第二次增加itemsstore.dispatch(addItem({id:1,name:'Ipad',price:'$5000'}));store.dispatch(addItem({id:2,name:'Iphone',price:'$6000'}));// 第三次删除itemsstore.dispatch(removeItem(2));// 第四次增加cartsstore.dispatch(addCart({id:1,name:'book',price:'$5000'}));store.dispatch(addCart({id:2,name:'Iphone',price:'$6000'}));// 第五次删除cartsstore.dispatch(removeCart(1));</script>
</body></html>

问题:每种数据进行更改时,必然会涉及到其他数据的控制,当不小心忘记或错该其他数据时,会造成报错或其他错误。

解决

  1. 将每种数据的操作拆分成不同的模块,users只涉及用户的行为操作,传入的只是users数据,更改也只涉及到users的数据,返回数据也涉及users;items和cart也只关心自己的行为。
  2. 而拆分的方法中只接受user函数为state;
  3. reducer函数中返回的是users,items,cart组成的对象,所以将users(),items(),cart()三个拆分出来的方法分别作为三给对象的key对应的值。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>模拟实现combineReducers()拆分融合函数——问题重现</title>
</head><body><script src="https://cdn.bootcss.com/redux/4.0.5/redux.js"></script><script>//初始化数据都为空let initData = {users: {},items: [],carts: []}//将纯函数分拆成user,item,cart等不同方法,不同方法处理各自数据;因为数据是单独传的了,所以不需要再分开处理如[...state.items]直接[...state]就能获得对应数据function users(state, action) {switch (action.type) {case 'USER_EDIT':return action.payload;}return state;}function items(state, action) {switch (action.type) {case 'ITEM_ADD':return [...state, action.payload];case 'ITEM_REMOVE':return [...state].filter((item) => item.id !== action.payload);}return state;}function carts(state, action) {switch (action.type) {case 'CART_ADD':return [...state, action.payload];case 'CART_REMOVE':return [...state].filter(v => v.id !== action.payload);}return state;}//纯函数中各个数据调用各自处理方法function reducer(state, action) {return {users:users(state.users, action),items:items(state.items,action),carts:carts(state.carts,action)};}//创建store仓库let store = Redux.createStore(reducer, initData);//dispatch()方法//users更改方法function updateUser(payload) {return {type: 'USER_EDIT',payload};}// items增加,删除方法function addItem(payload) {return {type: 'ITEM_ADD',payload};}function removeItem(payload) {return {type: 'ITEM_REMOVE',payload};}// carts增加,删除方法function addCart(payload) {return {type: 'CART_ADD',payload};}function removeCart(payload) {return {type: 'CART_REMOVE',payload};}console.log(store.getState());store.subscribe(() => {console.log(store.getState());});//第一次只更改usersstore.dispatch(updateUser({ id: 1, username: 'MT' }));//第二次增加itemsstore.dispatch(addItem({ id: 1, name: 'Ipad', price: '$5000' }));store.dispatch(addItem({ id: 2, name: 'Iphone', price: '$6000' }));// 第三次删除itemsstore.dispatch(removeItem(2));// 第四次增加cartsstore.dispatch(addCart({ id: 1, name: 'book', price: '$5000' }));store.dispatch(addCart({ id: 2, name: 'Iphone', price: '$6000' }));// 第五次删除cartsstore.dispatch(removeCart(1));</script>
</body></html>

使用Redux中自带的combineReducers()方法原理和以上基本一致,只是会有一些优化。

5.1combineReducers() 方法

该方法的作用是可以把多个 reducer 函数合并成一个 reducer

注意:

  1. combineReducers()方法没有像自己实现的拆分融合方法一样,在纯函数返回值时,将user()方法中传入state.user传入拆分方法中,所以需要在user(),items(),cart()方法中给users,items,cart三种数据初始化值。如,user:user(state.user,action)
  2. 但是使用combineReducers()方法后,Redux内部会帮我们做很多事情,比如获取state和action,但是在处理时Redux不知道应该传入哪种数据,所以在拆分方法中要给state初始值,如user(state={},action)
let reducers = Redux.combineReducers({user,items,cart
});let store = createStore(reducers);

使用combineReducers()方法完整代码实现:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>模拟实现combineReducers()拆分融合函数——问题重现</title>
</head><body><script src="https://cdn.bootcss.com/redux/4.0.5/redux.js"></script><script>//初始化数据都为空let initData = {users: {},items: [],carts: []}//使用combineReducers()函数实现拆分融合需要给state初始值function users(state = {}, action) {switch (action.type) {case 'USER_EDIT':return action.payload;}return state;}function items(state = [], action) {switch (action.type) {case 'ITEM_ADD':return [...state, action.payload];case 'ITEM_REMOVE':return [...state].filter((item) => item.id !== action.payload);}return state;}function carts(state = [], action) {switch (action.type) {case 'CART_ADD':return [...state, action.payload];case 'CART_REMOVE':return [...state].filter(v => v.id !== action.payload);}return state;}//combineReducers()方法会自动注入state,actionlet reducer = Redux.combineReducers({//此时只要users的key和方法名user相同,根据ES6写法即可省略value值users,items,carts});//创建store仓库let store = Redux.createStore(reducer, initData);//dispatch()方法//users更改方法function updateUser(payload) {return {type: 'USER_EDIT',payload};}// items增加,删除方法function addItem(payload) {return {type: 'ITEM_ADD',payload};}function removeItem(payload) {return {type: 'ITEM_REMOVE',payload};}// carts增加,删除方法function addCart(payload) {return {type: 'CART_ADD',payload};}function removeCart(payload) {return {type: 'CART_REMOVE',payload};}console.log(store.getState());store.subscribe(() => {console.log(store.getState());});//第一次只更改usersstore.dispatch(updateUser({ id: 1, username: 'MT' }));//第二次增加itemsstore.dispatch(addItem({ id: 1, name: 'Ipad', price: '$5000' }));store.dispatch(addItem({ id: 2, name: 'Iphone', price: '$6000' }));// 第三次删除itemsstore.dispatch(removeItem(2));// 第四次增加cartsstore.dispatch(addCart({ id: 1, name: 'book', price: '$5000' }));store.dispatch(addCart({ id: 2, name: 'Iphone', price: '$6000' }));// 第五次删除cartsstore.dispatch(removeCart(1));</script>
</body></html>

6.react-redux

再次强调的是,redux 与 react 并没有直接关系,它是一个独立的 JavaScript 状态管理库,如果我们希望中 React 中使用 Redux,需要先安装 react-redux。

6.1安装

npm i -S redux react-redux

// ./store/reducer/users.js
let users = [{id: 1,username: 'baoge',password: '123'
},
{id: 2,username: 'MT',password: '123'
},
{id: 3,username: 'dahai',password: '123'
},
{id: 4,username: 'zMouse',password: '123'
}];export default (state = users, action) => {switch (action.type) {default:return state;}
}
// ./store/reducer/items.js
let items = [{id: 1,name: 'iPhone XR',price: 542500},{id: 2,name: 'Apple iPad Air 3',price: 377700},{id: 3,name: 'Macbook Pro 15.4',price: 1949900},{id: 4,name: 'Apple iMac',price: 1629900},{id: 5,name: 'Apple Magic Mouse',price: 72900},{id: 6,name: 'Apple Watch Series 4',price: 599900}
];export default (state = items, action) => {switch (action.type) {default:return state;}
}
// ./store/reducer/cart.js
export default (state = [], action) => {switch (action.type) {default:return state;}
}
// ./store/index.js
import {createStore, combineReducers} from 'redux';import user from './reducer/user';
import users from './reducer/users';
import items from './reducer/items';
import cart from './reducer/cart';let reducers = combineReducers({user,users,items,cart
});const store = createStore(reducers);export default store;

6.2 Provider 组件

想在 React 中使用 Redux ,还需要通过 react-redux 提供的 Provider 容器组件把 store 注入到应用中

// index.js
import {Provider} from 'react-redux';
import store from './store';ReactDOM.render(<Provider store={store}><Router><App /></Router></Provider>, document.getElementById('root')
);

6.3connect 方法

有了 connect 方法,我们不需要通过 props 一层层的进行传递, 类似路由中的 withRouter ,我们只需要在用到 store 的组件中,通过 react-redux 提供的 connect 方法,把 store 的state数据注入到组件的 props 中,其他组件使用时才能通过this.props获得注入的store仓库。

  • connect()方法返回一个包装函数,通过返回的包装函数去包装应用组件(如此处的Main),这个过程中会把store对象中的数据注入到自己的组件的props中;
  • connect()的参数是一个回调函数,回调函数的返回值就是要注入到组件props中的数据;
  • 使用时,直接通过this.props即可获取
import {connect} from 'react-redux';
?
class Main extends React.Component {render() {console.log(this.props);}
}
?
export default connect()(Main);

默认情况下,connect 会自动注入 dispatch 方法

connect()方法简单原理:

function connect(callback){return function(Component){//执行过程中会调用callback函数,然后获取User组件中的数据id:1 let obj = callback(store.state);for(let k in obj){//然后把获得的数据注入到组件的props属性中Component.props[k] = obj[k];}}
}connect(function(state){return {id:1}
})(User)

6.3.1注入 state 到 props

export default connect( state => {return {items: state.items}
} )(Main);

connect 方法的第一个参数是一个函数

  • 该函数的第一个参数就是 store 中的 state : store.getState()
  • 该函数的返回值将被解构赋值给 props : this.props.items

使用react-redux完整代码示例(只注入users数据)

React 中使用 Redux步骤分析(**):

  1. 自定义组件:创建react项目,使用链接并显示users信息(安装react-router-dom,使用Link,Router,BrowserRouter组件);
  2. 建立user.js仓库:返回reducer函数,并给出reducer函数初始化值(要使用融合函数combineReducers()就必须给纯函数初始值);
  3. 安装Redux,创建仓库,调用融合函数,并到处创建好的仓库(注意:在react项目中,不能直接引入Redux,需要将Redux解构成{createStore,combineReduces()}后直接调用);
  4. 在需要使用仓库的地方,引入store,获取数据(注意:如果每次获取数据,都需要引入store并调用store.getState()会非常麻烦,所以需要安装react-redux,将store对象注入组件应用中,因为redux 与 react 并没有直接关系,只是一个独立的JS库);
  5. <Provider>组件:如果组件及其内部组件,都需要获取需要,每次组件及其内部组件都import仓库会非常繁琐,所以此处需要使用react-redux提供的<Provider>容器组件。用<Provider>组件包裹用到store仓库的所有根节点,并通过属性store将仓库向下传递(即将store数据注入到整个应用中)
  6. connect(callback)()方法:只使用<Provider>组件只是将store仓库注入到了应用组件中,如果想要注入store仓库中的数据state,还需要使用connect()()方法。使用此方法后不需要再通过 props 一层层的进行传递, 类似路由中的 withRouter ,只要再需要获取store 中数据的组件中,通过 react-redux 提供的 connect 方法,把 store 的state数据注入到组件的 props 中,其他组件使用时就能通过this.props获得注入的store仓库。

App.js:引入根级组件BaseApp

import React from 'react';
import './App.css';import BaseApp from './components/BaseApp';function App() {return (<div className="App"><BaseApp /></div>);
}export default App;

BaseApp.js:路由页面;获取创建好的仓库;通过Provider将仓库数据注入到整个组件

import React from 'react';import { BrowserRouter as Router, Link, Route } from 'react-router-dom';
import User from '../views/User';
import { Provider } from 'react-redux';
import store from '../store/createStore';/*** 用于路由各个页面*/
class BaseApp extends React.Component {render() {return (<div>{/* 使用Provider组件可以将store数据注入整个应用 */}<Provider store={store}><Router><Link to="/">User</Link><hr /><Route path="/" component={User} /></Router></Provider></div>);}
}export default BaseApp;

 createStore.js:调用融合函数,且调用纯函数(注意纯函数的实现方式和初始值的传递);创建仓库

//引入Redux相关方法createStore和combineReducers(React中不能直接引入Redux)
import { createStore, combineReducers,dispatch } from 'redux';
// import users from '../data/users';
import users from '../data/users';
import items from '../data/items';//调用融合函数,并创建仓库let reducer = combineReducers({users,items
});
//注意要在此处给初始化数据的话,因为combineReducers函数中初始化了数据为{},所以需要在调用dispatch()且调用store.getState()后才能获得数据
// 但如果直接在combineReducers()给出初始化数据,首次创建时就能获取store数据
let store = createStore(reducer);
export default store;

users.js:实现纯函数和初始数据的传递(注意纯函数的实现及导出;初始化数据方式) 

let users = [{id: 1,username: 'baoge',password: '123'
},
{id: 2,username: 'MT',password: '123'
},
{id: 3,username: 'dahai',password: '123'
},
{id: 4,username: 'zMouse',password: '123'
}];//使用箭头函数直接导出函数,将users作为初始化数据传入,创建仓库后直接获取
//使用箭头函数直接导出函数,将users作为初始化数据传入,创建仓库后直接获取
export default (state = users, action)=>{switch (action.type) {case 'USER_ADD':return [...state.users,action.payload]default:return state;}
}

User.js:用户组件的显示(注意connect()()方法的使用,及方法内state数据的注入)

import React from 'react';
import { connect } from 'react-redux';class User extends React.Component{render(){console.log(this.props.users);return(<div><ul>{this.props.users.map(user=>{return <li key={user.id}>#{user.username}/{user.password}</li>})}</ul></div>);}
}
// 该函数的第一个参数就是 store 中的 state : store.getState()
// 该函数的返回值将被解构赋值给 props : this.props.items
export default connect(state=>{return {users:state.users}
})(User);

 效果:

React(五)——Redux

当需要新增用户时:

users.js纯函数中:

let idMax = 4;//使用箭头函数直接导出函数,将users作为初始化数据传入,创建仓库后直接获取
export default (state = users, action)=>{switch (action.type) {case 'USER_ADD'://每次增加时idMax需不同,可以在每次调用时将idMax+1return [...state,{id:++idMax,...action.payload}]default:return state;}
}

User.js:

import React from 'react';
import { connect } from 'react-redux';class User extends React.Component{constructor(props){super(props);this.addUser = this.addUser.bind(this);//通过ref属性的createRef()获取用户名this.username = React.createRef();}addUser(){let {value} = this.username.current;//判断value不为空时,新增if(value!==""){let {dispatch} = this.props;dispatch({type:'USER_ADD',payload:{username:value,password:'888888'}});}}render(){// let idMax = 4;return(<div>用户名:<input ref={this.username} type="text" ></input><button onClick={this.addUser}>新增</button><ul>{this.props.users.map(user=>{return <li key={user.id}>#{user.username}/{user.password}</li>})}</ul></div>);}
}
// 该函数的第一个参数就是 store 中的 state : store.getState()
// 该函数的返回值将被解构赋值给 props : this.props.items
export default connect(state=>{return {users:state.users}
})(User);

效果:

React(五)——Redux

7.Redux DevTools extension

为了能够更加方便的对 redux 数据进行观测和管理,我们可以使用 Redux DevTools extension 这个浏览器扩展插件

安装指引:https://github.com/zalmoxisus/redux-devtools-extension

浏览器中加载插件:扩展程序--》开启开发者模式--》加载插件

React(五)——Redux

代码调用:安装完插件后,只需要在创建仓库方法createStore()的第二个参数加上window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();

const store = createStore(reducer,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

效果:打开浏览器开发者工具面板 -> redux

React(五)——Redux

React(五)——Redux

8.异步 action

许多时候,我们的数据是需要和后端进行通信的,而且在开发模式下,很容易就会出现跨域请求的问题,好在 create-react-app 中内置了一个基于 node 的后端代理服务,我们只需要少量的配置就可以实现跨域

  • 后端:安装npm i -S koa koa-router;并建立基于 node 的后端应用服务器参考;
  • 前端:通过生命周期函数componentDidMount()函数,并安装axios (npm i -S axios)发送异步请求获取数据;通过axios发送异步请求;更改纯函数增加获取方法USER_GET;并将请求到的数据通过dispatch方法更新到前端页面。
  • 两种方式解决跨域问题:package.json(简单跨域)和./src/setupProxy.js 配置(相对复杂)

8.1package.json 配置

相对比较的简单的后端 URL 接口,我们可以直接中 package.json 文件中进行配置

注意: 设置"proxy": "http://localhost:8989"的位置在最外层。

// 后端接口
http://localhost:8989/api/getUser
// http://localhost:3000
{"proxy": "http://localhost:8989"
}
axios({url: '/api/getUser'
});

 package.json 配置方式完整示例

后端:安装npm i -S koa koa-router;并建立基于 node 的后端应用服务器参考;

Background.js:

/*** 使用node环境及koa框架建立后台服务器*/
//注意import是ES6语法,如果想直接在node环境下运行该文件需要安装babel编译,否则会报错。可以使用require()语法即可
// const Koa = require("Koa");
// const Router = require("koa-router");
// const users = require('../data/users.js');import Koa from 'koa';
import Router from 'koa-router';
import koaBody from 'koa-body';
import users from './userData';//注意此处不能使用const声明
let app = new Koa();
let router = new Router();
console.log(users);app.use(koaBody({multipart:true
}));router.get("/getUser",ctx=>{console.log(users);ctx.body = users;});app.use(router.routes());
app.listen("8989",function(){console.log("8989服务器已开启。。。。。。。");});

 userData.js数据:将users数据单独提取出来

let users = [{id: 1,username: 'baoge',password: '123'
},
{id: 2,username: 'MT',password: '123'
},
{id: 3,username: 'dahai',password: '123'
},
{id: 4,username: 'zMouse',password: '123'
},
{id: 5,username: 'zMouse2',password: '123'
}];export default users;

后台通过localhost:8989/getUser能获取到数据即为成功 

更改纯函数:增加获取数据方法USER_GET

user_async.js:


let idMax = 5;//使用箭头函数直接导出函数,将users作为初始化数据传入,创建仓库后直接获取
export default (state = [], action)=>{console.log(action.payload);switch (action.type) {case 'USER_GET'://注意此处直接将所有数据返回,所以不用解构原来的statereturn action.payload;case 'USER_ADD'://每次增加时idMax需不同,可以在每次调用时将idMax+1return [...state,{id:++idMax,...action.payload}]default:return state;}
}

componentDidMount()函数:通过生命周期函数componentDidMount()函数,并安装axios (npm i -S axios)发送异步请求获取数据;通过axios发送异步请求;并将请求到的数据通过dispatch方法更新到前端页面。

前端组件User_async.js:

import React from 'react';
import { connect } from 'react-redux';
// 引入axios发送异步请求
import axios from 'axios';class User extends React.Component{constructor(props){super(props);this.addUser = this.addUser.bind(this);//通过ref属性的createRef()获取用户名this.username = React.createRef();}addUser(){let {value} = this.username.current;//判断value不为空时,新增if(value!==""){let {dispatch} = this.props;dispatch({type:'USER_ADD',payload:{username:value,password:'888888'}});}//清空输入框this.username.current.value = '';}//通过发送异步请求获取数据,React发送异步请求在componentDidMount()方法,使用axios发送异步请求到后端// 注意:需要使用异步async和awaitasync componentDidMount(){let rs = await axios({//package.json中加上 "proxy": "http://localhost:8989"url:'/getUser'// 当前端和后台系统请求地址需要使用/api进行区分时,使用proxy//url:'/api/getUser'});//获取数据后,通过dispatch将数据进行显示USER_GETlet {dispatch} = this.props;dispatch({type:'USER_GET',payload:rs.data});}render(){return(<div>用户名:<input ref={this.username} type="text" ></input><button onClick={this.addUser}>新增</button><ul>{this.props.users.map(user=>{return <li key={user.id}>#{user.username}/{user.password}</li>})}</ul></div>);}
}
// 该函数的第一个参数就是 store 中的 state : store.getState()
// 该函数的返回值将被解构赋值给 props : this.props.items
export default connect(state=>{return {users:state.users}
})(User);

package.json配置:两种方式解决跨域问题:package.json(简单跨域)和./src/setupProxy.js 配置(相对复杂)

package.json中加上 "proxy": "http://localhost:8989"即可

create-react-app脚手架低于2.0版本时候,可以使用对象类型,但是最新的create-react-app脚手架2.0版本以上只能配置string类型,否则会报错。因此针对相对复杂的情况,可以有更多的配置时,使用setupProxy.js文件配置。

"proxy":{"/api/**":{"target":"http://localhost:8989","changeOrigin": true}
}

React(五)——Redux 

8.2./src/setupProxy.js 配置

针对相对复杂的情况,可以有更多的配置时,使用setupProxy.js文件配置。在项目src目录项,建立setupProxy.js文件,并安装http-proxy-middleware代理服务器。

 

npm i -S http-proxy-middleware

如:前端和后端请求地址通过是否有/api 进行区分

http-proxy-middleware版本问题:针对http-proxy-middleware的官方文档,发现最新的1.0.0版本已经对模块的引用作了明确的要求 (参考自https://blog.csdn.net/balics/article/details/104479641)

0.x.x版本的引用方式 const proxy = require('http-proxy-middleware');

// setupProxy.js
const proxy = require('http-proxy-middleware');
?
module.exports = function(app) {app.use(proxy('/api', {target: 'http://localhost:8989/',
//重新路径,即去掉/api后再发起请求pathRewrite: {'^/api': ''}}));
};

1.0.0版本的引用方式 const { createProxyMiddleware } = require('http-proxy-middleware');

// setupProxy.js
//http-proxy-middlewar 1.0.0版本使用
const { createProxyMiddleware } = require('http-proxy-middleware');module.exports = function (app){app.use('/api', createProxyMiddleware({ target: 'http://localhost:8989/', changeOrigin: true , secure: false,pathRewrite: {'^/api': ''} }));
} 

注意:如果报以下报错就是http-proxy-middleware引用方式错误

React(五)——Redux

其他代码通配置package.json方式,只是前端页面使用/api/getUser区分URL即可

async componentDidMount(){let rs = await axios({//package.json中加上 "proxy": "http://localhost:8989"// url:'/getUser'// 当前端和后台系统请求地址需要使用/api进行区分时,使用proxyurl:'/api/getUser'});//获取数据后,通过dispatch将数据进行显示USER_GETlet {dispatch} = this.props;dispatch({type:'USER_GET',payload:rs.data});}

9.Middleware

默认情况下,dispatch 是同步的,我们需要用到一些中间件来处理。

中间件使用场景:redux是将数据存储在内存中,因此每次页面刷新后,存在内存中的数据就没有了,再想对原始数据进行操作就不行了。如,想要在每次刷新时,都将原来数据同步存储在本地存储中,如果单纯使用dispatch()方法就无法实现,于是就可以使用中间件进行处理进行数据持久化。

例如,koa框架的异步:

  • koa=>处理请求=>发送响应=>{中间件处理函数}=>发送响应;
  • 当koa框架除了处理请求以外,还需要进行一些其他处理的情况下,不能去更改koa框架的代码;
  • 于是通过中间件函数去处理上游发出的指定请求,再处理的结果,将处理后获得的数据发送给下游;
const logger = store => next => action => {console.group(action.type)console.info('dispatching', action)let result = next(action)console.log('next state', store.getState())console.groupEnd(action.type)return result
}

 模拟redux.applyMiddleware()方法实现中间件同样功能(在更改数据的同时,打印日志)(以下示例使用combineReducers()完整实现代码基础上进行更改测试):

需求:只要调用dispatch()方法都打印一下日志

原理分析

  • 不使用dispatch()方法进行直接调用,而是通过myDispatch()方法进行包装后再调用dispatch()方法,即包装函数;
  • 在包装函数中,调用打印日志方法,之后就不需要再调用打印日志的方法;

模拟中间件代码实现:以使用combineReducers()实现拆分与融合完整代码为例,只需要将store.dispatch()包装到自己的myDispatch()方法中,并加入打印日志功能,在调用时使用自定义的myDispatch()方法即可。

//自己写方法实现中间件(在更改数据的同时,打印日志)function myDispatch(action){store.dispatch(action);console.log(store.getState());}//第一次只更改usersmyDispatch(updateUser({ id: 1, username: 'MT' }));//第二次增加itemsmyDispatch(addItem({ id: 1, name: 'Ipad', price: '$5000' }));myDispatch(addItem({ id: 2, name: 'Iphone', price: '$6000' }));// 第三次删除itemsmyDispatch(removeItem(2));// 第四次增加cartsmyDispatch(addCart({ id: 1, name: 'book', price: '$5000' }));myDispatch(addCart({ id: 2, name: 'Iphone', price: '$6000' }));// 第五次删除cartsmyDispatch(removeCart(1));

问题:以上代码能使用中间件的功能,如果在使用过程中,使用到了dispatch(),就写了第三方类库方法去包装dispatch()方法,用到了其他redux方法或其他框架都如此进行调用,则使用的类库和方法就会乱套,每个人都定义自己的。

那么如何做到,所有的方法包装过后不用更改任何代码,就能实现自己的其他功能?

解决:可以先将原来的方法store.dispatch()通过变量存下来,再把自己增加功能的方法赋值给原来的方法(此时该方法已改变可通过变量进行调用),并在方法中通过变量调用存下来的方法处理原有的操作。这样用户在真正调用store.dispatch()方法时,仍然和之前操作一样就可以实现附加功能:

//先将原本的store.dispatch方法通过变量进行存储
let myDispatch = store.dispatch;//再将加入自定义功能的方法赋值给原来的方法store.dispatch,此时原本的方法就包含有自定义功能(如打印日志)
store.dispatch = function(action){//在方法中调用存储下来的方法,完成原本store.dispatch的应有功能myDispatch(action);console.log(store.getState());
}

完整代码实现:发现同样实现数据的更改,且添加了打印日志的功能,却不需要在调用dispatch()方法时做修改即可做到。

//自己写方法实现中间件(在更改数据的同时,打印日志)
let myDispatch = store.dispatch;
store.dispatch = function (action) {console.log("-----------------------------");console.log("更改数据前:", store.getState());myDispatch(action);console.log("更改数据后:", store.getState());console.log("-----------------------------");
}

 redux.applyMiddleware()使用原理和模拟相似,只是逻辑和功能更加复杂。

9.1redux.applyMiddleware()

通过 applyMiddleware 方法,我们可以给 store 注册多个中间件

注意:devTools 的使用需要修改一下配置。即使用代码版,而不是通过windows进行调用。

npm i -D redux-devtools-extension
...
import { composeWithDevTools } from 'redux-devtools-extension';
...
const store = createStore( reducres, composeWithDevTools(applyMiddleware( logger ))
)

10.redux-chunk

使用redux-chunk中间件必要性

  • 实际运用中,不可能每次发送axios异步请求,都使用componentDidMount()方法去做一次请求代理。而是需要将所有发送异步请求的代码进行复用。此时就使用到了中间件redux-chunk。
  • redux-chunk中间件会把同步 dispatch 封装成异步 dispatch 去发送请求。
  • 使用redux-chunk中间件,仍然想使用redux-devtools-extension工具时,不能直接在创建仓库方法第二个参数加上window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),而是需要引入import { composeWithDevTools } from 'redux-devtools-extension',再在第二个参数上使用中 composeWithDevTools(applyMiddleware( thunk ))。

不使用redux-chunk中间件问题重现:使用异步函数包装方式,实现不用多次调用即可多次异步发送请求。即将axios发送请求和dispatch函数进行统一封装成action函数。

import axios from 'axios';export function selectUser(payload){return async dispatch=>{let rs = await axios({url:'/api/getUser'});dispatch({type:'USER_GET',payload:rs.data});}
}
//模拟实现chunk
let {dispatch} = this.props;
dispatch(selectUser());

 发现会报错:

React(五)——Redux

10.1安装

npm i -S redux-chunk

不同版本redux-chunk使用方法不同:

 低版本下:

import {createStore, combineReducers, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';import user from './reducer/user';
import items from './reducer/items';
import cart from './reducer/cart';let reducers = combineReducers({user,items,cart
});const store = createStore(reducers,composeWithDevTools(applyMiddleware(thunk))
);

高版本下(此处1.0.11):

//引入Redux相关方法createStore和combineReducers(React中不能直接引入Redux)
import { createStore, combineReducers, applyMiddleware } from 'redux';
//测试异步时数据从后端获取,但是纯函数还需要使用
// import users from '../data/users';
import users from '../data/users_async';
import items from '../data/items';// 引入redux-chunk的applyMiddleware 方法
// import chunk from 'redux-chunk';
import { middleware as apiMiddleware } from 'redux-chunk';//调用融合函数,并创建仓库
let reducer = combineReducers({users,items
});
//注意要在此处给初始化数据的话,因为combineReducers函数中初始化了数据为{},所以需要在调用dispatch()且调用store.getState()后才能获得数据
// 但如果直接在combineReducers()给出初始化数据,首次创建时就能获取store数据
const store = createStore(reducer,// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()applyMiddleware(apiMiddleware)
);
export default store;

高版本下使用地方版方式会报错:

React(五)——Redux

10.2redux-chunk下使用redux-devtools-extension

使用redux-chunk进行异步发送请求时,要使用redux-devtools-extension插件需要安装且引入redux-devtools-extension 。

npm i -S redux-devtools-extension

//引入Redux相关方法createStore和combineReducers(React中不能直接引入Redux)
import { createStore, combineReducers, applyMiddleware } from 'redux';
//测试异步时数据从后端获取,但是纯函数还需要使用
// import users from '../data/users';
import users from '../data/users_async';
import items from '../data/items';// 引入redux-chunk的applyMiddleware 方法
// import chunk from 'redux-chunk';
// 高版本redux-chunk引入方式,且要使用redux-devtools-extension插件需要引入
import { middleware as apiMiddleware } from 'redux-chunk';
import {composeWithDevTools} from 'redux-devtools-extension';//调用融合函数,并创建仓库
let reducer = combineReducers({users,items
});
//注意要在此处给初始化数据的话,因为combineReducers函数中初始化了数据为{},所以需要在调用dispatch()且调用store.getState()后才能获得数据
// 但如果直接在combineReducers()给出初始化数据,首次创建时就能获取store数据
const store = createStore(reducer,// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()composeWithDevTools(applyMiddleware(apiMiddleware))//  applyMiddleware(chunk)
);
export default store;

 

  相关解决方案