Redux 中間件和異步操作


  回顧一下Redux的數據流轉,用戶點擊按鈕發送了一個action,  reducer 就根據action 和以前的state 計算出了新的state, store.subscribe 方法的回調函數中 store.getState() 獲取新的state, 把state 注入到頁面元素中,實現頁面狀態的更新。你發現根本就沒有機會去做一個異步的操作,但現實世界中又有大量的異步操作,那怎么辦? 這就用到了Redux的中間件。

  中間件,說白了,就是中間的部分,正常的流程中,先執行函數a,再執行 b 函數,但如果在a 和b之間插入了中間件,那么流程就變成了,先執行a 函數,再執行中間件,最后執行b函數, 如果中間件在執行的過程中出錯了,那b 函數就沒有機會執行了,可以看到中間件的出現,阻礙a 到b 的順序執行,那么這時候,我們就可以在中間件中作一系的事情,比如日志的記錄,異步處理。具體到Redux中就是,在正常的Redux流程 中,發送一個action, 它立刻會到reducer中,那么我們就要在發送action 和進入reducer 之間插入中間件,阻止這種順序的操作,我們在這些中間件中作異步操作等等。怎樣插入中間件呢? Redux 提供了一個applyMiddleware 函數,把要使用的中間件作為參數傳遞給它就好了,  applyMiddleware 函數在createStore的時候調用,使用到的中間件是redux-thunk, 用於發送異步請求。為了更好使用中間件,純html 方式不太好演示,這里簡配置一個webpack,  只使用webapck-dev-server 就可以了。

  在任意的文件夾中打開命令行工具(git bash),  mkdir redux-middleware && cd redux-middleware && npm init -y  快速創建項目。npm i webpack webpack-dev-server webpack-cli -D 安裝webpack-dev-server. webpack4 提供了零配置,默認的入口文件是src目錄下的index.js, 要創建src目錄和它下面的index.js。 由於webpack-dev-server 是把文件打包到內存中,打包后的文件放到根目錄下面的main.js, 所以main.js 不用建,不過要建一個html 文件引入它們, 最后npm install redux redux-thunk -S.

  redux-thunk 作為異步請求的中間件,使用方式非常簡單,當dispatch 一個函數的時候,它就執行這個函數,而不傳遞給reducer, 只有當dispatch 一個對象的時候,它才會傳遞到reducer 中,進行state的更改。也就是說,當進行異步請求的時候,首先要dispatch一個函數,也就是說action creator 要返回一個函數(異步action creator),這個函數有一個dispatch 作為參數,函數體內就可以發送異步請求, 然后在函數的內部,比如獲取到數據了或報錯了, 再dispatch 一個對象,把獲取的到數據或錯誤信息傳遞到reducer 中,進而改變state,完成數據的更新。模板如下

function asyncActionCreator() {
  return dispacth => {
    fetchData(url)
      .then(() => {
        dispacth({
          type: 'success'
        })
      })
      .catch(err => {
        dispacth({
          type: 'failure'
        })
      })
  }
}

store.dispacth(asyncActionCreator())

   現在做一個簡單的實例,就是點擊按鈕,隨機獲取一個用戶的信息,網上有這么一個api接口https://randomuser.me/api/?results=1 , 后面的result=1 表示一次只獲取一個,是一個get 請求,返回的結果(json 格式)如下,進行了刪減, 只顯示name,geneder, email 和img

{
    "results": [
        {
            "gender": "female",
            "name": {
                "title": "mrs",
                "first": "minttu",
                "last": "murto"
            },
            "email": "minttu.murto@example.com",
            "picture": {
                "large": "https://randomuser.me/api/portraits/women/40.jpg",
                "medium": "https://randomuser.me/api/portraits/med/women/40.jpg",
                "thumbnail": "https://randomuser.me/api/portraits/thumb/women/40.jpg"
            }
        }
    ]
}

  准備工作完畢,開始寫代碼,但在寫之前,還要考慮一下程序的state, 到底存儲什么信息,頁面上要展示什么?看一下操作,點擊按鈕,發起異步的數據請求,這里是一個異步action, 異步請求開始,這時就要給用戶提示,需要一個狀態status,提示正在加載,因此也需要一個action; 請求數據成功,獲取到user,除了status 需要更新之外,它還需要一個狀態 user 來存儲獲取到的數據,需要一個action. 如果失敗了,通常更新status 表示加載失敗,一個action. 只要涉及到狀態的改變,都需要一個action, 在Redux 中只有action 才能改變state. 此外最好再加兩個state,statusClass 和sendingRequest. statusClass 對提示的status 進行樣式處理,比如加載失敗用紅色.  sendingRequest 它的取值為true or false, 表示有沒有在請求,可以通過它,來決定頁面上顯示什么內容,比如在加載的時候,不顯示用戶信息。這兩個狀態隨着status的變化而變化,所以不需要action.那我們的state 就如下所示, 在index.js 中書寫

// 初始狀態, 定義state的形態
const initialState = {
    sendingRequest: false, // 是否正在請求
    status: '',    // 加載提示
    statusClass: '',  // 加載提示樣式
    user: {  // 用戶信息
        name: '',
        gender: '',
        email: '',
        img: ''
    }
}

  那頁面上顯示信息呢?status 加載提示和user 信息,status 這里用bootstrap 的alert 組件 ,user 信息用Card組件.  當然status 還要加上樣式stautsClass

<body style="text-align: center; margin-top: 50px">
    <button type="button" class="btn btn-primary" id="getUser">獲取用戶信息</button>
    <!-- 加載提示 -->
    <div style="width: 18rem; margin: 20px auto; display: none" id="status"></div>
    <!-- 用戶信息 -->
    <div class="card" style="width: 20rem; margin: 20px auto; display: none" id="userInfo">
        <img class="card-img-top" src="" id="img">
        <div class="card-body">
            <h5 class="card-title" id="name"></h5>
            <h5 class="card-title" id="gender"></h5>
            <h5 class="card-title" id="email"></h5>
        </div>
    </div>

    <script src="main.js"></script>
</body>

   npx webpack-dev-server 啟動服務器, 瀏覽器中輸入localhost:8080 看一下效果, 可以看到在初始狀態,加載提示和用戶信息都是display 為none,不顯示,整個頁面只顯示一個button. 

  那么現在要做的就是當用戶點擊的時候,動態顯示提示內容,

  和最終的用戶信息

 

  好了,現在開始寫js 代碼來實現這個功能。通過分析state的時候,一個異步action,就是點擊按鈕發送請求,它需要寫一個異步的action creator. 三個同步action  因為它們改變狀態,請求發送type: 'USER_REQUEST';  請求成功 type: 'USER_RECEIVED',  還要帶有一個請求成功回來的user;數據請求失敗type:  'USER_FAIL'; 寫一下這四個action creator

// 三個同步action 都是返回的對象,用來改變state.
function userRequest() {  //獲取數據時
    return { type: 'USER_REQUEST' }
}

function userReceived(userData) { // 獲取成功
    return {
        type: 'USER_RECEIVED',
        payload: userData
    }
}

function userFail() { // 獲取失敗
    return {
        type: 'USER_FAIL'
    }
}

// getUser的異步action, 注意,它一定返回的是一個函數, 該函數有一個dispatch 作為參數,
// 該函數內部根據不同的情況發送不同的 同步action 來改變state
function getUser() {
    return dispatch => {
        dispatch(userRequest()); // 正在請求action,'USER_REQUEST', 整個應用的state 可以設為status為‘正在加載’

        return fetch('https://randomuser.me/api/?results=1')
            .then(
                response => {
                    if (response.ok) {
                        return response.json()
                    } else {
                        return undefined;
                    }
                },
                error => {
                    dispatch(userFail(error));  // 請求失敗的action, 'USER_FAIL',status為‘加載失敗’
                }
            )
            .then(json => {
                console.log(json)
                if (!json) {
                    dispatch(userFail()); 
                    return;
                }
                dispatch(userReceived(json.results)) // 請求成功的action 'USER_RECEIVED', 直接更改user
            })
    }
}

  現在有了action 就再寫reducer 了,改變的狀態的action只有3個,type: 'USER_REQUEST';   type: 'USER_RECEIVED',   type: 'USER_FAIL'

  type: 'USER_REQUEST', 表示正在發送請求,那么 sendingRequest 肯定設為true, status 就設為'正在加載', 正在加載 使用樣式statusClass 為 'alert alert-info';

  type: 'USER_RECEIVED', 請求成功了,sendingRequest 設為false, user的信息也獲取到了,status 也就不用顯示了,還是初始化的'' ,statusClass 也是一樣,不過user 狀態的處理有點麻煩,因為在intialState中 user 是一個對象,所以在這里要重新創建一個對象user 來替換以前的user, 又因為獲取回來的數據不能直接使用,所以進行拼接,然后給新創建的user 對象的屬性一一進行賦值。

  type: 'USER_FAIL': 請求失敗了,雖然失敗了,但請求還是發送成功了, 所以sendingRequest 設為false, 由於沒有獲取到數據,user 信息不用更新,還是初始化的狀態,但status 信息肯定要更新,status 設為 '加載數據失敗' ,statusClass 為紅色 'alert alert-danger'

  最后不要忘記default 分支,返回默認的初始值。

function userState(state = initialState, action) {
    switch (action.type) {
        case 'USER_REQUEST':
            return {
                ...state,
                sendingRequest: true,
                status: '正在加載',
                statusClass: 'alert alert-info'
            }
        case 'USER_RECEIVED': {
            const user = {
                name: '',
                email: '',
                gender: '',
                img: ''
            }
            user.name = `${action.payload[0].name.first} ${action.payload[0].name.last}`
            user.email = action.payload[0].email;
            user.gender = action.payload[0].gender;
            user.img = action.payload[0].picture.large;
            return {
                ...state,
                user,
                sendingRequest: false
            }
        }
        case 'USER_FAIL':
            return {
                ...state,
                sendingRequest: false,
                status: '獲取數據失敗',
                statusClass: 'alert alert-danger'
            }
        default:
            return state;
    }
}

  有了reducer , 終於可以創建store了,由於使用es6 模塊化,所以要進行引用,由於使用了中間件redux-thunk, 所以要引入createStore, applyMiddleware,  thunk, index.js 頂部

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

  創建store

// 創建store, 要把中間件傳進去
const store = createStore(userState, applyMiddleware(thunk))

  獲取狀態渲染頁面了,使用document.getElementById 獲取元素比較簡單,稍微有點麻煩的是頁面元素的切換,首先是 sendingRequest 的判斷,為true時,正在請求,只能顯示加載提示,為false的時候,表示請求完成,但又有兩種情況,成功或失敗,所以渲染函數如下

const statusEl = document.getElementById('status'); // 加載提示部分
const userInfoEl = document.getElementById('userInfo'); // 用戶信息展示部分
const nameEl = document.getElementById('name');
const genderEl = document.getElementById('gender');
const emailEl = document.getElementById('email');
const imgEl = document.getElementById('img');

function render() {
    const state = store.getState();
    if (state.sendingRequest) { // 正在請求的時候,顯示提示信息status, 用戶信息不顯示
        userInfoEl.style.display = 'none';
        statusEl.style.display = 'block';
        statusEl.innerHTML = state.status;
        statusEl.className = state.statusClass;
    } else { // 請求完成后它又分兩種情況,
        console.log(state)
        if (state.user.name !== '') { // 請求成功,獲取到user 顯示user 
            userInfoEl.style.display = 'block';
            statusEl.style.display = 'none';
            nameEl.innerHTML = state.user.name;
            emailEl.innerHTML = state.user.email;
            genderEl.innerHTML = state.user.gender;
            imgEl.src = state.user.img;
        } else { // 請求失敗,這里按理說應該寫一個error 顯示的,為了簡單,直接使用了提示status 
            userInfoEl.style.display = 'none';
            statusEl.style.display = 'block';
            statusEl.innerHTML = state.status;
            statusEl.className = state.statusClass;
        }
    }
}

  使用subscribe 監聽狀態的變化,然后調用render 函數。

store.subscribe(() => {
    render()
})

  最后就是獲取到button, 添加點擊事件,發送異步action getUser

// 點擊按鈕發送請求
document.getElementById('getUser').addEventListener('click', () => {
    store.dispatch(getUser());
})

  整個功能就完成了。這時隨着功能的復雜,程序開發過程中隨時都有可能出現錯誤,就需要進行調試,對於redux 來說,最主要的就發送的action 和 改變的state, 如果能記錄下來,就可以加快調試,有一個中間件,就是redux-logger, 干這個活, npm install redux-logger --save,  安裝完成后,使用就簡單了,在js 中引入,並放入到applyMiddleware

import { createLogger } from 'redux-logger';
const store = createStore(userState, applyMiddleware(thunk, createLogger()));

   總結一下在redux-thunk 中間件下的異步請求,整個請求過程放到一個接受dispatch 作為參數的函數體( dispatch => { 異步請求})中,只有dispatch 一個函數,redux-thunk 才不會把action 發送給reducer.  而在異步請求的過程中,至少要發送三個同步action, 請求中,請求成功,請求失敗,它們要傳遞給reducer 去改變state, 因為這些信息是要給用戶看的,相應的,我們的 reducer 也要至少處理 請求中,請求成功,請求失敗 三個action, 返回各個對應的狀態。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM