目錄
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;