回顧一下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, 返回各個對應的狀態。
