Redux中間件與異步操作

前端開發React用的很多,但它只是一個view,涉及到復雜的功能時就必須要用到Redux/Mobx等狀態管理器,React 只是 DOM 的一個抽象層,并不是 Web 應用的完整解決方案。
參考http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html

上一章,我介紹了 Redux 的基本做法:用戶發出 Action,Reducer 函數算出新的 State,View 重新渲染。

但是,一個關鍵問題沒有解決:異步操作怎么辦?Action 發出以后,Reducer 立即算出 State,這叫做同步;Action 發出以后,過一段時間再執行 Reducer,這就是異步。

怎么才能 Reducer 在異步操作結束后自動執行呢?這就要用到新的工具:中間件(middleware)。

中間件的概念

為了理解中間件,讓我們站在框架作者的角度思考問題:如果要添加功能,你會在哪個環節添加?

  1. Reducer:純函數,只承擔計算 State 的功能,不合適承擔其他功能,也承擔不了,因為理論上,純函數不能進行讀寫操作。
  2. View:與 State 一一對應,可以看作 State 的視覺層,也不合適承擔其他功能。
  3. Action:存放數據的對象,即消息的載體,只能被別人操作,自己不能進行任何操作。

想來想去,只有發送 Action 的這個步驟,即store.dispatch()方法,可以添加功能。舉例來說,要添加日志功能,把 Action 和 State 打印出來,可以對store.dispatch進行如下改造。

let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  next(action);
  console.log('next state', store.getState());
}

上面代碼中,對store.dispatch進行了重定義,在發送 Action 前后添加了打印功能。這就是中間件的雛形。

中間件就是一個函數,對store.dispatch方法進行了改造,在發出 Action 和執行 Reducer 這兩步之間,添加了其他功能。

中間件的用法

本教程不涉及如何編寫中間件,因為常用的中間件都有現成的,只要引用別人寫好的模塊即可。比如,上一節的日志中間件,就有現成的redux-logger模塊。這里只介紹怎么使用中間件。

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

const store = createStore(
  reducer,
  applyMiddleware(logger)
);

上面代碼中,redux-logger提供一個生成器createLogger,可以生成日志中間件logger。然后,將它放在applyMiddleware方法之中,傳入createStore方法,就完成了store.dispatch()的功能增強。

這里有兩點需要注意:

  1. createStore方法可以接受整個應用的初始狀態作為參數,那樣的話,applyMiddleware就是第三個參數了。
const store = createStore(
  reducer,
  initial_state,
  applyMiddleware(logger)
);
  1. 中間件的次序有講究。
const store = createStore(
  reducer,
  applyMiddleware(thunk, promise, logger)
);

上面代碼中,applyMiddleware方法的三個參數,就是三個中間件。有的中間件有次序要求,使用前要查一下文檔。比如,logger就一定要放在最后,否則輸出結果會不正確。

applyMiddlewares()

看到這里,你可能會問,applyMiddlewares這個方法到底是干什么的?

它是 Redux 的原生方法,作用是將所有中間件組成一個數組,依次執行。下面是它的源碼。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    var store = createStore(reducer, preloadedState, enhancer);
    var dispatch = store.dispatch;
    var chain = [];

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    };
    chain = middlewares.map(middleware => middleware(middlewareAPI));
    dispatch = compose(...chain)(store.dispatch);

    return {...store, dispatch}
  }
}

上面代碼中,所有中間件被放進了一個數組chain,然后嵌套執行,最后執行store.dispatch。可以看到,中間件內部(middlewareAPI)可以拿到getState和dispatch這兩個方法。

異步操作的基本思路

理解了中間件以后,就可以處理異步操作了。

同步操作只要發出一種 Action 即可,異步操作的差別是它要發出三種 Action。

  • 操作發起時的 Action
  • 操作成功時的 Action
  • 操作失敗時的 Action

以向服務器取出數據為例,三種 Action 可以有兩種不同的寫法。

// 寫法一:名稱相同,參數不同
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }

// 寫法二:名稱不同
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

除了 Action 種類不同,異步操作的 State 也要進行改造,反映不同的操作狀態。下面是 State 的一個例子。

let state = {
  // ... 
  isFetching: true,
  didInvalidate: true,
  lastUpdated: 'xxxxxxx'
};

上面代碼中,State 的屬性isFetching表示是否在抓取數據。didInvalidate表示數據是否過時,lastUpdated表示上一次更新時間。

現在,整個異步操作的思路就很清楚了。

  • 操作開始時,送出一個 Action,觸發 State 更新為"正在操作"狀態,View 重新渲染
  • 操作結束后,再送出一個 Action,觸發 State 更新為"操作結束"狀態,View 再一次重新渲染

redux-thunk 中間件

異步操作至少要送出兩個 Action:用戶觸發第一個 Action,這個跟同步操作一樣,沒有問題;如何才能在操作結束時,系統自動送出第二個 Action 呢?

奧妙就在 Action Creator 之中。

class AsyncApp extends Component {
  componentDidMount() {
    const { dispatch, selectedPost } = this.props
    dispatch(fetchPosts(selectedPost))
  }

// ...

上面代碼是一個異步組件的例子。加載成功后(componentDidMount方法),它送出了(dispatch方法)一個 Action,向服務器要求數據 fetchPosts(selectedSubreddit)。這里的fetchPosts就是 Action Creator。

下面就是fetchPosts的代碼,關鍵之處就在里面。


image.png
const fetchPosts = postTitle => (dispatch, getState) => {
  dispatch(requestPosts(postTitle));
  return fetch(`/some/API/${postTitle}.json`)
    .then(response => response.json())
    .then(json => dispatch(receivePosts(postTitle, json)));
  };
};

// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
  console.log(store.getState())
);

上面代碼中,fetchPosts是一個Action Creator(動作生成器),返回一個函數。這個函數執行后,先發出一個Action(requestPosts(postTitle)),然后進行異步操作。拿到結果后,先將結果轉成 JSON 格式,然后再發出一個 Action( receivePosts(postTitle, json))。

上面代碼中,有幾個地方需要注意。

  1. fetchPosts返回了一個函數,而普通的 Action Creator 默認返回一個對象。
  2. 返回的函數的參數是dispatch和getState這兩個 Redux 方法,普通的 Action Creator 的參數是 Action 的內容。
  3. 在返回的函數之中,先發出一個 Action(requestPosts(postTitle)),表示操作開始。
  4. 異步操作結束之后,再發出一個 Action(receivePosts(postTitle, json)),表示操作結束。

這樣的處理,就解決了自動發送第二個 Action 的問題。但是,又帶來了一個新的問題,Action 是由store.dispatch方法發送的。而store.dispatch方法正常情況下,參數只能是對象,不能是函數。

這時,就要使用中間件redux-thunk

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';

// Note: this API requires redux@>=3.1.0
const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

上面代碼使用redux-thunk中間件,改造store.dispatch,使得后者可以接受函數作為參數。

中間件里通過一個action類型的判斷,輕松的處理了store.dispatch參數只能是對象,不能為函數的問題。
thunk中間件源碼如下:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

因此,異步操作的第一種解決方案就是,寫出一個返回函數的 Action Creator,然后使用redux-thunk中間件改造store.dispatch。

redux-promise 中間件

既然 Action Creator 可以返回函數,當然也可以返回其他值。另一種異步操作的解決方案,就是讓 Action Creator 返回一個 Promise 對象。

這就需要使用redux-promise中間件。

import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';

const store = createStore(
  reducer,
  applyMiddleware(promiseMiddleware)
); 

這個中間件使得store.dispatch方法可以接受 Promise 對象作為參數。這時,Action Creator 有兩種寫法。寫法一,返回值是一個 Promise 對象。

const fetchPosts = 
  (dispatch, postTitle) => new Promise(function (resolve, reject) {
     dispatch(requestPosts(postTitle));
     return fetch(`/some/API/${postTitle}.json`)
       .then(response => {
         type: 'FETCH_POSTS',
         payload: response.json()
       });
});

寫法二,Action 對象的payload屬性是一個 Promise 對象。這需要從redux-actions模塊引入createAction方法,并且寫法也要變成下面這樣。

import { createAction } from 'redux-actions';

class AsyncApp extends Component {
  componentDidMount() {
    const { dispatch, selectedPost } = this.props
    // 發出同步 Action
    dispatch(requestPosts(selectedPost));
    // 發出異步 Action
    dispatch(createAction(
      'FETCH_POSTS', 
      fetch(`/some/API/${postTitle}.json`)
        .then(response => response.json())
    ));
  }

上面代碼中,第二個dispatch方法發出的是異步 Action,只有等到操作結束,這個 Action 才會實際發出。注意,createAction的第二個參數必須是一個 Promise 對象。

看一下redux-promise源碼,就會明白它內部是怎么操作的。

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

    return isPromise(action.payload)
      ? action.payload.then(
          result => dispatch({ ...action, payload: result }),
          error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          }
        )
      : next(action);
  };
}

從上面代碼可以看出,如果 Action 本身是一個 Promise,它 resolve 以后的值應該是一個 Action 對象,會被dispatch方法送出(action.then(dispatch)),但 reject 以后不會有任何動作;如果 Action 對象的payload屬性是一個 Promise 對象,那么無論 resolve 和 reject,dispatch方法都會發出 Action。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。