使用fetch api进行Redux异步请求

时间:2016-02-13 14:46:42

标签: javascript fetch race-condition redux es6-promise

我陷入了一种我无法调试的奇怪行为。

商店调度执行登录请求的操作,传递用户名和密码。然后,当响应准备就绪时,我将凭证存储在redux存储中。当我需要执行授权请求时,我在头请求中设置这些参数。当我收到响应时,我使用从响应中获得的新凭据更新商店中的凭据。 当我尝试执行第三个请求时,它将以未经授权的方式响应。我发现这是因为传递给我的动作生成器setCredentials的所有参数都是null。我也无法理解为什么,因为如果我在setCredentials函数的return语句之前添加调试器并且在重新开始执行之前等待几秒钟,我发现参数不再是null。我正在考虑这样一个事实,即请求是异步的但是在then语句中,响应应该准备好了吗?我还注意到fetch为每个请求发送了两个请求。 这里的代码更清晰。

import { combineReducers } from 'redux'
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const initialState = {
  currentUser: {
    credentials: {},
    user: {}
  },
  test: {},
  users: []
}

export const SUBMIT_LOGIN = 'SUBMIT_LOGIN'
export const SET_USER = 'SET_USER'
export const TEST = 'TEST'
export const SET_USERS = 'SET_USERS'
export const SET_CREDENTIALS = 'SET_CREDENTIALS'

//actions
const submitLogin = () => (dispatch) => {
  return postLoginRequest()
    .then(response => {
      dispatch(setCredentials(
        response.headers.get('access-token'),
        response.headers.get('client'),
        response.headers.get('expiry'),
        response.headers.get('token-type'),
        response.headers.get('uid')
      ));
      return response
    })
    .then(response => {
      return response.json();
    })
    .then(
      (user) => dispatch(setUser(user.data)),
    );
}

const performRequest = (api) => (dispatch) => {
  return api()
    .then(response => {
      dispatch(setCredentials(
        response.headers.get('access-token'),
        response.headers.get('client'),
        response.headers.get('expiry'),
        response.headers.get('token-type'),
        response.headers.get('uid')
      ));
      return response
    })
    .then(response => {return response.json()})
    .then(
      (users) => {
        dispatch(setUsers(users.data))
      },
    );
}

const setUsers = (users) => {
  return {
    type: SET_USERS,
    users
  }
}

const setUser = (user) => {
  return {
    type: SET_USER,
    user
  }
}

const setCredentials = (
  access_token,
  client,
  expiry,
  token_type,
  uid
) => {
  debugger
  return {
    type: SET_CREDENTIALS,
    credentials: {
      'access-token': access_token,
      client,
      expiry,
      'token-type': token_type,
      uid
    }
  }
}

//////////////
const currentUserInitialState = {
  credentials: {},
  user: {}
}

const currentUser = (state = currentUserInitialState, action) => {
  switch (action.type) {
    case SET_USER:
      return Object.assign({}, state, {user: action.user})
    case SET_CREDENTIALS:
      return Object.assign({}, state, {credentials: action.credentials})
    default:
      return state
  }
}

const rootReducer = combineReducers({
  currentUser,
  test
})

const getAuthorizedHeader = (store) => {
  const credentials = store.getState().currentUser.credentials
  const headers = new Headers(credentials)
  return headers
}

//store creation

const createStoreWithMiddleware = applyMiddleware(
  thunk
)(createStore);

const store = createStoreWithMiddleware(rootReducer);

const postLoginRequest = () => {
  return fetch('http://localhost:3000/auth/sign_in', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      email: 'test@test.com',
      password: 'password',
    })
  })
}

const getUsers = () => {
  const autorizedHeader = getAuthorizedHeader(store)
  return fetch('http://localhost:3000/users',
    {
      method: 'GET',
      headers : autorizedHeader
    }
  )
}

const getWorks = () => {
  const autorizedHeader = getAuthorizedHeader(store)
  return fetch('http://localhost:3000/work_offers',
    {
      method: 'GET',
      headers : autorizedHeader
    }
  )
}
// this request works fine
store.dispatch(submitLogin())

// this request works fine
setTimeout(() => {
  store.dispatch(performRequest(getUsers))
}, 3000)

// this fails
setTimeout(() => {
  store.dispatch(performRequest(getWorks))
}, 5000)

1 个答案:

答案 0 :(得分:2)

当我问

时,我应该澄清一下
  

您是否确认所有端点都返回了这些标头而不仅仅是登录标头?也许当你performRequest(getUsers)时,它会返回空标题。

我不仅仅意味着服务器逻辑。我的意思是在DevTools中打开“网络”选项卡,实际上验证您的回复是否包含您期望的标题。事实证明getUsers()标题并不总是包含凭据:

现在我们确认了这种情况,让我们看看为什么。

您同时发送submitLogin()performRequest(getUsers) 大致。在再现错误的情况下,问题在于以下步骤序列:

  1. 您关闭了submitLogin()
  2. performRequest(getUsers)返回
  3. 之前,您将关闭submitLogin()
  4. submitLogin()返回并存储响应标头中的凭据
  5. performRequest(getUsers)返回但是,因为它在凭据可用之前启动,服务器以空标头响应,并且存储那些空凭据而不是现有凭证
  6. 现在,在没有凭据的情况下请求
  7. performRequest(getWorks)

    此问题有几种解决方法。

    不要让旧的未经授权的请求覆盖凭证

    我不认为用空的覆盖现有的良好凭证是否真的有意义,是吗?在发送之前,您可以在performRequest中检查它们是非空的:

    const performRequest = (api) => (dispatch, getState) => {
      return api()
        .then(response => {
          if (response.headers.get('access-token')) {
            dispatch(setCredentials(
              response.headers.get('access-token'),
              response.headers.get('client'),
              response.headers.get('expiry'),
              response.headers.get('token-type'),
              response.headers.get('uid')
            ));
          }
          return response
        })
        .then(response => {return response.json()})
        .then(
          (users) => {
            dispatch(setUsers(users.data))
          },
        );
    }
    

    或者,您可以忽略reducer本身中的无效凭据:

    case SET_CREDENTIALS:
      if (action.credentials['access-token']) {
        return Object.assign({}, state, {credentials: action.credentials})
      } else {
        return state
      }
    

    这两种方式都很好,取决于对你更有意义的惯例。

    在执行请求之前等待

    在任何情况下,你真的想在获得凭据之前解雇getUsers() 吗?如果没有,请仅在凭据可用之前触发请求。像这样:

    store.dispatch(submitLogin()).then(() => {
      store.dispatch(performRequest(getUsers))
      store.dispatch(performRequest(getWorks))
    })
    

    如果它不总是可行或者您想要更复杂的逻辑,例如重试失败的请求,我建议您查看Redux Saga,它允许您使用强大的并发原语来安排此类工作。