Redux:使用异步中间件与在成功函数上调度操作

Redux: Using async middlewares vs dispatching actions on success functions

我正在尝试将 Redux 集成到我的 React 项目中。 目前我没有使用任何 Flux 框架。

我的应用程序从 API 获取一些数据并以漂亮的方式显示它,如下所示:

componentDidMount() {
  getData();
}

getData() {
  const self = this;

  ajax({
    url: apiUrl,
  })
  .success(function(data) {
    self.setState({
      data: data,
    });
  })
  .error(function() {
    throw new Error('Server response failed.');
  });
}

在阅读有关 Redux 的文章时,我确定了两种可能的方法,可用于处理在商店中存储我的成功数据:

但我不确定哪种方法更好。

回调中的调度操作听起来很容易实现和理解,而异步中间件则很难向不习惯使用函数式语言的人解释。

我使用 redux-thunk 进行 ajax 调用,并使用 redux-promise 处理承诺,如下所示。

  function getData() {             // This is the thunk creator
    return function (dispatch) {   // thunk function
      dispatch(requestData());     // first set the state to 'requesting'
      return dispatch(
        receiveData(               // action creator that receives promise
          webapi.getData()         // makes ajax call and return promise
        )
      );
    };
  }

在回调中调度一个动作对于初学者来说似乎更容易理解,但是使用中间件有以下优点:

  • thunk 允许分派多个动作(如上例所示—— 首先将状态设置为'requesting',可以通过加载指标使用, 等等)
  • 它允许有条件地分派额外的动作。例如,仅当自上次获取以来的时间超过阈值时才获取
  • 您仍然可以在没有中间件的情况下实现所有这些,但是使用中间件可以帮助您将所有异步行为保留在操作创建者中

我个人更喜欢使用自定义中间件来完成此操作。它使操作更容易遵循,并且 IMO 样板更少。

我已经设置我的中间件来查找从与特定签名匹配的操作返回的对象。如果找到此对象模式,它会进行特殊处理。

例如,我使用如下所示的操作:

export function fetchData() {
  return {
    types: [ FETCH_DATA, FETCH_DATA_SUCCESS, FETCH_DATA_FAILURE ],
    promise: api => api('foo/bar')
  }
}

我的自定义中间件看到对象有一个 types 数组和一个 promise 函数并对其进行特殊处理。这是它的样子:

import 'whatwg-fetch';

function isRequest({ promise }) {
  return promise && typeof promise === 'function';
}

function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  } else {
    const error = new Error(response.statusText || response.status);
    error.response = response.json();
    throw error;
  }
}

function parseJSON(response) {
  return response.json();
}

function makeRequest(urlBase, { promise, types, ...rest }, next) {
  const [ REQUEST, SUCCESS, FAILURE ] = types;

  // Dispatch your request action so UI can showing loading indicator
  next({ ...rest, type: REQUEST });

  const api = (url, params = {}) => {
    // fetch by default doesn't include the same-origin header.  Add this by default.
    params.credentials = 'same-origin';
    params.method = params.method || 'get';
    params.headers = params.headers || {};
    params.headers['Content-Type'] = 'application/json';
    params.headers['Access-Control-Allow-Origin'] = '*';

    return fetch(urlBase + url, params)
      .then(checkStatus)
      .then(parseJSON)
      .then(data => {
        // Dispatch your success action
        next({ ...rest, payload: data, type: SUCCESS });
      })
      .catch(error => {
        // Dispatch your failure action
        next({ ...rest, error, type: FAILURE });
      });
  };

  // Because I'm using promise as a function, I create my own simple wrapper
  // around whatwg-fetch. Note in the action example above, I supply the url
  // and optionally the params and feed them directly into fetch.

  // The other benefit for this approach is that in my action above, I can do 
  // var result = action.promise(api => api('foo/bar'))
  // result.then(() => { /* something happened */ })
  // This allows me to be notified in my action when a result comes back.
  return promise(api);
}

// When setting up my apiMiddleware, I pass a base url for the service I am
// using. Then my actions can just pass the route and I append it to the path
export default function apiMiddleware(urlBase) {
  return function() {
    return next => action => isRequest(action) ? makeRequest(urlBase, action, next) : next(action);
  };
}

我个人喜欢这种方法,因为它集中了很多逻辑,并为您提供了 api 操作结构的标准实施方式。这样做的缺点是,对于那些不熟悉 redux 的人来说,它可能有点神奇。我也使用 thunk 中间件,这两个一起解决了我目前的所有需求。

这两种方法都不是更好,因为它们是相同的。无论您是在回调中分派操作还是使用 redux thunk,您都在有效地执行以下操作:

function asyncActionCreator() {
  // do some async thing
  // when async thing is done, dispatch an action.
}

就我个人而言,我更喜欢跳过中间件/thunk,只使用回调。我真的不认为与中间件/thunk 相关的额外开销是必要的,编写自己的 "async action creator" 函数并没有那么困难:

var store = require('./path-to-redux-store');
var actions = require('./path-to-redux-action-creators');

function asyncAction(options) {
  $.ajax({
    url: options.url,
    method: options.method,
    success: function(response) {
      store.dispatch(options.action(response));
    }
  });
};

// Create an async action
asyncAction({
  url: '/some-route',
  method: 'GET',
  action: actions.updateData
}); 

我想你真正想问的是 AJAX 调用你的 action creator 还是你的组件。

如果您的应用足够小,将它放在您的组件中就可以了。但是随着您的应用程序变得越来越大,您将需要重构。在较大的应用程序中,您希望您的组件尽可能简单和可预测。在您的组件中进行 AJAX 调用会大大增加其复杂性。此外,在动作创建器中调用 AJAX 使其更易于重用。

惯用的 Redux 方法是将所有异步调用放入动作创建器中。这使您的应用程序的其余部分更具可预测性。您的组件始终是同步的。您的减速器始终是同步的。

对异步操作创建者的唯一要求是 redux-thunk。您不需要知道中间件的来龙去脉就可以使用redux-thunk,您只需要知道如何在创建商店时应用它。

以下直接摘自redux-thunkgithub页面:

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

// create a store that has redux-thunk middleware enabled
const createStoreWithMiddleware = applyMiddleware(
    thunk
)(createStore);

const store = createStoreWithMiddleware(rootReducer);

就是这样。现在您可以拥有异步操作创建器。

你的看起来像这样:

function getData() {

    const apiUrl = '/fetch-data';

    return (dispatch, getState) => {

        dispatch({
            type: 'DATA_FETCH_LOADING'
        });

        ajax({
            url: apiUrl,
        }).done((data) => {
            dispatch({
                type: 'DATA_FETCH_SUCCESS',
                data: data
            });
        }).fail(() => {
            dispatch({
                type: 'DATA_FETCH_FAIL'
            });
        });

   };

}

就是这样。每当动作创建者 returns 一个函数时,thunk 中间件就会公开 dispatch(以及您可能不需要的 getState)以允许异步动作。