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

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

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

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

  • React 全家桶
  • TypeScript
  • RxJS

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

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


1. 统一处理 HTTP 请求

1.1 为什么要这样做?

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

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

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

1.2 如何设计和实现?

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

1.2.1 Request Action

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

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

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

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

interface IRequestActionCreator<TReq, 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;
  };
}

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

const user: typeof getUser.TResp = { name: "Lee", age: 10 };

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

interface IRequestStartAction<T = any> {
  type: T; // xxx_START
  meta: {
    prevAction: IRequestAction; // 保存其对应的 reqeust action
  };
}

interface IRequestSuccessAction<T = any, TResp = any> {
  type: T; // xxx_SUCCESS
  payload: AxiosResponse<TResp>; // 保存 API Response
  meta: {
    prevAction: IRequestAction; 
  };
}

interface IRequestFailAction<T = any> {
  type: T; // xxx_FAIL
  error: true;
  payload: AxiosError; // 保存 Error
  meta: {
    prevAction: IRequestAction; 
  };
}

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

`${getData}` // "GET_DATA"
`${getData.start}` // "GET_DATA_START"
`${getData.success}` // "GET_DATA_SUCCESS"
`${getData.fail}`  // "GET_DATA_FAIL"

1.2.2 Request Middleware

接下来,我们需要创建一个 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 即可。

image.png

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

实现 useRequest Hook

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

interface IRequestCallbacks<TResp> {
  onSuccess?: (action: IRequestSuccessAction<TResp>) => void;
  onFail?: (action: IRequestFailAction) => void;
}

export enum RequestStage {
  START = "START",
  SUCCESS = "SUCCESS",
  FAILED = "FAIL",
}

const useRequest = <T extends IRequestActionCreator<T["TReq"], T["TResp"]>>(
  actionCreator: T,
  options: IRequestCallbacks<T["TResp"]> = {},
  deps: DependencyList = [],
) => {
  
  // ...
  
  return [request, requestStage$] as [typeof request, BehaviorSubject<RequestStage>];
};

它接收 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 ,可以应对很多复杂场景。

处理多个相互独立的 Request Action

同时发起多个不同的 request action,这些 request action 之间相互独立,并无关联。这种情况很简单,使用多个 useRequest 即可。

const [requestA] = useRequest(A);
const [requestB] = useRequest(B);
const [requestC] = useRequest(C);

useEffect(() => {
  requestA();
  requestB();
  requestC();
}, []);
处理多个相互关联的 Request Action

同时发起多个不同的 request action,这些 request action 之间有先后顺序。比如发起 A 请求,A 请求成功了之后发起 B 请求,B 请求成功了之后再发起 C 请求。

由于 useRequest 会创建发起请求的函数,并在请求成功之后执行 onSuccess 回调。因此,我们可以通过 useRequest 创建多个 request 函数,并预设它们成功响应之后的逻辑。就像 RXJS 中「预铺设管道」一样,当事件发生之后,系统会按照预设的管道运作。

// 预先创建所有的 request 函数,并预设 onSuccess 的逻辑
const [requestC] = useRequest(C);

const [requestB] = useRequest(B, {
  onSuccess: () => {
    requestC();
  },
});
const [requestA] = useRequest(A, {
  onSuccess: () => {
    requestB();
  },
});

// 当 requestA 真正调用之后,程序会按照预设的逻辑执行。

<form onSubmit={requestA}>
处理多个相同的 request action

同时发起多个完全相同的 request action,但是出于性能的考虑,我们通常会「吃掉」相同的 action,只有最后一个 action 会发起 API 请求。也就是我们前面提到过的 API 去重。但是对于 request action 的回调函数来说,可能会有下面两种不同的需求:

  1. 每个相同 request action 所对应的 onSuccess/onFail 回调在请求成功时都会被执行。
  2. 只执行真正发起请求的这个 action 所对应的 onSuccess/onFail 回调。

对于第一个场景来说,我们可以判断 action 的 type 和 payload 是否一致,如果一致就执行对应的 callback,这样相同 action 的回调都可以被执行。对于第二个场景,我们可以从 action 的 payload 上做点「手脚」,action 的 payload 放置的是我们发起请求时需要的 request config,通过添加一个 UUID,可以让这个和其他 action「相同」的 action 变得「不同」,这样就只会执行这个 request action 所对应的回调函数。

组件卸载

通常我们会使用 Promise 或者 XMLHttpRequest 发起 API 请求,但由于 API 请求是异步的,在组件卸载之后,它们的回调函数仍然会被执行。这就可能导致一些问题,比如在已卸载的组件里执行 setState。

组件被卸载之后,组件内部的逻辑应该随之「销毁」,我们不应该再执行任何组件内包含的任何逻辑。利用 RxJS,useRequest 能够在组件销毁时自动取消所有逻辑。换句话说,就是不再执行请求成功或者失败的回调函数。


2. 存储并使用请求响应的数据

对于 API Response 这一类数据,我们应该如何存储呢?由于不同的 API Response 数据对应用有着不同的作用,因此我们可以抽象出对应的数据模型,然后分类存储。就像我们收纳生活用品一样,第一个抽屉放餐具,第二个抽屉放零食......

image.png

按照数据变化的频率,或者说数据的存活时间,我们可以将 API response 大致归为两类:

一类是变化频率非常高的数据,比如排行榜列表,可能每一秒都在发生变化,这一类数据没有缓存价值,我们称之为临时数据(temporary data)。临时数据用完之后会被销毁。

另一类是不常发生变化的数据,我们称之为实体数据(entity),比如国家列表、品牌列表。这一类数据很多时候需要缓存到本地,将它们归为一类更易于做数据持久化。

2.1 useTempData

2.1.2 背景

通过 useRequest 我们已经能够非常方便的去调用 API 请求了。但是对于大部分业务场景来说,还是会比较繁琐。试想一个非常常见的需求:将 API 数据渲染到页面上。我们通常需要以下几个步骤:

Step1: 组件 mount 时,dispatch 一个 request action。这一步可以通过 useRequest 实现。

Step2: 处理 request success action,并将数据存入 store 中。

Step3: 从 store 的 state 中 pick 出对应的数据,并将其提供给组件。

Step4: 组件拿到数据并渲染页面。

Step5: 执行某些操作之后,用新的 request 参数重新发起请求。

Step6: 重复 Step2、Step3、Step4。

如果每一次集成 API 都要通过上面的这些步骤才能完成,不仅会浪费大量时间,也会生产大量模板代码。并且,由于逻辑非常地分散,我们无法为它们统一添加测试,因此需要在每个使用的地方单独去测。可想而知,开发效率一定会大打折扣。

为了解决这个问题,我们抽象了 useTempData。之前也提到过 temp data 的概念,其实它就是指页面上的临时数据,通常都是「阅后即焚」。我们项目上通过 API 请求获取的数据大部分都是这一类。useTempData 主要用于在组件 mount 时自动获取 API 数据,并在组件 unmount 时自动销毁它们。

2.1.3 输入和输出

useTempData 会在组件 mount 时自动分发 request action,当请求成功之后将响应数据存入 redux  store,然后从 store 提取出响应数据,将响应数据提供给外部使用。当然,你也可以通过配置,让 useTempData 响应请求参数的变化,当请求参数发生变化时,useTempData 会携带新的请求参数重新发起请求。

其核心的输入输出如下:

export const useTempData = <T extends IRequestActionCreator<T["TReq"], T["TResp"]>>(
  actionCreator: T,
  args?: T["TReq"],
  deps: DependencyList = [],
) => {
  // ...
  return [data, requestStage, fetchData] as [
    typeof actionCreator["TResp"],
    typeof requestStage,
    typeof fetchData,
  ];
};

它接收 actionCreator 作为第一个参数,用于创建相应的 request action。当组件 mount 时,会自动分发 request action。args 作为第二个参数,用于设置请求参数。 deps 作为第三个参数,当它发生变化时,会重新分发 request action。

同时,它会返回 API 响应的数据 data、表示请求当前所处阶段的 requestStage  以及用于分发 request action 的函数 fetchData 。

使用起来也非常方便,如果业务场景比较简单,集成 API 就是一行代码的事:

const [books] = useTempData(getBooksUsingGET, { bookType }, [bookType]);

// 拿到 books 数据,渲染 UI

2.1.4 实现思路

useTempData 基于 useRequest 实现。在组件 mount 时分发 request action,然后在请求成功的回调函数 onSuccess 中再分发另一个 action,将请求响应的数据存入 redux store。

const [fetchData] = useRequest(actionCreator, {
  success: (action) => {
    dispatch(updateTempData(groupName, reducer(dataRef.current, action))),
  },
});

useEffect(() => {
  fetchData(args as any);
}, deps);

2.1.5 组件卸载

当组件卸载时,如果 store 的 state 已经保存了这个 request action 成功响应的数据,useTempData 会自动将它清除。发起 API 请求之后,如果组件已经卸载,useTempData 就不会将请求成功响应的数据存入 redux store。

2.2 useEntity

基于 useTempData 的设计,我们可以封装 useEntity, 用于统一处理 entity 这类数据。这里不再赘述。


3. 自动生成代码

利用代码生成工具,我们可以通过 swagger 文档自动生成 request action creator 以及接口定义。并且,每一次都会用服务端最新的 swagger json 来生成代码。这在接口变更时非常有用,只需要一行命令,就可以更新接口定义,然后通过 TypeScript 的报错提示,依次修改使用的地方即可。

同一个 swagger 生成的代码我们会放到同一个文件里。在多人协作时,为了避免冲突,我们会将生成的 request action creator 以及接口定义按照字母顺序进行排序,并且每一次生成的文件都会覆盖之前的文件。因此,我们在项目上还硬性规定了:生成的文件只能自动生成,不能够手动修改。


4. 最后

自动生成代码工具为我们省去了很大一部分工作量,再结合我们之前讲过的 useRequest、useTempData 和 useEntity,集成 API 就变成了一项非常轻松的工作。


站长推荐

1.阿里云: 本站目前使用的是阿里云主机,安全/可靠/稳定。点击领取2000元代金券、了解最新阿里云产品的各种优惠活动点击进入

2.腾讯云: 提供云服务器、云数据库、云存储、视频与CDN、域名等服务。腾讯云各类产品的最新活动,优惠券领取点击进入

3.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/5804

Notification API,为你的网页添加桌面通知推送

其实,MDN 的说明已经可以让我们很清楚知道 Notification 的作用。Notification 能够为用户提供异步的桌面消息通知,即使你缩小浏览器或是活动在其他标签页,只要调用该 Api 的标签页没被关闭

Composition API

Composition API的主要思想是,我们将它们定义为从新的 setup 函数返回的JavaScript变量,而不是将组件的功能(例如state、method、computed等)定义为对象属性。

Vue3 Composition API 如何替换Vue Mixins

想在你的Vue组件之间共享代码?如果你熟悉 Vue 2 则可能知道使用 mixin ,但是新的 Composition API 提供了更好的解决方案。在本文中,我们将研究mixins的缺点,并了解Composition API如何克服它们

React的Context API

但是我们依然间接的使用着它,比如许多官方依赖在使用,如:react-redux, mobx-react,react-router。我们需要它功能的时候,更多是靠第三方依赖库就能实现,而不是自己手动写context。但是,依然需要理解它

超赞的腾讯短网址(微信url.cn短链接)生成api接口

腾讯短网址的应用场景很广,譬如短信营销、邮件推广、微信营销、QQ营销、自媒体推广、渠道推广等,都会用到短网址。究其原因是在于短网址可以降低推广成本、用户记忆成本,提高用户点击率;在特定的场景下推广还能规避关键词,防止域名被拦截

API是什么?

API就是接口,就是通道,负责一个程序和其他软件的沟通,本质是预先定义的函数。譬如我们去办事,窗口就类似一个API,如果对于某一件不简单的事情,这个窗口能做到让我们,当然,API不太一样,适用接口隔离原则,即使用多个隔离的接口

如何更好的设计 RESTful API

当您的数据模型已开始稳定,您可以为您的网络应用程序创建公共API。 你意识到,很难对你的API进行重大更改,一旦它发布,并希望尽可能得到尽可能多的前面。 现在,互联网对API设计的意见有很多。

API 接口设计规范

这篇文章分享 API 接口设计规范,目的是提供给研发人员做参考。规范是死的,人是活的,希望自己定的规范,不要被打脸。url?后面的参数,存放请求接口的参数数据。

JSON Web 令牌(JWT)是如何保护 API 的?

你可以已经听说过 JSON Web Token (JWT) 是目前用于保护 API 的最新技术。与大多数安全主题一样,如果你打算使用它,那很有必要去了解它的工作原理(一定程度上)。问题在于,对 JWT 的大多数解释都是技术性的,这一点让人很头疼。

scrollIntoView 与 scrollIntoViewIfNeeded API 介绍

Element.scrollIntoView()方法让当前的元素滚动到浏览器窗口的可视区域内。而Element.scrollIntoViewIfNeeded()方法也是用来将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域。但如果该元素已经在浏览器窗口的可见区域内

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!

文章投稿关于web前端网站点搜索站长推荐网站地图站长QQ:522607023

小程序专栏: 土味情话心理测试脑筋急转弯幽默笑话段子句子语录成语大全运营推广