资讯专栏INFORMATION COLUMN

如何构建通用 api 中间层

BingqiChen / 2151人阅读

摘要:是在收到响应后执行的函数,可以不用返回。一步步介绍了如何构建以及使用中间层,来统一管理接口地址,最后还介绍了下中间件等高级功能。

零、问题的由来

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

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

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

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

import { fooApi } from "@/apis/"

fooApi
    .bar({ a: "1", b: "2" }) // 发起请求,a、b 是请求参数
    .then(console.log)       // 收到响应
    .catch(console.error)    // 处理错误

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

如何发请求,即“武器”部分

如何组织管理 api 地址

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

一、如何发请求 1.1.原生 XHR (XMLHttpRequest)

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

var xhr = window.XMLHttpRequest
    ? new XMLHttpRequest()
    // 在万恶的 IE 上可能还没有 XMLHttpRequest 这对象
    : new ActiveXObject("Microsoft.XMLHTTP")

xhr.open("GET", "some url")
xhr.responseType = "json"

// 传统使用 onreadystatechange
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText)
    }
}

// 或者直接使用 onload 事件
xhr.onload = function () {
    console.log(xhr.response)
}

// 处理出错
xhr.onerror = console.error

xhr.send()

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

1.2.jQuery 封装的 ajax

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

var params = {
    url: "some url",
    data: { name: "Steve", location: "Beijing" },
}

$.ajax(params)
    .done(console.log)
    .fail(console.error)

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

$.ajax({
    url: "some url",
    data: { name: "Steve", location: "Beijing" },
    dataType: "jsonp",
    success: console.log,
    error: console.error,
})

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

1.3.现代浏览器的原生 fetch

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

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

fetch(url)
    .then(res => res.json())
    .then(console.log)
    .catch(console.error)

async/await 自然也能使用

try {
    const data = await fetch(url).then(res => res.json())
    console.log(data)
} catch (e) {
    console.error(e)
}

当然 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/ 一般是这样的文件结构:

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

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

2.3.基础配置内容

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

// src/apis/foo.js

export default {
    // 请求的公用服务器地址。
    host: "http://example-base.com/",

    // 请求的中间路径,建议与文件同名,以便后期维护。
    prefix: "foo",

    // 接口地址数组
    pathList: [
        { path: "create" },
        { path: "modify" },
        { path: "delete" },
    ],
}

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

// src/apis/foo.js

// 某个获取页面地址参数的函数
const getUrlParams = () => {...}

export default {
    // 根据 NODE_ENV 采用不同的服务器
    host: process.env.NODE_ENV === "test"
        ? "http://example-test.com/"
        : "http://example-base.com/",

    // 根据页面参数采用不同的服务器,即页面地址带 ?test=1 则走测试地址
    host: getUrlParams().test
        ? "http://example-test.com/"
        : "http://example-base.com/",

    // ...
}
2.4.配置导出

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

import TuaApi from "tua-api"

// 初始化
const tuaApi = new TuaApi({ ... })

// 导出
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 来抽象这些逻辑?

// src/apis/foo.js

export default {
    // ...

    // 公共参数,将会合并到后面的各个接口参数中
    commonParams: {
        foo: "bar",
        from: "index-page",
    },

    pathList: [
        {
            path: "create",
            params: {
                // 类似 Vue 中 props 的类型检查
                id: { required: true },
            },
        },
        {
            path: "modify",
            // 使用 post 的方式
            type: "post",
            params: {
                // 写成 isRequired 也行
                id: { isRequired: true },
                // 接口不合并公共参数,即不传 from 参数
                commonParams: null,
            },
        },
        {
            path: "delete",
            // 使用 jsonp 的方式(不填则默认使用 axios)
            reqType: "jsonp",
            params: {
                id: { required: true },
                // 这里填写的 from 会覆盖 commonParams 中的同名属性
                from: "delete-page",
            },
        },
    ],
}

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

import { fooApi } from "@/apis/"

// 直接调用将会报错,因为没有传递 id 参数
await fooApi.create()

// 请求参数使用传入的 from:id=1&foo=bar&from=foo-page
await fooApi.create({ id: 1, from: "foo-page" })

// 请求参数将只有 id:id=1
await fooApi.modify({ id: 1 })

// 请求参数将使用自身的 from:id=1&foo=bar&from=delete-page
await fooApi.delete({ id: 1 })
2.6.接口重命名

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

remove/all

add-array

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

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

fooApi["remove/all"]
fooApi["add-array"]

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

// src/apis/foo.js

export default {
    // ...

    pathList: [
        // ...

        { path: "remove/all", name: "removeAll" },
        { path: "add-array", name: "addArray" },
    ],
}

更多配置请点击这里查看

三、高级功能

一个接口层仅仅只能发 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)。

export default {
    middleware: [ fn1, fn2, fn3 ],
}

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

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

简单说下中间件的写法,分为两种

普通函数:注意一定要 return next() 否则 Promise 链就断了!

async 函数:注意一定要 await next()

// 普通函数,注意一定要 return next()
function (ctx, next) {
    ctx.req       // 请求的各种配置
    ctx.res       // 响应,但这时还未发起请求,所以是 undefined!
    ctx.startTime // 发起请求的时间

    // 传递控制权给下一个中间件
    return next().then(() => {
        // 注意这里才有响应!
        ctx.res       // 响应对象
        ctx.res.data  // 响应的数据
        ctx.reqTime   // 请求花费的时间
        ctx.endTime   // 收到响应的时间
    })
}

// async/await
async function (ctx, next) {
    ctx.req // 请求的各种配置

    // 传递控制权给下一个中间件
    await next()

    // 注意这里才有响应响应!
    ctx.res // 响应对象
}

其他参数参阅这里

四、小结

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

参考文献

Jquery ajax, Axios, Fetch区别之我见

传统 Ajax 已死,Fetch 永生

fetch 没有你想象的那么美

fetch 使用的常见问题及解决方法

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99151.html

相关文章

  • 如何构建通用存储间层

    摘要:并且数据同步后默认会保存下来,这样下次再请求时存储层中就有数据了。以下参数会传到中这么一来,存储层就和接口层对接起来了。五支持永久保存在某些场景下,可能不方便写过期时间,这时默认可以传递,标记该数据永不过期。 零、问题的由来 开门见山地说,这篇文章【又】是一篇安利软文~,安利的对象就是 tua-storage。 顾名思义,这就是一款存储数据的工具。 用 tua-storage 好处大大...

    hersion 评论0 收藏0
  • Docker和rkt快别争了,k8s才是容器生态的中心

    摘要:在这一假设之下,是一个新奇的观点编排才是容器生态的中心,而引擎就我们所知,只是一个开发工具。是特有的概念,但容器生态系统必须采用这个概念。 showImg(https://segmentfault.com/img/remote/1460000007157260?w=640&h=480); 开源项目 CRI-O ,其前身为 OCID ,官方简介是 OCI-based implementa...

    NSFish 评论0 收藏0
  • vue服务端渲染demo将vue-cli生成的项目转为ssr

    摘要:无需使用服务器实时动态编译,而是使用预渲染方式,在构建时简单地生成针对特定路由的静态文件。与可以部署在任何静态文件服务器上的完全静态单页面应用程序不同,服务器渲染应用程序,需要处于运行环境。更多的服务器端负载。 目录结构 -no-ssr-demo 未做ssr之前的项目代码用于对比 -vuecli2ssr 将vuecli生成的项目转为ssr -prerender-demo 使用prer...

    whinc 评论0 收藏0
  • django官方文档1.11编翻:首页

    摘要:源网页说明文档所有关于你应该且必须知道的。性能和优化概述的兼容性旨在兼容多种不同版本的支持的兼容性地理框架打算成为世界级的地理框架。其目标是尽可能简单地构建应用程序并利用空间使能数据的功能。 源网页:https://docs.djangoproject.co... django说明文档 所有关于django你应该且必须知道的。 第一步 你是否django编程新手,那就从此开始!从零开始...

    Michael_Lin 评论0 收藏0
  • 小程序上云,有点猛

    摘要:另外小程序云应用有一套高可用架构,提供监控预警能力。自主可控小程序云应用提供服务器,开发者可以拥有登录或重启,也可以修改密码。也就是说,服务器是由小程序云应用提供,但使用权归开发者。  前不久有一个朋友问我,到底是做什么端的小程序比较好?   我只问了一句,你的产品里是否涉及钱和服务,如果涉及这两者,建议你选择支付宝小程序。你可以通过其他小程序玩裂变,但如果你想做服务和商业,一定要考虑支付宝...

    jsdt 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<