【问题标题】:Redux async requests with fetch api使用 fetch api 的 Redux 异步请求
【发布时间】:2016-02-13 14:46:42
【问题描述】:

我陷入了无法真正调试的奇怪行为。

商店调度执行登录请求并传递用户名和密码的操作。然后,当响应准备好时,我将凭据存储在 redux 存储中。当我需要执行授权请求时,我在标头请求中设置这些参数。当我收到响应时,我会使用从响应中获得的新凭据更新商店中的凭据。 当我尝试执行第三个请求时,它将未经授权响应。我发现这是因为传递给我的动作生成器 setCredentials 的所有参数都是空的。我也无法理解为什么,因为如果我在 setCredentials 函数的 return 语句之前添加一个调试器,并在重新启动执行之前等待几秒钟,我发现参数不再为空。我在考虑请求是异步的但在 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)

【问题讨论】:

  • 您是否已验证 all 您的端点返回这些标头而不仅仅是登录标头?也许当你performRequest(getUsers) 时,它会返回空标题。
  • 是的。已验证。对我的后端的每个授权请求都会返回这些标头。 @丹
  • 唉,很难从代码中猜到。如果您可以创建一个在 JSBin 中重现该问题的独立示例,我可以看看。
  • 非常感谢。我会做的。 @丹
  • 嗨,丹。我为测试创建了这个repo。谢谢@DanAbramov

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


【解决方案1】:

当我问的时候我应该澄清一下

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

我指的不仅仅是服务器逻辑。我的意思是在 DevTools 中打开 Network 选项卡并实际验证您的响应是否包含您期望的标题。事实证明getUsers() 标头并不总是包含凭据:

现在我们确认会发生这种情况,让我们看看原因。

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

  1. 你开火了submitLogin()
  2. 你在submitLogin() 回来之前关闭performRequest(getUsers)
  3. submitLogin() 返回并存储来自响应标头的凭据
  4. performRequest(getUsers) 回来了,但由于它在凭据可用之前开始,服务器以空标头响应,并且存储这些空凭据而不是现有凭据
  5. 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,它可以让您使用强大的并发原语来安排此类工作。

【讨论】:

  • 我认为这可能是问题所在,但我不明白为什么会发生这种情况,因为performRequest(getUsers)performRequest(getUsers)setTimeout 函数中被触发。我认为登录请求应该在 3 秒内返回响应。无论如何,您是否认为这是管理凭据的好方法(我的意思是将它们保存在商店中)?非常感谢您的宝贵时间。 @丹
  • 嗯,依靠在一段时间内准确 的响应并不是很明智,至少在生产代码中是这样的:-)。你可以放更多的日志,看看我的诊断是否正确。不幸的是,我现在没有时间再次打开你的项目并为你做这件事:-)。是的,这种方法看起来不错。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-09
  • 2018-05-12
  • 2019-01-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多