零、问题的由来

开门见山地说,这篇文章是一篇安利软文~,安利的对象就是最近搞的 tua-api

顾名思义,这就是一款辅助获取接口数据的工具。

发请求相关的工具辣么多,那我为啥要用你呢?

理想状态下,项目中应该有一个 api 中间层。各种接口在这里定义,业务侧不应该手动编写接口地址,而应该调用接口层导出的函数。

  1. import { fooApi } from '@/apis/'
  2. fooApi
  3. .bar({ a: '1', b: '2' }) // 发起请求,a、b 是请求参数
  4. .then(console.log) // 收到响应
  5. .catch(console.error) // 处理错误

那么如何组织实现这个 api 中间层呢?这里涉及两方面:

  • 如何发请求,即“武器”部分
  • 如何组织管理 api 地址

让我们先回顾一下有关发请求的历史。

一、如何发请求

1.1.原生 XHR (XMLHttpRequest)

说到发请求,最经典的方式莫过于调用浏览器原生的 XHR。在此不赘述,有兴趣可以看看MDN 上的文档

  1. var xhr = window.XMLHttpRequest
  2. ? new XMLHttpRequest()
  3. // 在万恶的 IE 上可能还没有 XMLHttpRequest 这对象
  4. : new ActiveXObject('Microsoft.XMLHTTP')
  5. xhr.open('GET', 'some url')
  6. xhr.responseType = 'json'
  7. // 传统使用 onreadystatechange
  8. xhr.onreadystatechange = function () {
  9. if (xhr.readyState === 4 && xhr.status === 200) {
  10. console.log(xhr.responseText)
  11. }
  12. }
  13. // 或者直接使用 onload 事件
  14. xhr.onload = function () {
  15. console.log(xhr.response)
  16. }
  17. // 处理出错
  18. xhr.onerror = console.error
  19. xhr.send()

这代码都不用看,想想就头皮发麻...

1.2.jQuery 封装的 ajax

由于原生 XHR 写起来太繁琐,再加上当时 jQuery 如日中天。日常开发中用的比较多的还是 jQuery 提供的 ajax 方法。jQuery ajax 文档点这里

  1. var params = {
  2. url: 'some url',
  3. data: { name: 'Steve', location: 'Beijing' },
  4. }
  5. $.ajax(params)
  6. .done(console.log)
  7. .fail(console.error)

jQuery 不仅封装了 XHR,还十分贴心地提供跨域的 jsonp 功能。

  1. $.ajax({
  2. url: 'some url',
  3. data: { name: 'Steve', location: 'Beijing' },
  4. dataType: 'jsonp',
  5. success: console.log,
  6. error: console.error,
  7. })

讲道理,jQuery 的 ajax 已经很好用了。然而随着 Vue、React、Angular 的兴起,连 jQuery 本身都被革命了。新项目为了发个请求还引入巨大的 jQuery 肯定不合理,当然后面这些替代方案也功不可没...

1.3.现代浏览器的原生 fetch

XHR 是一个设计粗糙的 API。记得当年笔试某部门的实习生的时候就有手写 XHR 的题目,我反正记不住 api,并没有写出来...

fetch api 基于 Promise 设计,调用起来比 XHR 方便多了。

  1. fetch(url)
  2. .then(res => res.json())
  3. .then(console.log)
  4. .catch(console.error)

async/await 自然也能使用

  1. try {
  2. const data = await fetch(url).then(res => res.json())
  3. console.log(data)
  4. } catch (e) {
  5. console.error(e)
  6. }

当然 fetch 也有不少的问题

  • 兼容性问题
  • 使用繁琐,详见参考文献之 fetch 没有你想象的那么美
  • 不支持 jsonp(虽然理论上不应该支持,但实际上日常还是需要使用的)
  • 只对网络请求报错,对400,500都当做成功的请求,需要二次封装
  • 默认不会带 cookie,需要添加配置项
  • 不支持 abort,不支持超时控制,使用 setTimeout 及 Promise.race 的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
  • 没有办法原生监测请求的进度,而 XHR 可以

1.4.基于 Promise 的 axios

axios 算是请求框架中的明星项目了。目前 github 5w+ 的 star...

先来看看有什么特性吧~

  • 同时支持浏览器端和服务端的请求。(XMLHttpRequests、http)
  • 支持 Promise
  • 支持请求和和数据返回的拦截
  • 转换请求返回数据,自动转换JSON数据
  • 支持取消请求
  • 客户端防止 xsrf 攻击

嗯,看起来确实是居家旅行全栈开发必备好库,但是 axios 并不支持 jsonp...

1.5.不得不用的 jsonp

在服务器端不方便配置跨域头的情况下,采用 jsonp 的方式发起跨域请求是一种常规操作。

在此不探究具体的实现,原理上来说就是

  • 由于 script 标签可以设置跨域的来源,所以首先动态插入一个 script,将 src 设置为目标地址
  • 服务端收到请求后,根据回调函数名(可自己约定,或作为参数传递)将 json 数据填入(即 json padding,所以叫 jsonp...)。例如 callback({ "foo": "bar" })
  • 浏览器端收到响应后自然会执行该 script 即调用该函数,那么回调函数就收到了服务端填入的 json 数据了。

上面讲到新项目一般都弃用 jQuery 了,那么跨域请求还是得发呀。所以可能你还需要一个发送 jsonp 的库。(实践中选了 fetch-jsonp,当然其他库也可以)

综上,日常开发在框架的使用上以 axios 为主,实在不得不发 jsonp 请求时,就用 fetch-jsonp。这就是我们中间层的基础,即“武器”部分。

1.6.小程序场景

在小程序场景没得选,只能使用官方的 wx.request 函数...

二、构建接口层基础功能

对于简单的页面,直接裸写请求地址也没毛病。但是一旦项目变大,页面数量也上去了,直接在页面,或是组件中裸写接口的话,会带来以下问题

  • 代码冗余:很多接口请求都是类似的代码,有许多相同的逻辑
  • 不同的库和场景下的接口写法不同(ajax、jsonp、小程序...)
  • 不方便切换测试域名
  • 不方便编写接口注释
  • 没法实现统一拦截器、甚至中间件功能

如何封装这些接口呢?

2.1.接口地址划分

首先我们来分析一下接口地址的组成

  • https://example-base.com/foo/create
  • https://example-base.com/foo/modify
  • https://example-base.com/foo/delete

对于以上地址,在 tua-api 中一般将其分为3部分

  • host: 'https://example-base.com/'
  • prefix: 'foo'
  • pathList: [ 'create', 'modify', 'delete' ]

2.2.文件结构

apis/ 一般是这样的文件结构:

  1. .
  2. └── apis
  3. ├── prefix-1.js
  4. ├── prefix-2.js
  5. ├── foo.js // <-- 以上的 api 地址会放在这里
  6. └── index.js

index.js 作为接口层的入口,会导入并生成各个 api 然后再导出。

2.3.基础配置内容

所以以上的示例接口地址可以这么写

  1. // src/apis/foo.js
  2. export default {
  3. // 请求的公用服务器地址。
  4. host: 'http://example-base.com/',
  5. // 请求的中间路径,建议与文件同名,以便后期维护。
  6. prefix: 'foo',
  7. // 接口地址数组
  8. pathList: [
  9. { path: 'create' },
  10. { path: 'modify' },
  11. { path: 'delete' },
  12. ],
  13. }

这时如果想修改服务器地址,只需要修改 host 即可。甚至还能这么玩

  1. // src/apis/foo.js
  2. // 某个获取页面地址参数的函数
  3. const getUrlParams = () => {...}
  4. export default {
  5. // 根据 NODE_ENV 采用不同的服务器
  6. host: process.env.NODE_ENV === 'test'
  7. ? 'http://example-test.com/'
  8. : 'http://example-base.com/',
  9. // 根据页面参数采用不同的服务器,即页面地址带 ?test=1 则走测试地址
  10. host: getUrlParams().test
  11. ? 'http://example-test.com/'
  12. : 'http://example-base.com/',
  13. // ...
  14. }

2.4.配置导出

下面来看一下 apis/index.js 该怎么写:

  1. import TuaApi from 'tua-api'
  2. // 小程序端这样导入
  3. // import TuaApi from 'tua-api/dist/mp'
  4. // 初始化
  5. const tuaApi = new TuaApi({ ... })
  6. // 导出
  7. export const fooApi = tuaApi.getApi(require('./foo').default)

这样我们就把接口地址封装了起来,业务侧不需要关心接口的逻辑,而后期接口的修改和升级时只需要修改这里的配置即可。

2.5.接口参数与接口类型

示例的接口地址太理想化了,如果有参数如何传递?

假设以上接口添加 id、from 和 foo 参数。并且增加以下逻辑:

  • foo 参数默认填 bar
  • from 参数默认填 index-page
  • delete 接口使用 jsonp 的方式,from 参数默认填 delete-page
  • modify 接口使用 post 的方式,from 参数不需要填

哎~,别急着死,暂且看看怎么用 tua-api 来抽象这些逻辑?

  1. // src/apis/foo.js
  2. export default {
  3. // ...
  4. // 公共参数,将会合并到后面的各个接口参数中
  5. commonParams: {
  6. foo: 'bar',
  7. from: 'index-page',
  8. },
  9. pathList: [
  10. {
  11. path: 'create',
  12. params: {
  13. // 类似 Vue 中 props 的类型检查
  14. id: { required: true },
  15. },
  16. },
  17. {
  18. path: 'modify',
  19. // 使用 post 的方式
  20. type: 'post',
  21. params: {
  22. // 写成 isRequired 也行
  23. id: { isRequired: true },
  24. // 接口不合并公共参数,即不传 from 参数
  25. commonParams: null,
  26. },
  27. },
  28. {
  29. path: 'delete',
  30. // 使用 jsonp 的方式(不填则默认使用 axios)
  31. reqType: 'jsonp',
  32. params: {
  33. id: { required: true },
  34. // 这里填写的 from 会覆盖 commonParams 中的同名属性
  35. from: 'delete-page',
  36. },
  37. },
  38. ],
  39. }

现在来看看业务侧代码有什么变化。

  1. import { fooApi } from '@/apis/'
  2. // 直接调用将会报错,因为没有传递 id 参数
  3. await fooApi.create()
  4. // 请求参数使用传入的 from:id=1&foo=bar&from=foo-page
  5. await fooApi.create({ id: 1, from: 'foo-page' })
  6. // 请求参数将只有 id:id=1
  7. await fooApi.modify({ id: 1 })
  8. // 请求参数将使用自身的 from:id=1&foo=bar&from=delete-page
  9. await fooApi.delete({ id: 1 })

2.6.接口重命名

假设现在后台又添加了以下两个新接口,咱们该怎么写配置呢?

  • remove/all
  • add-array

首先,把后台同学砍死...2333

这什么鬼接口地址,直接填的话会业务侧就会写成这样。

  1. fooApi['remove/all']
  2. fooApi['add-array']

这代码简直无法直视...让我们用 name 属性,将接口重命名一下。

  1. // src/apis/foo.js
  2. export default {
  3. // ...
  4. pathList: [
  5. // ...
  6. { path: 'remove/all', name: 'removeAll' },
  7. { path: 'add-array', name: 'addArray' },
  8. ],
  9. }

更多配置请点击这里查看

三、高级功能

一个接口层仅仅只能发 api 请求是远远不够的,在日常使用中往往还有以下需求

  • 发起请求时展示 loading,收到响应后隐藏
  • 出错时展示错误信息,例如弹一个 toast
  • 接口上报:包括性能和错误
  • 添加特技:如接口参数加密、校验

3.1.小程序端的 loading 展示

小程序端由于原生自带 UI 组件,所以框架内置了该功能。主要包括以下参数

  • isShowLoading
  • showLoadingFn
  • hideLoadingFn

顾名思义,就是开关和具体的显示、隐藏的方法,详情参阅这里

3.2.基础钩子函数

最简单的钩子函数就是 beforeFn/afterFn 这俩函数了。

beforeFn 是在请求发起前执行的函数(例如小程序可以通过返回 header 传递 cookie),因为是通过 beforeFn().then(...) 调用,所以注意要返回 Promise。

afterFn 是在收到响应后执行的函数,可以不用返回 Promise。

注意接收的参数是一个【数组】 [ res.data, ctx ]

所以默认值是 const afterFn = ([x]) => x,即返回接口数据到业务侧

  • 第一个参数是接口返回的数据对象 { code, data, msg }
  • 第二个参数是请求相关参数的对象,例如有请求的 host、type、params、fullPath、reqTime、startTime、endTime 等等

3.3.middleware 中间件

钩子函数有时不太够用,并且代码一长不太好维护。所以 tua-api 还引入了中间件功能,用法上和 koa 的中间件很像(其实底层直接用了 koa-compose)。

  1. export default {
  2. middleware: [ fn1, fn2, fn3 ],
  3. }

首先说下中间件执行顺序,koa 中间件的执行顺序和 redux 的正好相反,例如以上写法会以以下顺序执行:

请求参数 -> fn1 -> fn2 -> fn3 -> 响应数据 -> fn3 -> fn2 -> fn1

接口说下中间件的写法,分为两种

  • 普通函数:注意一定要 return next() 否则 Promise 链就断了!
  • async 函数:注意一定要 await next()
  1. // 普通函数,注意一定要 return next()
  2. function (ctx, next) {
  3. ctx.req // 请求的各种配置
  4. ctx.res // 响应,但这时还未发起请求,所以是 undefined!
  5. ctx.startTime // 发起请求的时间
  6. // 传递控制权给下一个中间件
  7. return next().then(() => {
  8. // 注意这里才有响应!
  9. ctx.res // 响应对象
  10. ctx.res.data // 响应的数据
  11. ctx.reqTime // 请求花费的时间
  12. ctx.endTime // 收到响应的时间
  13. })
  14. }
  15. // async/await
  16. async function (ctx, next) {
  17. ctx.req // 请求的各种配置
  18. // 传递控制权给下一个中间件
  19. await next()
  20. // 注意这里才有响应响应!
  21. ctx.res // 响应对象
  22. }

其他参数参阅这里

四、小结

这篇安利文,先是从前端发请求的历史出发。一步步介绍了如何构建以及使用 api 中间层,来统一管理接口地址,最后还介绍了下中间件等高级功能。话说回来,这么好用的 tua-api 各位开发者老爷们不来了解一下么?

参考文献

原文地址:https://segmentfault.com/a/1190000016966523

如何构建通用 api 中间层的更多相关文章

  1. 使用ASP.NET Core 3.x 构建 RESTful API - 2. 什么是RESTful API

    1. 使用ASP.NET Core 3.x 构建 RESTful API - 1.准备工作 什么是REST REST一词最早是在2000年,由Roy Fielding在他的博士论文<Archit ...

  2. 使用ASP.NET Core构建RESTful API的技术指南

    译者荐语:利用周末的时间,本人拜读了长沙.NET技术社区翻译的技术标准<微软RESTFul API指南>,打算按照步骤写一个完整的教程,后来无意中看到了这篇文章,与我要写的主题有不少相似之 ...

  3. vue2升级vue3:Vue Demij打通vue2与vue3壁垒,构建通用组件

    如果你的vue2代码之前是使用vue-class-component 类组件模式写的.选择可以使用 https://github.com/facing-dev/vue-facing-decorator ...

  4. Java学习笔记之使用反射+泛型构建通用DAO

    PS:最近简单的学了学后台Servlet+JSP.也就只能学到这里了.没那么多精力去学SSH了,毕竟Android还有很多东西都没学完.. 学习内容: 1.如何使用反射+泛型构建通用DAO. 1.使用 ...

  5. Spring MVC中使用 Swagger2 构建Restful API

    1.Spring MVC配置文件中的配置 [java] view plain copy <!-- 设置使用注解的类所在的jar包,只加载controller类 --> <contex ...

  6. java JDK8 学习笔记——第15章 通用API

    第十五章 通用API 15.1 日志 15.1.1 日志API简介 1.java.util.logging包提供了日志功能相关类与接口,不必额外配置日志组件,就可在标准Java平台使用是其好处.使用日 ...

  7. php 使用 restler 框架构建 restfull api

    php 使用 restler 框架构建 restfull api restler 轻量级,小巧,构建restfull api非常方便! 官网:http://restler3.luracast.com/ ...

  8. spring boot / cloud (三) 集成springfox-swagger2构建在线API文档

    spring boot / cloud (三) 集成springfox-swagger2构建在线API文档 前言 不能同步更新API文档会有什么问题? 理想情况下,为所开发的服务编写接口文档,能提高与 ...

  9. Spring Boot-------JPA——EntityManager构建通用DAO

    EntityManager EntityManager 是用来对实体Bean 进行操作的辅助类.他可以用来产生/删除持久化的实体Bean,通过主键查找实体bean,也可以通过EJB3 QL 语言查找满 ...

随机推荐

  1. POJ 1198/HDU 1401

    双向广搜... 呃,双向广搜一般都都用了HASH判重,这样可以更快判断两个方向是否重叠了.这道题用了双向的BFS,有效地减少了状态.但代码太长了,不写,贴一个别人的代码.. #include<i ...

  2. 利用runtime动态生成对象?

    利用runtime我们能够动态生成对象.属性.方法这特性 假定我们要动态生成DYViewController,并为它创建属性propertyName 1)对象名 NSString *class = @ ...

  3. Xcode6+Cocos2d-x真机调试 报错

    眼下真机调试时遇到下面问题. Undefined symbols for architecture arm64: "_png_get_io_ptr", referenced fro ...

  4. JAVA基础实例(一)

    1写一个方法,用一个for循环打印九九乘法表 /** *一个for循环打印九九乘法表 */ public void nineNineMultiTable() { for (int i = 1,j = ...

  5. C语言播放声音最简单的两种方法

    1. 假设仅须要播放波形文件wav格式的声音,非常easy.仅仅需一句话: PlaySound(TEXT("Data\\1.wav"), NULL, SND_FILENAME | ...

  6. bzoj5178: [Jsoi2011]棒棒糖

    就是裸的主席树嘛... 表扬一下自己1A #include<cstdio> #include<iostream> #include<cstring> #includ ...

  7. 怎样才是一个基本水平的java程序员?

    怎样才是一个基本水平的java程序员? 熟悉常用的数据结构,包括数组,链表,树,哈希表等. 熟悉结构化编程和面向对象编程. 能够阅读UML设计图,根据UML语义进行编码 了解RDBMS和SQL的使用, ...

  8. html5 初探

    html5是越来越火了.小小菜鸟也来学习学习. 相比于之前的几个版本,HTML5提供了更加丰富的多媒体标签使得音乐,视频的播放不用再借助于flah了.不过暂时各浏览器间样式还是有差别. 除此之外,表单 ...

  9. MVC HtmlHelper扩展——实现分页功能

    MVC HtmlHelper扩展类(PagingHelper) using System; using System.Collections.Generic; using System.Collect ...

  10. Java基础3一基础语句

    1.条件语句:所谓的条件语句就是指有选择的去执行部分代码. 包括:if条件语句和switch条件语句 if条件语句: 语法: (1)if(条件语句){ //条件成立时需要执行的代码   } (2)if ...