react redux-thunk项目中的模拟api返回未定义

Posted

技术标签:

【中文标题】react redux-thunk项目中的模拟api返回未定义【英文标题】:Mock api in react redux-thunk project returning undefined 【发布时间】:2019-02-05 03:10:14 【问题描述】:

我是 redux 世界的新手,还没有一个以ducks 方式构建的项目。我试图理解它并用它来制作一个模拟 api,因为我还没有准备好后端。我正在使用我试图弄清楚的遗留代码。有一个名为 data 的文件夹,其中有一个 duck 和一个 backendApi 文件。 Duck 文件长这样。

data/duck.jsx

import  createSelector  from 'reselect';
import  createReduxApi  from './backendApi';

const getDataContext = state => state.default.dataContext;

const backendReduxApi = createBackendReduxApi(getDataContext);

// Action creators
export const makeRestApiRequest = endpointName => backendReduxApi .makeRequestActionCreator(endpointName);

export const resetRestApi = endpointName => backendReduxApi .makeResetActionCreator(endpointName);

// Reducers
export const dataReducer = backendReduxApi .createReducer();

// Selectors
const getRestApiState = endpointName => backendReduxApi .getEndpointState(endpointName);
export const getRestApiData = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.data);
export const getRestApiMeta = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.meta);
export const getRestApiError = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.error);
export const getRestApiStarted = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.started);
export const getRestApiFinished = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.finished);

backendApi.jsx 文件如下所示:

数据/backendApi.jsx

import ReduxRestApi from './rest/ReduxRestApi';

export const BackendApi =  // NOSONAR
  LANGUAGE_FILE: 'languageFile',
  EMPLOYEE: 'employee',
;

const backendReduxApiBuilder = ReduxRestApi.build()
  /* /api */

  /* /api/employee */
  .withGet('/myproject/api/employee', BackendApi.EMPLOYEE)

  /* /language*/
  .withGet('/myproject/language/nb_NO.json', BackendApi.LANGUAGE_FILE)

export const createBackendReduxApi = restApiSelector => backendReduxApiBuilder
  .withRestApiSelector(restApiSelector)
  .create();

然后在 data/rest 文件夹中我有 4 个文件:ReduxRestApirestConfigRestDuckrestMethods

data/rest/ReduxRestApi.jsx

import  combineReducers  from 'redux';
import  get, post, postAndOpenBlob  from './restMethods';
import RestDuck from './RestDuck';

class ReduxRestApi 
  constructor(endpoints, getRestApiState) 
    this.createReducer = this.createReducer.bind(this);
    this.getEndpoint = this.getEndpoint.bind(this);
    this.makeRequestActionCreator = this.makeRequestActionCreator.bind(this);
    this.makeResetActionCreator = this.makeResetActionCreator.bind(this);
    this.getEndpointState = this.getEndpointState.bind(this);
    this.ducks = endpoints.map(( name, path, restMethod ) => new RestDuck(name, path, restMethod, getRestApiState));
  

  createReducer() 
    const reducers = this.ducks
      .map(duck => ( [duck.name]: duck.reducer ))
      .reduce((a, b) => ( ...a, ...b ), );
    return combineReducers(reducers);
  

  getEndpoint(endpointName) 
    return this.ducks.find(duck => duck.name === endpointName)
      ||  actionCreators:  ;
  

  makeRequestActionCreator(endpointName) 
    return this.getEndpoint(endpointName).actionCreators.execRequest;
  

  makeResetActionCreator(endpointName) 
    return this.getEndpoint(endpointName).actionCreators.reset;
  

  getEndpointState(endpointName) 
    return this.getEndpoint(endpointName).stateSelector;
  

  static build() 
    class RestApiBuilder 
      constructor() 
        this.withGet = this.withGet.bind(this);
        this.withPost = this.withPost.bind(this);
        this.withPostAndOpenBlob = this.withPostAndOpenBlob.bind(this);
        this.withRestApiSelector = this.withRestApiSelector.bind(this);
        this.endpoints = [];
      

      withGet(path, name) 
        this.endpoints.push( path, name, restMethod: get );
        return this;
      

      withPost(path, name) 
        this.endpoints.push( path, name, restMethod: post );
        return this;
      

      withPostAndOpenBlob(path, name) 
        this.endpoints.push( path, name, restMethod: postAndOpenBlob );
        return this;
      

      withRestApiSelector(restApiSelector) 
        this.restApiSelector = restApiSelector;
        return this;
      

      create() 
        return new ReduxRestApi(
          this.endpoints,
          this.restApiSelector
        );
      
    

    return new RestApiBuilder();
  


export default ReduxRestApi;

restConfig.jsx

import axios from 'axios';
import  removeErrorMessage, showErrorMessage  from '../../app/duck';
import  is401Error, isHandledError  from '../../app/ErrorTypes';

const isDevelopment = process.env.NODE_ENV === 'development';

const configureRequestInterceptors = (store) => 
  const onRequestAccepted = (config) => 
    store.dispatch(removeErrorMessage());
    return config;
  ;

  const onRequestRejected = error => Promise.reject(error);

  axios.interceptors.request.use(onRequestAccepted, onRequestRejected);
;

const configureResponseInterceptors = (store) => 
  const onSuccessResponse = response => response;

  const onErrorResponse = (error) => 
    if (is401Error(error) && !isDevelopment) 
      window.location.reload();
    
    if (!isHandledError(error)) 
      store.dispatch(showErrorMessage(error));
    
    return Promise.reject(error);
  ;

  axios.interceptors.response.use(onSuccessResponse, onErrorResponse);
;

const configureRestInterceptors = (store) => 
  configureRequestInterceptors(store);
  configureResponseInterceptors(store);
;

export default configureRestInterceptors;

data/rest/RestDuck.jsx

import  createSelector  from 'reselect';

import  get, getBlob, post, postAndOpenBlob, postBlob  from './restMethods';

/**
 * getMethodName
 * Helper function that maps given AJAX-method to a name
 *
 * Ex. getMethodName(getBlob) -> 'GET'
 */
const getMethodName = (restMethod) => 
  switch (restMethod) 
    case get:
    case getBlob:
      return 'GET';
    case post:
    case postBlob:
    case postAndOpenBlob:
      return 'POST';
    default:
      return '';
  
;

/**
 * createRequestActionType
 * Helper function to generate actionType for actions related to AJAX calls
 *
 * Ex: createRequestActionType('fetchEmployee', 'ERROR', get, '/myproject/api/employee') -> '@@REST/fetchEmployee GET /myproject/api/employeeERROR'
 */
const createRequestActionType = (name, qualifier, restMethod = '', path = '') => [`@@REST/$name`, getMethodName(restMethod), path, qualifier]
  .filter(s => s !== '')
  .join(' ');

/**
 * createRequestActionTypes
 * Helper function to generate ActionTypes for a given AJAX method and resource.
 *
 * Ex. createRequestActionType(fetchEmployee, get, '/myproject/api/employee') -> 
 *   reset: '@@REST GET /myproject/api/employee RESET',
 *   requestStarted: '@@REST GET /myproject/api/employee STARTED',
 *   requestError: '@@REST GET /myproject/api/employee ERROR',
 *   requestFinished: '@@REST GET /myproject/api/employee FINISHED',
 * 
 */
const createRequestActionTypes = (name, restMethod, path) => (
  reset: createRequestActionType(name, 'RESET'),
  requestStarted: createRequestActionType(name, 'STARTED', restMethod, path),
  requestError: createRequestActionType(name, 'ERROR', restMethod, path),
  requestFinished: createRequestActionType(name, 'FINISHED', restMethod, path)
);

/**
 * createRequestThunk
 * Helper function that generates a thunk that performs an AJAX call specified by 'restMethod' and 'restEndpoint'
 *
 * When the thunk is running, the action 'requestStarted' will be dispatched immediately.
 * Then, it performs the AJAX call that returns a promise.
 *  If the call goes well, the action 'requestFinished' will be dispatched with data from the call.
 * If the call fails, the action 'requestError' is dispatched with the contents of the error.
 */
const createRequestThunk = (restMethod, restEndpoint, requestStarted, requestFinished, requestError) => (
  (params, options = ) => (dispatch) => 
    dispatch(requestStarted(params, options));
    return restMethod(restEndpoint, params)
      .catch((error) => 
        const data = error.response && error.response.data ? error.response.data : error;
        dispatch(requestError(data));
        return Promise.reject(error);
      )
      .then((response) => 
        dispatch(requestFinished(response.data));
        return response;
      );
  
);

/**
 * createRequestActionCreators
 * Helper function that creates action creators 'requestStarted', 'requestFinished' and 'requestError',
 * @see createRequestThunkCreator
 */
const createRequestActionCreators = (restMethod, restEndpoint, actionTypes) => 
  const reset = () => ( type: actionTypes.reset );
  const requestStarted = (params, options = ) => ( type: actionTypes.requestStarted, payload:  params, timestamp: Date.now() , meta:  options  );
  const requestFinished = data => ( type: actionTypes.requestFinished, payload: data );
  const requestError = error => ( type: actionTypes.requestError, payload: error );
  const execRequest = createRequestThunk(restMethod, restEndpoint, requestStarted, requestFinished, requestError);
  return 
    reset, requestStarted, requestFinished, requestError, execRequest
  ;
;

/**
 * createRequestReducer
 *
 * Helper function that creates a reducer for an AJAX call.
 * Reducer alters the state of the actions with the name defined by
 *   actionTypes.requestStarted
 *   actionTypes.requestFinished
 *   actionTypes.requestError
 */
const createRequestReducer = (restMethod, resourceName, actionTypes) => 
  const initialState = 
    data: undefined,
    meta: undefined,
    error: undefined,
    started: false,
    finished: false
  ;

  return (state = initialState, action = ) => 
    switch (action.type) 
      case actionTypes.requestStarted:
        return 
          ...initialState,
          data: action.meta.options.keepData ? state.data : initialState.data,
          started: true,
          meta: action.payload
        ;
      case actionTypes.requestFinished:
        return 
          ...state,
          started: false,
          finished: true,
          data: action.payload
        ;
      case actionTypes.requestError:
        return 
          ...state,
          started: false,
          error: action.payload
        ;
      case actionTypes.reset:
        return 
          ...initialState
        ;
      default:
        return state;
    
  ;
;

/**
 * RestDuck
 * Class that offers action types, action creators, reducers and selectors for an AJAX call.
 * @see createRequestActionTypes
 * @see createRequestActionCreators
 * @see createRequestReducer
 *
 * Ex.
 * const getEmployeeDuck = new RestDuck(execGetRequest, 'employee', GET_EMPLOYEE_SERVER_URL);
 * // Action creators
 * export const fetchEmployee = getEmployeeDuck.actionCreators.execRequest;
 * // Reducer
 * export const dataReducer = combineReducers(
 *   ...,
 *   getEmployeeDuck.reducer,
 * 
 * // Selectors
 * export const getDataContext = state => state.default.dataContext;
 * export const getEmployeeData = getEmployeeDuck.selectors.getRequestData(getDataContext);
 * export const getEmployeeStarted = getEmployeeDuck.selectors.getRequestStarted(getDataContext);
 * ...
 */
class RestDuck 
  constructor(name, path, restMethod, getApiContext) 
    this.restMethod = restMethod;
    this.name = name;
    this.path = path;
    this.getApiContext = getApiContext;
    this.$$duck = ; // for class internal use
  

  get actionTypes() 
    if (!this.$$duck.actionTypes) 
      this.$$duck.actionTypes = createRequestActionTypes(this.name, this.restMethod, this.path);
    
    return this.$$duck.actionTypes;
  

  get actionCreators() 
    if (!this.$$duck.actionCreators) 
      this.$$duck.actionCreators = createRequestActionCreators(this.restMethod, this.path, this.actionTypes);
    
    return this.$$duck.actionCreators;
  

  get reducer() 
    if (!this.$$duck.reducer) 
      this.$$duck.reducer = createRequestReducer(this.restMethod, this.name, this.actionTypes);
    
    return this.$$duck.reducer;
  

  get stateSelector() 
    return createSelector([this.getApiContext], restApiContext => restApiContext[this.name]);
  


export default RestDuck;

data/rest/restMethods.jsx

import axios,  CancelToken  from 'axios';

const openPreview = (data) => 
  if (window.navigator.msSaveOrOpenBlob) 
    window.navigator.msSaveOrOpenBlob(data);
   else 
    window.open(URL.createObjectURL(data));
  
;

const cancellable = (config) => 
  let cancel;
  const request = axios(
    ...config,
    cancelToken: new CancelToken((c) =>  cancel = c; )
  );
  request.cancel = cancel;
  return request.catch(error => (axios.isCancel(error) ? Promise.reject(new Error(null)) : Promise.reject(error)));
;

const defaultHeaders = 
  'Cache-Control': 'no-cache',
  Pragma: 'no-cache',
  Expires: 0
;

const defaultPostHeaders = 
  'Content-Type': 'application/json'
;

export const get = (url, params, responseType = 'json') => cancellable(
  url,
  params,
  responseType,
  method: 'get',
  headers: 
    ...defaultHeaders
  
);

export const post = (url, data, responseType = 'json') => cancellable(
  url,
  responseType,
  data: JSON.stringify(data),
  method: 'post',
  headers: 
    ...defaultHeaders,
    ...defaultPostHeaders
  ,
  cache: false
);

export const getBlob = (url, params) => get(url, params, 'blob');

export const postBlob = (url, data) => post(url, data, 'blob');

export const postAndOpenBlob = (url, data) => postBlob(url, data)
  .then((response) => 
    openPreview(response.data);
    return 
      ...response,
      data: 'blob opened as preview' // Don't waste memory by storing blob in state
    ;
  );

我不知道在哪里放置以及如何在这个结构中进行模拟 api 调用。我正在考虑制作一个类似于 one 的模拟 api,在那里我会模仿 ajax 调用并将它们存储在 redux 中,但只是不确定如何在这种设置中执行此操作?

我尝试制作 mockApi 文件夹,而不是使用 restMethods,而是使用我将在其中编写可以解析 mockData 的 promise 的文件。这是我的尝试:

mockRestMethods

const employee = 
  name: 'Joe Doe'

const data = 
  employee 
;

export const get = item => new Promise((resolve) => 
  setTimeout(() => 
    resolve( data: data[item] );
  , 1000);
);

但是,如果我检查 RestDuck 文件中的 createRequestThunk 函数中的 response.data 返回的内容,我会在那里得到 data: undefined。为什么会这样,我做错了什么?

【问题讨论】:

你能在codesandbox.io/s/new上创建一个***.com/help/mcve吗? 【参考方案1】:

我可能有这个错误,但似乎你正在替换

export const get = (url, params, responseType = 'json') => cancellable(

export const get = item => new Promise((resolve) => 具有不同的 API。

无论如何,您是否尝试过在模拟 get 函数中记录 item 的值。我猜这不是“员工”,这是data 中唯一的属性。


是的,这是我的目标,将指向后端 API 的调用替换为返回模拟数据的调用。我试图记录项目的值,但我得到未定义

好的,所以那里有很多抽象。我建议首先将data/rest/restMethods.jsx 中的get 直接替换为返回承诺的版本,让它工作,然后将其分解。这样你就不会一次处理太多的未知数。

【讨论】:

是的,这是我的目标,将指向后端 API 的调用替换为返回模拟数据的调用。我试图记录item 的值,但我得到undefined 我试过直接替换方法,但是我遇到了同样的问题,我无法让promise方法工作。【参考方案2】:

我使用 redux-saga 做过类似的事情。经过调试,我发现必须有data属性作为根键。你应该这样做:

const employee = 
  data:  // root key of employee
    items: [
       name: 'Bhojendra' ,
       name: 'Rauniyar' 
    ]
  


// and no need to use setTimeout, we're just resolving some constant data
export const getItems = item => new Promise(resolve => resolve(employee))

现在,我希望您知道为什么 data 是 undefined 与您的代码。

还是不清楚?

响应查找 data 属性。就是这样。

【讨论】:

以上是关于react redux-thunk项目中的模拟api返回未定义的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 redux-thunk 调度操作让 connected-react-router 重定向?

在 React-redux 和 Redux-Thunk 中从 API 获取数据的问题

Redux-thunk 异步调用和状态

如何使用 redux-thunk `bindActionCreators`

Redux 进阶之 react-redux 和 redux-thunk 的应用

在 redux-thunk 中循环 api 调用(React redux 应用程序)