在前端开发的过程当中,咱们可能会花很多的时间去集成 API、与 API 联调、或者解决 API 变更带来的问题。若是你也但愿减轻这部分负担,提升团队的开发效率,那么这篇文章必定会对你有所帮助。前端
文章中使用到的技术栈主要有:react
文章中会讲述集成 API 时遇到的一些复杂场景,并给出对应解决方案。经过本身写的小工具,自动生成 API 集成的代码,极大提高团队开发效率。ios
本文的全部代码都在这个仓库:request。git
自动生成代码的工具在这里:ts-codegen。github
咱们能够直接经过 fetch 或者 XMLHttpRequest 发起 HTTP 请求。可是,若是在每一个调用 API 的地方都采用这种方式,可能会产生大量模板代码,并且很难应对一些业务场景:typescript
所以,为了减小模板代码并应对各类复杂业务场景,咱们须要对 HTTP 请求进行统一处理。json
经过 redux,咱们能够将 API 请求 「action 化」。换句话说,就是将 API 请求转化成 redux 中的 action。一般来讲,一个 API 请求会转化为三个不一样的 action: request action、request start action、request success/fail action。分别用于发起 API 请求,记录请求开始、请求成功响应和请求失败的状态。而后,针对不一样的业务场景,咱们能够实现不一样的 middleware 去处理这些 action。redux
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"
接下来,咱们须要建立一个 middleware 来统一处理 request action。middleware 的逻辑很简单,就是拦截全部的 request action,而后发起 HTTP 请求:
这里须要注意的是,request middleware 须要「吃掉」request action,也就是说不把这个 action 交给下游的 middleware 进行处理。一是由于逻辑已经在这个 middleware 处理完成了,下游的 middleware 无需处理这类 action。二是由于若是下游的 middleware 也 dispatch request action,会形成死循环,引起没必要要的问题。
咱们能够经过分发 request action 来触发请求的调用。而后在 reducer 中去处理 request success action,将请求的响应数据存入 redux store。
可是,不少时候咱们不只要发起 API 请求,还要在 请求成功 和 请求失败 的时候去执行一些逻辑。这些逻辑不会对 state 形成影响,所以不须要在 reducer 中去处理。好比:用户填写了一个表单,点击 submit 按钮时发起 API 请求,当 API 请求成功后执行页面跳转。这个问题用 Promise 很好解决,你只须要将逻辑放到它的 then 和 catch 中便可。然而,将请求 「action化」以后,咱们不能像 Promise 同样,在调用请求的同时注册请求成功和失败的回调。
如何解决这个问题呢?咱们能够实现一种相似 Promise 的调用方式,容许咱们在分发 request action 的同时去注册请求成功和失败的回调。也就是咱们即将介绍的 useRequest。
为了让发起请求、请求成功和请求失败这几个阶段再也不割裂,咱们设计了 onSuccess
和 onFail
回调。相似于 Promise 的 then 和 catch。但愿可以像下面这样去触发 API 请求的调用:
// 伪代码 useRequest(xxxActionCreator, { onSuccess: (requestSuccessAction) => { // do something when request success }, onFail: (requestFailAction) => { // do something when request fail }, });
Promise 和 callback 都像「泼出去的水」,正所谓「覆水难收」,一旦它们开始执行便没法取消。若是遇到须要「取消」的场景就会比较尴尬。虽然能够经过一些方法绕过这个问题,但始终以为代码不够优雅。所以,咱们引入了 RxJS,尝试用一种新的思路去探索并解决这个问题。
咱们能够改造 redux 的 dispatch
方法,在每次 dispatch 一个 action 以前,再 dispatch 一个 subject$
(观察者)。接着,在 middleware 中建立一个 rootSubject$
(可观察对象),用于拦截 dispatch 过来的 subject$
,并让它成为 rootSubject$
的观察者。rootSubject$
会把 dispatch 过来的 action 推送给它的全部观察者。所以,只须要观察请求成功和失败的 action,执行对应的 callback 便可。
利用 Rx 自身的特性,咱们能够方便地控制复杂的异步流程,固然也包括取消。
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 之间相互独立,并没有关联。这种状况很简单,使用多个 useRequest
便可。
const [requestA] = useRequest(A); const [requestB] = useRequest(B); const [requestC] = useRequest(C); useEffect(() => { requestA(); requestB(); requestC(); }, []);
同时发起多个不一样的 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,可是出于性能的考虑,咱们一般会「吃掉」相同的 action,只有最后一个 action 会发起 API 请求。也就是咱们前面提到过的 API 去重。可是对于 request action 的回调函数来讲,可能会有下面两种不一样的需求:
对于第一个场景来讲,咱们能够判断 action 的 type 和 payload 是否一致,若是一致就执行对应的 callback,这样相同 action 的回调均可以被执行。对于第二个场景,咱们能够从 action 的 payload 上作点「手脚」,action 的 payload 放置的是咱们发起请求时须要的 request config,经过添加一个 UUID,可让这个和其余 action「相同」的 action 变得「不一样」,这样就只会执行这个 request action 所对应的回调函数。
一般咱们会使用 Promise 或者 XMLHttpRequest 发起 API 请求,但因为 API 请求是异步的,在组件卸载以后,它们的回调函数仍然会被执行。这就可能致使一些问题,好比在已卸载的组件里执行 setState。
组件被卸载以后,组件内部的逻辑应该随之「销毁」,咱们不该该再执行任何组件内包含的任何逻辑。利用 RxJS,useRequest 可以在组件销毁时自动取消全部逻辑。换句话说,就是再也不执行请求成功或者失败的回调函数。
对于 API Response 这一类数据,咱们应该如何存储呢?因为不一样的 API Response 数据对应用有着不一样的做用,所以咱们能够抽象出对应的数据模型,而后分类存储。就像咱们收纳生活用品同样,第一个抽屉放餐具,第二个抽屉放零食......
按照数据变化的频率,或者说数据的存活时间,咱们能够将 API response 大体归为两类:
一类是变化频率很是高的数据,好比排行榜列表,可能每一秒都在发生变化,这一类数据没有缓存价值,咱们称之为临时数据(temporary data)。临时数据用完以后会被销毁。
另外一类是不常发生变化的数据,咱们称之为实体数据(entity),好比国家列表、品牌列表。这一类数据不少时候须要缓存到本地,将它们归为一类更易于作数据持久化。
经过 useRequest 咱们已经可以很是方便的去调用 API 请求了。可是对于大部分业务场景来讲,仍是会比较繁琐。试想一个很是常见的需求:将 API 数据渲染到页面上。咱们一般须要如下几个步骤:
Step1: 组件 mount 时,dispatch 一个 request action。这一步能够经过 useRequest 实现。
Step2: 处理 request success action,并将数据存入 store 中。
Step3: 从 store 的 state 中 pick 出对应的数据,并将其提供给组件。
Step4: 组件拿到数据并渲染页面。
Step5: 执行某些操做以后,用新的 request 参数从新发起请求。
Step6: 重复 Step二、Step三、Step4。
若是每一次集成 API 都要经过上面的这些步骤才能完成,不只会浪费大量时间,也会生产大量模板代码。而且,因为逻辑很是地分散,咱们没法为它们统一添加测试,所以须要在每一个使用的地方单独去测。可想而知,开发效率必定会大打折扣。
为了解决这个问题,咱们抽象了 useTempData。以前也提到过 temp data 的概念,其实它就是指页面上的临时数据,一般都是「阅后即焚」。咱们项目上经过 API 请求获取的数据大部分都是这一类。useTempData 主要用于在组件 mount 时自动获取 API 数据,并在组件 unmount 时自动销毁它们。
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
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);
当组件卸载时,若是 store 的 state 已经保存了这个 request action 成功响应的数据,useTempData 会自动将它清除。发起 API 请求以后,若是组件已经卸载,useTempData 就不会将请求成功响应的数据存入 redux store。
基于 useTempData 的设计,咱们能够封装 useEntity, 用于统一处理 entity 这类数据。这里再也不赘述。
利用代码生成工具,咱们能够经过 swagger 文档自动生成 request action creator 以及接口定义。而且,每一次都会用服务端最新的 swagger json 来生成代码。这在接口变动时很是有用,只须要一行命令,就能够更新接口定义,而后经过 TypeScript 的报错提示,依次修改使用的地方便可。
同一个 swagger 生成的代码咱们会放到同一个文件里。在多人协做时,为了不冲突,咱们会将生成的 request action creator 以及接口定义按照字母顺序进行排序,而且每一次生成的文件都会覆盖以前的文件。所以,咱们在项目上还硬性规定了:生成的文件只能自动生成,不可以手动修改。
自动生成代码工具为咱们省去了很大一部分工做量,再结合咱们以前讲过的 useRequest、useTempData 和 useEntity,集成 API 就变成了一项很是轻松的工做。