Redux 入門教程(二):中間件與異步操作

hwh1990 9年前發布 | 13K 次閱讀 中間件 Redux JavaScript開發

上一篇文章,我介紹了 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)
);

(2)中間件的次序有講究。

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 的代碼,關鍵之處就在里面。

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 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。

中間件和異步操作,就介紹到這里。下一篇文章將是最后一部分,介紹如何使用 react-redux 這個庫。

 

 

 

來自:http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html

 

 本文由用戶 hwh1990 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!