【问题标题】:how to handle errors in redux observable?如何处理 redux observable 中的错误?
【发布时间】:2018-01-10 15:57:00
【问题描述】:

我有以下代码:

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));

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

有没有办法“理解”其中一个“压缩”的 observables 出错,然后无法进入 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 中攻击这个问题的正确方法是什么?

谢谢。

【问题讨论】:

    标签: rxjs redux-observable


    【解决方案1】:

    这是一个详细的示例,其中包含一个 API 包装器,可以帮助您实现您想要实现的目标。

    要点可在 GitHub 上找到 here

    这里是包装 Observable.ajax 的 API 包装器,可让您分派单个动作或动作数组,并处理 XHR 和应用程序级别生成的错误,这些错误源于使用 Observable.ajax 发出的请求

    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" 
      }
    }
    

    【讨论】:

    • 感谢您的回答,但我还是不明白:if Observable.ajax('/first').catch(...); throws 或 Observable.ajax('/second').catch(...);抛出。我不会抓住合并的 observable 吗? const fetchBoth = () => (action, store) => Observable.merge( fetchFirst()(action, store), fetchSecond()(action, store), ) .catch(err => {我会到这部分吗? 从我在示例中看到的情况来看,我没有得到这个捕获,因为我们捕获了源 observable 并将其映射到一个动作})我会玩它并弄清楚:) 你的 API 想法很棒。跨度>
    • 有不同的方法可以做到这一点。我个人在 API 包装器中捕获,因此我不必在触发请求的每个可观察对象上实现捕获。如果您有一个具有一致错误约定的 API,并且您可以使用状态代码来处理您的错误,那么我会放弃 Observable 级别的错误捕获并将其全部集中到 API 包装器中。
    • 这是一个带有完整详细示例的要点。希望这能更彻底地解释如何构建 API 包装器和处理多个异步请求/动作调度/错误处理。 Click Here To See Gist on Github
    猜你喜欢
    • 1970-01-01
    • 2017-06-09
    • 1970-01-01
    • 2019-06-14
    • 2017-05-16
    • 1970-01-01
    • 2017-01-22
    • 2017-08-27
    • 1970-01-01
    相关资源
    最近更新 更多