Промежуточное ПО Api с наблюдаемым редуксом - PullRequest
0 голосов
/ 26 апреля 2019

Я рефакторинг моего приложения «Реакция / редукция», чтобы использовать redux-observable вместо redux-thunk. Используя thunk, я настроил промежуточное программное обеспечение API для прослушивания любых действий с помощью клавиши CALL_API и выполнения некоторых манипуляций с данными, подготовки заголовков, подготовки полного URL-адреса, выполнения вызова API с использованием axios, а также выполнения некоторых дополнительных действий. рассылка действий, связанных с вызовом API.

Важно, что промежуточное ПО api отправляет действие REQUEST_START, которое присваивает запросу идентификатор и устанавливает его статус на pending в части network моего состояния. Когда обещание от axios разрешается или отклоняется, промежуточное программное обеспечение отправляет действие REQUEST_END, обновляя состояние так, чтобы текущий запрос был установлен на resolved или rejected. Затем ответ возвращается создателю вызывающего действия, который первоначально отправил действие CALL_API.

Я не смог понять, как это сделать с redux-observable. Часть о промежуточном программном обеспечении API, описанная выше, которую я хочу воспроизвести, - это рассылки действий REQUEST_START и REQUEST_END. Очень удобно иметь централизованное место, где обрабатываются все вещи, связанные с вызовами API. Я знаю, что могу эффективно распределять действия REQUEST_START и REQUEST_END в каждой из моих эпопей, выполняющих вызов API, но я не хочу повторять один и тот же код во многих местах.

Мне удалось частично решить эту проблему, создав apiCallEpic, который прослушивает действия с типом CALL_API и выполняет вышеуказанную настройку для вызовов API. Однако проблема (или, скорее, что-то мне не нравится) заключается в том, что эпопея, которая инициирует вызов API (например, getCurrentUserEpic), по существу, передает контроль над apiCallEpic.

Так, например, когда вызов API успешен и имеет ответ, я, возможно, захочу отформатировать эти данные ответа перед отправкой действия, которое будет обработано моим редуктором. То есть getCurrentUserEpic должен выполнить некоторое форматирование данных, возвращаемых из вызова API, перед отправкой в ​​редуктор. Я смог достичь чего-то близкого к этому, передав функцию обратного вызова payloadHandler, определенную в getCurrentUserEpic, которую apiCallEpic может вызвать, если / когда получит успешный ответ. Однако мне не нравится эта архитектура обратного вызова, и кажется, что должен быть лучший способ.

Вот некоторый код, который демонстрирует мое использование промежуточного программного обеспечения API с использованием thunk.

import axios from 'axios';


// actionCreators.js

// action types
const CALL_API = "CALL_API";
const FETCH_CURRENT_USER = "FETCH_CURRENT_USER";
const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER";

// action creators for request start and end
export const reqStart = (params = {}) => (dispatch) => {
  const reduxAction = {
    type: REQ_START,
    status: 'pending',
    statusCode: null,
    requestId: params.requestId,
  }
  dispatch(reduxAction);
}

export const reqEnd = (params = {}) => (dispatch) => {
  const {
    requestId,
    response = null,
    error = null,
  } = params;

  let reduxAction = {}

  if (response) {
    reduxAction = {
      type: REQ_END,
      status: 'success',
      statusCode: response.status,
      requestId,
    }
  }
  else if (error) {
    if (error.response) {
      reduxAction = {
        type: REQ_END,
        status: 'failed',
        statusCode: error.response.status,
        requestId,
      }
    }
    else {
      reduxAction = {
        type: REQ_END,
        status: 'failed',
        statusCode: 500,
        requestId,
      }
    }
  }
  dispatch(reduxAction);
}

// some api call to fetch data
export const fetchCurrentUser = (params = {}) => (dispatch) => {
  const config = {
    url: '/current_user',
    method: 'get',
  }

  const apiCall = {
    [CALL_API]: {
      config,
      requestId: FETCH_CURRENT_USER,
    }
  }

  return dispatch(apiCall)
  .then(response => {
    dispatch({
      type: RECEIVE_CURRENT_USER,
      payload: {response},
    })
    return Promise.resolve({response});
  })
  .catch(error => {
    return Promise.reject({error});
  })
}




// apiMiddleware.js

// api endpoint
const API_ENTRY = "https://my-api.com";

// utility functions for request preparation
export const makeFullUrl = (params) => {
  // ...prepend endpoint url with API_ENTRY constant
  return fullUrl
}

export const makeHeaders = (params) => {
  // ...add auth token to headers, etc.
  return headers;
}


export default store => next => action => {
  const call = action[CALL_API];

  if (call === undefined) {
    return next(action);
  }

  const requestId = call.requestId;

  store.dispatch(reqStart({requestId}));

  const config = {
    ...call.config, 
    url: makeFullUrl(call.config),
    headers: makeHeaders(call.config);
  }

  return axios(config)
  .then(response => {
    store.dispatch(reqEnd({
      response,
      requestId,
    }))
    return Promise.resolve(response);
  })
  .catch(error => {
    store.dispatch(reqEnd({
      error,
      requestId,
    }))
    return Promise.reject(error);
  })
}


// reducers.js

// Not included, but you can imagine reducers handle the
// above defined action types and update the state
// accordingly. Most usefully, components can always
// subscribe to specific api calls and check the request
// status.  Showing loading indicators is one
// use case.

Вот код, который я реализовал для выполнения аналогичной операции с redux-observable.

export const fetchCurrentUserEpic = (action$, state$) => {
  const requestType = FETCH_CURRENT_USER;
  const successType = RECEIVE_CURRENT_USER;
  const requestConfig = {
    url: "/current_user",
    method: "get",
  }
  const payload = {requestConfig, requestType, successType};
  const payloadNormalizer = ({response}) => {
    return {currentUser: response.data.data};
  }

  return action$.ofType(FETCH_CURRENT_USER).pipe(
    switchMap((action) => of({
      type: CALL_API,
      payload: {...payload, requestId: action.requestId, shouldFail: action.shouldFail, payloadNormalizer},
    })),
  )
}

export const apiEpic = (action$, state$) => {
  return action$.ofType(CALL_API).pipe(
    mergeMap((action) => (
      concat(
        of({type: REQ_START, payload: {requestId: action.payload.requestId, requestType: action.payload.requestType}}),
        from(callApi(action.payload.requestConfig, action.payload.shouldFail)).pipe(
            map(response => {
              return {
                type: action.payload.successType,
                payload: action.payload.payloadNormalizer({response})
              }
            }),
            map(() => {
              return {
                type: REQ_END,
                payload: {status: 'success', requestId: action.payload.requestId, requestType: action.payload.requestType},
              }
           })
          )
        )
      ).pipe(
        catchError(error => {
          console.log('error', error);
          return of({type: REQ_END, payload: {status: 'failed', requestId: action.payload.requestId, requestType: action.payload.requestType}, error});
        })
      )
    )
  )
}

Любые комментарии или предложения приветствуются!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...