目录
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。
什么是纯函数?
纯函数
- 相同的输入永远返回相同的输出
- 不修改函数的输入值
- 不依赖外部环境状态
- 无任何副作用
使用纯函数的好处
- 便于测试
- 有利重构
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 函数
- 第一个参数是原 state 对象
- Reducer 函数不能修改原 state,而应该返回一个新的 state(如,[...state])
- 第二参数是一个 action 对象,包含要执行的操作和数据
- 如果没有操作匹配,则返回原 state 对象
reducer函数的作用:用于接收原始(上一次)的state,并返回一个新的state数据。
3.3action 对象
我们对 state 的修改是通过 reducer 纯函数来进行的,同时通过传入的 action 来执行具体的操作,action 是一个对象
-
type 属性 : 表示要进行操作的动作类型,增删改查……
-
payload属性 : 操作 state 的同时传入的数据
但是这里需要注意的是,我们不直接去调用 Reducer 函数,而是通过 Store 对象提供的 dispatch 方法来调用,store.dispath()方法:用于提交和修改数据。
3.4Store 对象
为了对 state,Reducer,action 进行统一管理和维护,我们需要创建一个 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中的state
console.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——工作流
工作流解析:
- UI中的数据维护不在UI中进行维护,UI对数据的状态state只有使用权;
- 状态state由store仓库进行统一管理。管理时有自己的一套方案;
- 要操作或修改数据时,提供访问和修改的方式:首先从Store仓库中去取数据state,修改数据时要提交action的动作,能否更改如何更改由仓库决定,此时仓库提供Reducer函数去修改状态,Reducer纯函数不是直接调用的,而是通过store对象的dispatch()方法进行调用;
5.Redux——Reducers 分拆与融合
当一个应用比较复杂的时候,状态数据也会比较多,如果所有状态都是通过一个 Reducer 纯函数来进行修改(增删改查)的话,那么这个 Reducer 就会变得特别复杂。这个时候,我们就会对这个 Reducer 进行必要的拆分
let datas = {
user: {},
items: []
cart: []
}
我们把上面的 users、items、cart 进行分拆
// user reducer
function user(state = {}, action) {
// ...
}
// items reducer
function items(state = [], action) {
// ...
}
// cart reducer
function cart(state = [], action) {
// ...
}
模拟 combineReducers()方式自己实现Reducer()纯函数的拆分与融合:
多种数据同时操作问题重现:
- 同时操作三个数据users,items,cart,不进行拆分与融合时:
- 每个数据都有不同的行为(增删改查)(user只有更改);
- 且每个case中返回值也需要同时控制三种数据,如操作users时,Items和cart保存不变;即其中一种数据改变时其他两种数据都保持不变;
- 使用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());
});
//第一次只更改users
store.dispatch(updateUser({ id: 1, username: 'MT' }));
//第二次增加items
store.dispatch(addItem({id:1,name:'Ipad',price:'$5000'}));
store.dispatch(addItem({id:2,name:'Iphone',price:'$6000'}));
// 第三次删除items
store.dispatch(removeItem(2));
// 第四次增加carts
store.dispatch(addCart({id:1,name:'book',price:'$5000'}));
store.dispatch(addCart({id:2,name:'Iphone',price:'$6000'}));
// 第五次删除carts
store.dispatch(removeCart(1));
</script>
</body>
</html>
问题:每种数据进行更改时,必然会涉及到其他数据的控制,当不小心忘记或错该其他数据时,会造成报错或其他错误。
解决:
- 将每种数据的操作拆分成不同的模块,users只涉及用户的行为操作,传入的只是users数据,更改也只涉及到users的数据,返回数据也涉及users;items和cart也只关心自己的行为。
- 而拆分的方法中只接受user函数为state;
- 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());
});
//第一次只更改users
store.dispatch(updateUser({ id: 1, username: 'MT' }));
//第二次增加items
store.dispatch(addItem({ id: 1, name: 'Ipad', price: '$5000' }));
store.dispatch(addItem({ id: 2, name: 'Iphone', price: '$6000' }));
// 第三次删除items
store.dispatch(removeItem(2));
// 第四次增加carts
store.dispatch(addCart({ id: 1, name: 'book', price: '$5000' }));
store.dispatch(addCart({ id: 2, name: 'Iphone', price: '$6000' }));
// 第五次删除carts
store.dispatch(removeCart(1));
</script>
</body>
</html>
使用Redux中自带的combineReducers()方法原理和以上基本一致,只是会有一些优化。
5.1combineReducers() 方法
该方法的作用是可以把多个 reducer 函数合并成一个 reducer。
注意:
- combineReducers()方法没有像自己实现的拆分融合方法一样,在纯函数返回值时,将user()方法中传入state.user传入拆分方法中,所以需要在user(),items(),cart()方法中给users,items,cart三种数据初始化值。如,user:user(state.user,action)
- 但是使用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,action
let 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());
});
//第一次只更改users
store.dispatch(updateUser({ id: 1, username: 'MT' }));
//第二次增加items
store.dispatch(addItem({ id: 1, name: 'Ipad', price: '$5000' }));
store.dispatch(addItem({ id: 2, name: 'Iphone', price: '$6000' }));
// 第三次删除items
store.dispatch(removeItem(2));
// 第四次增加carts
store.dispatch(addCart({ id: 1, name: 'book', price: '$5000' }));
store.dispatch(addCart({ id: 2, name: 'Iphone', price: '$6000' }));
// 第五次删除carts
store.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步骤分析(**):
- 自定义组件:创建react项目,使用链接并显示users信息(安装react-router-dom,使用Link,Router,BrowserRouter组件);
- 建立user.js仓库:返回reducer函数,并给出reducer函数初始化值(要使用融合函数combineReducers()就必须给纯函数初始值);
- 安装Redux,创建仓库,调用融合函数,并到处创建好的仓库(注意:在react项目中,不能直接引入Redux,需要将Redux解构成{createStore,combineReduces()}后直接调用);
- 在需要使用仓库的地方,引入store,获取数据(注意:如果每次获取数据,都需要引入store并调用store.getState()会非常麻烦,所以需要安装react-redux,将store对象注入组件应用中,因为redux 与 react 并没有直接关系,只是一个独立的JS库);
- <Provider>组件:如果组件及其内部组件,都需要获取需要,每次组件及其内部组件都import仓库会非常繁琐,所以此处需要使用react-redux提供的<Provider>容器组件。用<Provider>组件包裹用到store仓库的所有根节点,并通过属性store将仓库向下传递(即将store数据注入到整个应用中)
- 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);
效果:
当需要新增用户时:
users.js纯函数中:
let idMax = 4;
//使用箭头函数直接导出函数,将users作为初始化数据传入,创建仓库后直接获取
export default (state = users, action)=>{
switch (action.type) {
case 'USER_ADD':
//每次增加时idMax需不同,可以在每次调用时将idMax+1
return [...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);
效果:
7.Redux DevTools extension
为了能够更加方便的对 redux 数据进行观测和管理,我们可以使用 Redux DevTools extension 这个浏览器扩展插件
安装指引:https://github.com/zalmoxisus/redux-devtools-extension
浏览器中加载插件:扩展程序--》开启开发者模式--》加载插件
代码调用:安装完插件后,只需要在创建仓库方法createStore()的第二个参数加上window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
效果:打开浏览器开发者工具面板 -> 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':
//注意此处直接将所有数据返回,所以不用解构原来的state
return action.payload;
case 'USER_ADD':
//每次增加时idMax需不同,可以在每次调用时将idMax+1
return [...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和await
async componentDidMount(){
let rs = await axios({
//package.json中加上 "proxy": "http://localhost:8989"
url:'/getUser'
// 当前端和后台系统请求地址需要使用/api进行区分时,使用proxy
//url:'/api/getUser'
});
//获取数据后,通过dispatch将数据进行显示USER_GET
let {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
}
}
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引用方式错误
其他代码通配置package.json方式,只是前端页面使用/api/getUser区分URL即可
async componentDidMount(){
let rs = await axios({
//package.json中加上 "proxy": "http://localhost:8989"
// url:'/getUser'
// 当前端和后台系统请求地址需要使用/api进行区分时,使用proxy
url:'/api/getUser'
});
//获取数据后,通过dispatch将数据进行显示USER_GET
let {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());
}
//第一次只更改users
myDispatch(updateUser({ id: 1, username: 'MT' }));
//第二次增加items
myDispatch(addItem({ id: 1, name: 'Ipad', price: '$5000' }));
myDispatch(addItem({ id: 2, name: 'Iphone', price: '$6000' }));
// 第三次删除items
myDispatch(removeItem(2));
// 第四次增加carts
myDispatch(addCart({ id: 1, name: 'book', price: '$5000' }));
myDispatch(addCart({ id: 2, name: 'Iphone', price: '$6000' }));
// 第五次删除carts
myDispatch(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());
发现会报错:
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;
高版本下使用地方版方式会报错:
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;