如何处理redux observable中的错误?

时间:2018-01-10 15:57:01

标签: rxjs redux-observable

我有以下代码:

export const myEpic = (action$, store) =>
action$.ofType("SOME_ACTION")
    .switchMap(action => {
        const {siteId, selectedProgramId} = action;
        const state = store.getState();
        const siteProgram$ = Observable.fromPromise(axios.get(`/url/${siteId}/programs`))
       .catch(error =>{
            return Observable.of({
                type: 'PROGRAM_FAILURE'
                error
            });
        });

        const programType$ = Observable.fromPromise(axios.get('url2'))
       .catch(error =>{
            return Observable.of({
                type: "OTHER_FAILURE",
                error
            });
        });

到目前为止一切顺利,当我抓住它时出现错误,并且(可能这是错误的)将其映射到一个动作(表示失败了)。

现在问题开始了,我有另一个observable,它是上面两个observable的zip运算符的结果:

        const siteProgram$result$ = Observable.zip(siteProgram$, programType$)
       .map(siteProgramsAndProgramTypes => siteProgramsAndProgramTypesToFinalSiteProgramsActionMapper(siteProgramsAndProgramTypes, siteId));

问题是我仍然可以看到这一点,好像一切都很好。

有没有办法理解"其中一个"压缩"观察者的错误,然后没有到达"下一个" siteProgram $ result $。 我想我错过了一些微不足道的事情......

我不想要执行此检查:

const siteProgramsAndProgramTypesToFinalSiteProgramsActionMapper = (siteProgramsAndProgramTypesArray, siteId) => {
const [programsResponse, programTypesResponse] = siteProgramsAndProgramTypesArray;
if (programsResponse.error || programTypesResponse.error){
    return {
        type: 'GENERAL_ERROR',
    };
}

每次我都有一个observable,它是其他observable操作符的结果,可能有错误。

在纯rxjs中(不在redux observable中)我想我可以订阅它传递一个对象

{
    next: val => some logic,
    error: err => do what ever I want :) //this is what I am missing in redux observable,
    complete: () => some logic
}

// some more logic
return Observable.concat(programType$Result$, selectedProgramId$, siteProgram$result$);

在redux observable中攻击它的正确方法是什么?

感谢。

1 个答案:

答案 0 :(得分:0)

这是一个使用API​​包装器的详细示例,以方便您尝试实现的目标。

要点可以在GitHub here

上找到

这是封装Observable.ajax的API包装器,允许您分派单个操作或操作数组,并处理源自Observable.ajax

请求的XHR和应用程序级生成的错误
import * as Rx from 'rxjs';
import queryString from 'query-string';

/**
 * This function simply transforms any actions into an array of actions
 * This enables us to use the synthax Observable.of(...actions)
 * If an array is passed to this function it will be returned automatically instead
 * Example: mapObservables({ type: ACTION_1 }) -> will return: [{ type: ACTION_1 }]
 * Example2: mapObservables([{ type: ACTION_1 }, { type: ACTION_2 }]) -> will return: [{ type: ACTION_1 }, { type: ACTION_2 }]
 */
function mapObservables(observables) {
  if (observables === null) {
    return null;
  } else if (Array.isArray(observables)) {
    return observables;
  }
  return [observables];
}

/**
 * Possible Options:
 * params (optional): Object of parameters to be appended to query string of the uri e.g: { foo: bar } (Used with GET requests)
 * headers (optional): Object of headers to be appended to the request headers
 * data (optional): Any type of data you want to be passed to the body of the request (Used for POST, PUT, PATCH, DELETE requests)
 * uri (required): Uri to be appended to our API base url
 */
function makeRequest(method, options) {
  let uri = options.uri;
  if (method === 'get' && options.params) {
    uri += `?${queryString.stringify(options.params)}`;
  }

  return Rx.Observable.ajax({
    headers: {
      'Content-Type': 'application/json;charset=utf-8',
      ...options.headers,
    },
    responseType: 'json',
    timeout: 60000,
    body: options.data || null,
    method,
    url: `http://www.website.com/api/v1/${uri}`, 
    // Most often you have a fixed API url so we just append a URI here to our fixed URL instead of repeating the API URL everywhere.
  })
  .flatMap(({ response }) => {
    /**
     * Here we handle our success callback, anyt actions returned from it will be dispatched.
     * You can return a single action or an array of actions to be dispatched eg. [{ type: ACTION_1 }, { type: ACTION_2 }].
     */
    if (options.onSuccess) {
      const observables = mapObservables(options.onSuccess(response));
      if (observables) {
        // This is only being called if our onSuccess callback returns any actions in which case we have to dispatch them
        return Rx.Observable.of(...observables);
      }
    }
    return Rx.Observable.of();
  })
  .catch((error) => {
      /**
       * This if case is to handle non-XHR errors gracefully that may be coming from elsewhere in our application when we fire 
       * an Observable.ajax request
       */
    if (!error.xhr) {
      if (options.onError) {
        const observables = mapObservables(options.onError(null)); // Note we pass null to our onError callback because it's not an XHR error
        if (observables) {
          // This is only being called if our onError callback returns any actions in which case we have to dispatch them
          return Rx.Observable.of(...observables);
        }
      }

      // You always have to ensure that you return an Observable, even if it's empty from all your Observables.
      return Rx.Observable.of();
    }

    const { xhr } = error;
    const { response } = error.xhr;
    const actions = [];
    const resArg = response || null;
    let message = null;

    if (xhr.status === 0) {
      message = 'Server is not responding.';
    } else if (xhr.status === 401) {
      // For instance we handle a 401 here, if you use react-router-redux you can simply push actions here to your router
      actions.push(
        replace('/login'),
      );
    } else if (
      response
      && response.errorMessage
    ) {
      /*
       * In this case the errorMessage parameter would refer to SampleApiResponse.json 400 example
       * { "errorMessage": "Invalid parameter." }
       */
      message = response.errorMessage;
    }

    if (options.onError) {
      // Here if our options contain an onError callback we can map the returned Actions and push them into our action payload
      mapObservables(options.onError(resArg)).forEach(o => actions.push(o));
    }

    if (message) {
      actions.push(showMessageAction(message));
    }

    /** 
     * You can return multiple actions in one observable by adding arguments Rx.Observable.of(action1, action2, ...) 
     * The actions always have to have a type { type: 'ACTION_1' }
     */
    return Rx.Observable.of(...actions);
  });
}

const API = {
  get: options => makeRequest('get', options),
  post: options => makeRequest('post', options),
  put: options => makeRequest('put', options),
  patch: options => makeRequest('patch', options),
  delete: options => makeRequest('delete', options),
};

export default API;

以下是动作,动作创作者和史诗:

import API from 'API';

const FETCH_PROFILE = 'FETCH_PROFILE';
const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
const FETCH_PROFILE_ERROR = 'FETCH_PROFILE_ERROR';
const FETCH_OTHER_THING = 'FETCH_OTHER_THING';
const FETCH_OTHER_THING_SUCCESS = 'FETCH_OTHER_THING_SUCCESS';
const FETCH_OTHER_THING_ERROR = 'FETCH_OTHER_THING_ERROR';

function fetchProfile(id) {
  return {
    type: FETCH_PROFILE,
    id,
  };
}

function fetchProfileSuccess(data) {
  return {
    type: FETCH_PROFILE_SUCCESS,
    data,
  };
}

function fetchProfileError(error) {
  return {
    type: FETCH_PROFILE_ERROR,
    error,
  };
}

function fetchOtherThing(id) {
  return {
    type: FETCH_OTHER_THING,
    id,
  };
}

const fetchProfileEpic = action$ => action$.
  ofType(FETCH_PROFILE)
  .switchMap(({ id }) => API.get({
    uri: 'profile',
    params: {
      id,
    },
    /*
     * We could also dispatch multiple actions using an array here you could dispatch another API request if needed
     * We can redispatch another action to fire another epic if we want also.
     * In both onSuccess and onError you can return a single action, an array of actions or null
     * Note here we fire fetchOtherThing(data.someOtherThingId) which will trigger our fetchOtherThingEpic!
     * In this case the data parameter would refer to SampleApiResponse.json 200 example
     * { firstName: "John", lastName: "Doe" }
     */
    onSuccess: ({ data }) => [fetchProfileSuccess(data), fetchOtherThing(data.someOtherThingId)],
    onError: error => fetchProfileError(error),
  })

const fetchOtherThingEpic = action$ => action$.
  ofType(FETCH_OTHER_THING)
  .switchMap(({ id }) => API.get({
    uri: 'other-thing',
    params: {
      id,
    },
    onSuccess: ...
    onError: ...
  });

以下是使用上述示例的数据示例:

/*
 * If possible, you should standardize your API response which will make error/data handling a lot easier on the client side
 * Note, this data format is to work with the example code above
 */

/**
  * Status code: 401
  * This error would be caught in the .catch() method of our API wrapper
  */
{
  "errorMessage": "Please login to perform this operation",
  "data": null,
}

/**
  * Status code: 400
  * This error would be caught in the .catch() method of our API wrapper
  *
  */
{
  "errorMessage": "Invalid parameter.",
  "data": null
}

/**
  * Status code: 200
  * This would be passed to our onSuccess function specified in our API options
  */
{
  "errorMessage": 'Please login to perform this operation',
  "data": { 
    "firstName": "John", 
    "lastName": "Doe" 
  }
}