我正在重构我的react/redux应用程序,使用redux-observable而不是redux-thunk。通过使用thunk,我设置了一个api中间件来监听任何使用api键的操作,并对数据进行一些操作,准备报头,准备完整的url,使用axios执行api调用,还执行一些与api调用相关的其他操作分派。
重要的是,api中间件分派一个REQUEST_START操作,该操作为请求提供一个id,并在我的状态的network部分中将其状态设置为pending。当来自axios的promise解析或拒绝时,中间件分派一个REQUEST_END操作,更新状态,以便将当前请求设置为resolved或rejected。然后将响应返回给最初分派CALL_API操作的调用操作创建者。
我还不知道如何使用redux-observable来做到这一点。关于上面描述的api中间件,我想复制的部分是REQUEST_START和REQUEST_END操作分派。有一个集中的地方来处理所有与api调用相关的事情是非常方便的。我知道我可以有效地在执行REQUEST_START调用的每个epics中分派REQUEST_END和api操作,但我不想在许多地方重复相同的代码。
我设法部分地解决了这个问题,方法是创建一个侦听类型为CALL_API的操作并为apiCallEpic调用执行上述设置的api。然而,一个问题(或者更确切地说,我不喜欢的事情)是,发起api调用的epic (例如getCurrentUserEpic)本质上将控制权交给了apiCallEpic。
因此,例如,当api调用成功并有一个响应时,我可能希望在分派一个要由reducer处理的操作之前以某种方式格式化该响应数据。也就是说,在发送到reducer之前,getCurrentUserEpic应该对从api调用返回的数据进行一些格式化。通过传递在getCurrentUserEpic中定义的payloadHandler回调函数,apiCallEpic可以在获得成功响应时调用该回调函数,从而实现了与此类似的功能。然而,我不喜欢这种回调架构,而且似乎还有更好的方法。
这里有一些代码演示了我如何使用thunk来使用api中间件。
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});
})
)
)
)
}任何意见或建议,欢迎光临!
发布于 2019-12-07 20:55:26
我发现redux-fetch-epic-builder (一个用于构建"fetch actions“和由redux-observable处理的通用epics的库)与您在这里尝试实现的目标类似(注意它使用了rxjs5,this guide来拯救)。它使用的是fetch,而不是axios,但它很容易被替换。此外,它还具有用于成功/失败操作的转换器。
这个库有点老了,但是克服样板代码的基本思想仍然有效:泛型epic-builder通过调用API来获取数据。
我是React / Redux / RxJS的新手,但我看到的redux-fetch-epic-builder的唯一问题是配置client的方式(在axios术语中)。也就是说,我不完全满意(因为它不是FSA或RSAA):
//action creators
const getComments = (id, page = 1) => ({
type: GET_COMMENTS,
host: 'http://myblog.com',
path: `/posts/${id}/comments`,
query: {
page,
},
})
// ...
const epics = [
buildEpic(GET_COMMENTS),
]但这可能仍然是一种优雅的方式。并且许可证允许进一步开发该库。我没有将示例从库文档转换为与用户相关的示例,但是使用react-observable肯定不需要引入单独的"api中间件“。(此外,与_SUBACTION相比,我更喜欢/SUBACTION,但是更改它是微不足道的。)
https://stackoverflow.com/questions/55874276
复制相似问题