来自 Web前端 2020-04-29 17:43 的文章
当前位置: 网上澳门金莎娱乐 > Web前端 > 正文

基于 React 和 Redux 的 API 集成解决方案

时间: 2019-09-08阅读: 141标签: api

如何使用saga:
1.使用createSagaMiddleware方法创建saga 的Middleware

在前端开发的过程中,我们可能会花不少的时间去集成 API、与 API 联调、或者解决 API 变动带来的问题。如果你也希望减轻这部分负担,提高团队的开发效率,那么这篇文章一定会对你有所帮助。

const sagaMiddleware = createSagaMiddleware()

文章中使用到的技术栈主要有:

2.在创建的redux的store时,使用applyMiddleware函数将创建的saga Middleware实例绑定到store上.

React 全家桶TypeScriptRxJS

applyMiddleware(sagaMiddleware,middleware)

文章中会讲述集成 API 时遇到的一些复杂场景,并给出对应解决方案。通过自己写的小工具,自动生成 API 集成的代码,极大提升团队开发效率。

3.最后可以调用saga Middleware的run函数来执行某个或者某些Middleware。

本文的所有代码都在这个仓库:request。自动生成代码的工具在这里:ts-codegen。

const store = createStoreWithMiddleware(rootReducer);
sagaMiddleware.run(rootSaga);
  1. 统一处理 HTTP 请求1.1 为什么要这样做?

原理:
在saga中,我们可以使用takeEvery或者takeLatest等API来监听某个action,当某个action触发后,saga可以使用call、fetch等api发起异步操作,操作完成后使用put函数触发action,同步更新state,从而完成整个State的更新。
例子1:
action

我们可以直接通过 fetch 或者 XMLHttpRequest 发起 HTTP 请求。但是,如果在每个调用 API 的地方都采用这种方式,可能会产生大量模板代码,而且很难应对一些业务场景:

export function requireDatas(reddit) : Action{
    return {
        type:GETDATA,
        reddit
    }
}
export function isLoading() : Action{
    return {
        type:ISLOADING,
        proload: true
    }
}
export function receiveDatas(reddit) : Action{
    return {
        type:RECEIVEDATAS,
        reddit
    }
}

如何为所有的请求添加 loading 动画?如何统一显示请求失败之后的错误信息?如何实现 API 去重?如何通过 Google Analytics 追踪请求?

当点击按钮,dispatch requireDatas 这个action,然后异步获取数据,然后,更新state。
设置saga:

因此,为了减少模板代码并应对各种复杂业务场景,我们需要对 HTTP 请求进行统一处理。

export  function getDataApi(reddit){
    let res =  axios.get('http://localhost:3000/getData',{params:reddit.reddit})
    return res;
}

export  function* requireDatas(reddit){
    yield put( actions.default.isLoading() )
    const params = yield call(getDataApi, reddit);
    yield put( actions.default.receiveDatas(params.data) )
}
export  function* watchGetData(){
     yield takeEvery(GETDATA,requireDatas)
}

1.2 如何设计和实现?

takeEvery监听'GETDATA',当分发这个action的时候,执行requireDatas。
requireDatas:使用call 调用getDataApi获取数据,然后执行put方法,相当于dispatch 'RECEIVEDATAS' 这个action,从而state发生改变。

通过 redux,我们可以将 API 请求 「action 化」。换句话说,就是将 API 请求转化成 redux 中的 action。通常来说,一个 API 请求会转化为三个不同的 action: request action、request start action、request success/fail action。分别用于发起 API 请求,记录请求开始、请求成功响应和请求失败的状态。然后,针对不同的业务场景,我们可以实现不同的 middleware 去处理这些 action。

API

1.2.1 Request Action

takeEvery

用来监听action。当监听到出发此action的时候,如果执行异步操作的话,无论第一次异步是否完成,当发生第二次的时候,都会继续执行。

export  function* requireDatas(reddit){
    const params = yield call(getDataApi, reddit);
    yield put( actions.default.receiveDatas(params.data) )
}
export  function* watchGetData(){
     yield takeEvery(GETDATA,requireDatas)
}

在上面的例子中,takeEvery 允许多个 requireDatas实例同时启动。即使第一个还没结束,第二个依然会启动。

redux 的 dispatch 是一个同步方法,默认只用于分发 action (普通对象)。但通过 middleware,我们可以 dispatch 任何东西,比如 function (redux-thunk) 和 observable,只要确保它们被拦截即可。

takeLatest

作用同takeEvery一样,唯一的区别是它只关注最后,也就是最近一次发起的异步请求,如果上次请求还未返回,则会被取消。

export  function* watchGetData(){
     yield takeLatest(GETDATA,requireDatas)
}

只执行最后一次出发的 requireDatas;

要实现异步的 HTTP 请求,我们需要一种特殊的 action,本文称之为request action。request action 会携带请求参数的信息,以便之后发起 HTTP 请求时使用。与其他 action 不同的是,它需要一个request属性作为标识。其定义如下:

put
yield put({ type: 'PRODUCTS_RECEIVED', products })

相当于dispatch({ type: 'PRODUCTS_RECEIVED', products })

interface IRequestActionT = any { type: T meta: { request: true // 标记 request action }; payload: AxiosRequestConfig; // 请求参数}
call

call用来调用异步函数,将异步函数和函数参数作为call函数的参数传入,返回一个对象。
saga引入他的主要作用是方便测试,同时也能让我们的代码更加规范化。

export  function getDataApi(reddit){
    let res =  axios.get('http://localhost:3000/getData',{params:reddit.reddit})
    return res;
}
export  function* requireDatas(reddit){
    const params = yield call(getDataApi, reddit);
    yield put( actions.default.receiveDatas(params.data) )
}

redux 的 action 一直饱受诟病的一点,就是会产生大量模板代码而且纯字符串的 type 也很容易写错。所以官方不推荐我们直接使用 action 对象,而是通过action creator函数来生成相应的 action。比如社区推出的 redux-actions,就能够帮助我们很好地创建 action creator。参考它的实现,我们可以实现一个函数createRequestActionCreator,用于创建如下定义的 action creator:

apply

saga同样提供apply函数,作用同call一样,参数形式同js原生apply方法。

interface IRequestActionCreatorTReq, TResp = any, TMeta = any { (args: TReq, extraMeta?: TMeta): IRequestAction; TReq: TReq; // 请求参数的类型 TResp: TResp; // 请求响应的类型 $name: string; // request action creator 函数的名字 toString: () = string; start: { toString: () = string; }; success: { toString: () = string; }; fail: { toString: () = string; };}
cps

同call方法基本一样,但是用处不太一样,call一般用来完成异步操作,cps可以用来完成耗时比较长的io操作等。
cps(fn, ...args)
fn: Function - 一个 Node 风格的函数。即不仅接受它自己的参数,fn 结束后会调用一个附加的回调函数。回调函数接受两个参数,第一个参数是报错信息,第二个是成功的结果。
args: Array - 一个数组,作为 fn 的参数
参考自 官方文档

在上面的代码中,TReq 和 TResp 分别表示请求参数的类型请求响应的类型。它们保存在 request action creator 函数的原型上。这样,通过 request action creator,我们就能迅速知道一个 API 请求参数的类型和响应数据的类型。

fork

非阻塞任务调用机制:相对于generator函数来说,call操作是阻塞的,只有等promise回来后才能继续执行,而fork是非阻塞的 ,当调用fork启动一个任务时,该任务在后台继续执行,从而使得我们的执行流能继续往下执行而不必一定要等待返回。
我们可以把上面的例子换成:

function* loginFlow() {
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    const task = yield fork(authorize, user, password)
    const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
    if(action.type === 'LOGOUT')
      yield cancel(task)
    yield call(Api.clearItem('token'))
  }
}
constuser:typeofgetUser.TResp = { name:"Lee", age:10};
take

takeEvery 在每个 action 来到时派生一个新的任务,它只是监听每个action,然后执行处理函数。被调用的任务无法控制何时被调用, 它们将在每次 action 被匹配时一遍又一遍地被调用。并且它们也无法控制何时停止监听。
而take则不一样,我们可以在generator函数中决定何时响应一个action,以及一个action被触发后做什么操作。

export  function fetchPostsApi(reddit){
    let res =  axios.get('http://localhost:3000/getData',{params:reddit.reddit})
    return res;
}

export  function* requireDatas(reddit){
    yield put( actions.default.isLoading() )
    const params = yield call(fetchPostsApi, reddit);
    yield put( actions.default.receiveDatas(params.data) )
}
export  function* watchGetData(){
     while(true) {
        const params = yield take(GETDATA);
        yield fork(requireDatas,params)
     }
     // yield takeEvery(GETDATA,requireDatas)
}

对于 API 请求来说,请求开始、请求成功和请求失败这几个节点非常重要。因为每一个节点都有可能触发 UI 的改变。我们可以定义三种特定 type 的 action 来记录每个异步阶段。也就是我们上面提到的 request start action、request success action 和 request fail action,其定义如下:

cancel

cancel的作用是用来取消一个还未返回的fork任务。防止fork的任务等待时间太长或者其他逻辑错误。

interface IRequestStartActionT = any { type: T; // xxx_START meta: { prevAction: IRequestAction; // 保存其对应的 reqeust action };}interface IRequestSuccessActionT = any, TResp = any { type: T; // xxx_SUCCESS payload: AxiosResponseTResp; // 保存 API Response meta: { prevAction: IRequestAction; };}interface IRequestFailActionT = any { type: T; // xxx_FAIL error: true; payload: AxiosError; // 保存 Error meta: { prevAction: IRequestAction; };}
call

类似Promise中的all操作,可以将多个异步操作作为参数参入all函数中,如果有一个call操作失败或者所有call操作都成功返回,则本次all操作执行完毕。

const [users, repos]  = yield all([
  call(fetch, '/users'),
  call(fetch, '/repos')
])

在上面的代码中,我们在 request action creator 的原型上绑定了toString方法,以及start、success和fail属性。因为 action type 是纯字符串,手写很容易出错,所以我们希望通过 request action creator 直接获取它们的 type,就像下面这样:

race

类似Promise中的race操作,可以将多个异步操作作为参数参入race函数中,如果有一个race操作失败或者成功,则本次race操作执行完毕。

yield race({
      task: call(backgroundTask),
      cancel: take('CANCEL_TASK')
    })
`${getData}` // "GET_DATA"`${getData.start}` // "GET_DATA_START"`${getData.success}` // "GET_DATA_SUCCESS"`${getData.fail}` // "GET_DATA_FAIL"
Interface Task

Task 接口指定了通过 fork,middleware.run 或 runSaga 执行 Saga 的结果。
task.isRunning() 如果任务还未返回或抛出了一个错误则返回 true
task.result() 任务的返回值。如果任务正在执行中则返回 undefined
task.error() 任务抛出的错误。如果任务正在执行中则返回 undefined
task.done 一个 Promise,以下二者之一:
以任务的返回值 resolve
以任务抛出的错误 reject
task.cancel() 取消任务(如果任务还在执行中)

1.2.2 Request Middleware

actionChannel

在之前的操作中,所有的action分发是顺序的,但是对action的响应是由异步任务来完成,也即是说对action的处理是无序的。
  如果需要对action的有序处理的话,可以使用actionChannel函数来创建一个action的缓存队列,但一个action的任务流程处理完成后,才可是执行下一个任务流。

function* watchRequests() {
  // 1- Create a channel for request actions
  const requestChan = yield actionChannel('REQUEST')
  while (true) {
    // 2- take from the channel
    const {payload} = yield take(requestChan)
    // 3- Note that we're using a blocking call
    yield call(handleRequest, payload)
  }
}

详情请参考这个开源项目以及官方文档
其他关于redux异步
更多文章...

接下来,我们需要创建一个 middleware 来统一处理 request action。middleware 的逻辑很简单,就是拦截所有的 request action,然后发起 HTTP 请求:

请求开始:dispatch xxx_STAT action,方便显示 loading请求成功:携带 API Response,dispatch xxx_SUCCESS action请求失败:携带 Error 信息,dispatch xxx_FAIL action

这里需要注意的是,request middleware 需要「吃掉」request action,也就是说不把这个 action 交给下游的 middleware 进行处理。一是因为逻辑已经在这个 middleware 处理完成了,下游的 middleware 无需处理这类 action。二是因为如果下游的 middleware 也 dispatch request action,会造成死循环,引发不必要的问题。

1.3 如何使用?

我们可以通过分发 request action 来触发请求的调用。然后在reducer 中去处理 request success action,将请求的响应数据存入 redux store。

但是,很多时候我们不仅要发起 API 请求,还要在请求成功请求失败的时候去执行一些逻辑。这些逻辑不会对 state 造成影响,因此不需要在 reducer 中去处理。比如:用户填写了一个表单,点击 submit 按钮时发起 API 请求,当 API 请求成功后执行页面跳转。这个问题用 Promise 很好解决,你只需要将逻辑放到它的 then 和 catch 中即可。然而,将请求 「action化」之后,我们不能像 Promise 一样,在调用请求的同时注册请求成功和失败的回调。

如何解决这个问题呢?我们可以实现一种类似 Promise 的调用方式,允许我们在分发 request action 的同时去注册请求成功和失败的回调。也就是我们即将介绍的 useRequest。

1.3.1 useRequest:基于 React Hooks 和 RXJS 调用请求

为了让发起请求、请求成功和请求失败这几个阶段不再割裂,我们设计了onSuccess和onFail回调。类似于 Promise 的 then 和 catch。希望能够像下面这样去触发 API 请求的调用:

// 伪代码useRequest(xxxActionCreator, { onSuccess: (requestSuccessAction) = { // do something when request success }, onFail: (requestFailAction) = { // do something when request fail },});

通过 RxJS 处理请求成功和失败的回调

Promise 和 callback 都像「泼出去的水」,正所谓「覆水难收」,一旦它们开始执行便无法取消。如果遇到需要「取消」的场景就会比较尴尬。虽然可以通过一些方法绕过这个问题,但始终觉得代码不够优雅。因此,我们引入了 RxJS,尝试用一种新的思路去探索并解决这个问题。

我们可以改造 redux 的dispatch方法,在每次 dispatch 一个 action 之前,再 dispatch 一个subject$(观察者)。接着,在 middleware 中创建一个rootSubject$(可观察对象),用于拦截 dispatch 过来的subject$,并让它成为rootSubject$的观察者。rootSubject$会把 dispatch 过来的 action 推送给它的所有观察者。因此,只需要观察请求成功和失败的 action,执行对应的 callback 即可。

利用 Rx 自身的特性,我们可以方便地控制复杂的异步流程,当然也包括取消。

实现 useRequest Hook

useRequest提供用于分发 request action 的函数,同时在请求成功或失败时,执行相应的回调函数。它的输入和输出大致如下:

interface IRequestCallbacksTResp { onSuccess?: (action: IRequestSuccessActionTResp) = void; onFail?: (action: IRequestFailAction) = void;}export enum RequestStage { START = "START", SUCCESS = "SUCCESS", FAILED = "FAIL",}const useRequest = T extends IRequestActionCreatorT["TReq"], T["TResp"]( actionCreator: T, options: IRequestCallbacksT["TResp"] = {}, deps: DependencyList = [],) = { // ... return [request, requestStage$] as [typeof request, BehaviorSubjectRequestStage];};

它接收actionCreator作为第一个参数,并返回一个request 函数,当你调用这个函数时,就可以分发相应的 request action从而发起 API 请求

同时它也会返回一个可观察对象requestStage$(可观察对象),用于推送当前请求所处的阶段。其中包括:请求开始、成功和失败三个阶段。这样,在发起请求之后,我们就能够轻松地追踪到它的状态。这在一些场景下非常有用,比如当请求开始时,在页面上显示 loading 动画,请求结束时关闭这个动画。

为什么返回可观察对象requestStage$而不是返回requestStage状态呢?如果返回状态,意味着在请求开始、请求成功和请求失败时都需要去 setState。但并不是每一个场景都需要这个状态。对于不需要这个状态的组件来说,就会造成一些浪费(re-render)。因此,我们返回一个可观察对象,当你需要用到这个状态时,去订阅它就好了。

options作为它的第二个参数,你可以通过它来指定onSuccess和onFail回调。onSuccess 会将 request success action 作为参数提供给你,你可以通过它拿到请求成功响应之后的数据。然后,你可以选择将数据存入 redux store,或是 local state,又或者你根本不在乎它的响应数据,只是为了在请求成功时去跳转页面。但无论如何,通过 useRequest,我们都能更加便捷地去实现需求。

const [getBooks] = useRequest(getBooksUsingGET, { success: (action) = { saveBooksToStore(action.payload.data); // 将 response 数据存入 redux store },});const onSubmit = (values: { name: string; price: number }) = { getBooks(values);};

复杂场景

useRequest封装了调用请求的逻辑,通过组合多个useRequest,可以应对很多复杂场景。

本文由网上澳门金莎娱乐发布于Web前端,转载请注明出处:基于 React 和 Redux 的 API 集成解决方案

关键词: