React(五)——Redux

目錄

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中的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——工作流

工作流解析:

  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());
        });
        //第一次只更改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>

問題:每種數據進行更改時,必然會涉及到其他數據的控制,當不小心忘記或錯該其他數據時,會造成報錯或其他錯誤。

解決

  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());
        });
        //第一次只更改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

注意:

  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,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步驟分析(**):

  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);

 效果:

當需要新增用戶時:

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;

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章