跳到主要内容

中间件

Keq 采用与 koa 相似的中间件模型。 这让 Keq 拥有了强大的自定义扩展能力。

从 NPM 上可以找到很多扩展 Fetch API 的三方库。这些库往往提供类似 wrap(fetch) 的 API。 例如:fetch-retry、node-fetch-har、fetch-enhanced。

这导致我们需要维护像 wrapX(wrapY(wrapZ(fetch))) 一样的烂代码。并且难以精准控制每个 wrapper 为不同的路由赋予不同的默认参数。

洋葱模型​

Keq Middleware

Keq 的核心功能也是通过层层组合的 Middleware 实现的,其用于发送请求的 Fetch API 也是一个 Middleware。

当调用 Keq 的 API 发送请求时,Request 参数经过层层 Middleware 处理,最终抵达 Fetch API Middleware。 Fetch API Middleware 将发送请求并返回 Response。最后 Response 再次经过层层 Middleware 处理后,返回给调用者。

提示

Keq 由内置的 5 层 Middleware 组合而成。图示为了表达清晰仅标注了其中 4 层,5 层由外至内分别是:

  • retryMiddleware: 提供失败重试功能
  • flowControlMiddleware: 提供并发控制功能
  • timeoutMiddleware: 提供请求超时控制功能
  • proxyResponseMiddleware: 根据 context.res 构造 context.response 属性。
  • fetchMiddleware: 根据 content.request 参数发送请求,并将 Response 写入 context.res。

自定义 Middleware​

Middleware 实际上是一个接收了两个参数 context 和 next 的异步函数:

参数名描述
context(第一个参数)请求上下文,其包含了所有的请求参数和响应结果。
next(第二个参数)执行下一层 Middleware。在 next 执行前修改 context.request 会影响实际发送的请求,在 next 执行后可以访问 context.response 获取请求结果并作进一步处理。

接下来,让我们添加一个简单的、用于处理请求错误的 Middleware,借此熟悉 Middleware。

import { request } from "keq"

request
  .use(async (context, next) => {
    await next() // 调用下一层 middleware 直至 fetch

    if (context.response) {
      if (!context.response.status !== 200) {
        alert(`${ctx.request.url.href} is not 200`)
        throw new Error("The Response is not 200.")
      }
    }
  })

try {
  await request.get("http://example.com/cat")
} catch (err) {
  // 如果 /cat 接口响应状态码不等于 200,将会捕获到异常
  console.log(err)
}

上述代码通过调用 request.use(middleware) 添加了一个检测所有请求响应状态码的 Middleware。 不过,在实际工程中,我们将面对许多不同的接口提供方,并需针对性的应用 Middleware,Keq 为此提供了灵活的路由方法:

import { request } from "keq"

request
  .useRouter()
  .host('example.com', async (context, next) => {
    await next()

    if (context.response) {
      if (context.response.status !== 200) {
        alert(`${ctx.request.url.href} is not 200`)
        throw new Error("The Response is not 200.")
      }
    }
  })

try {
  await request.get("http://example.com/cat")
} catch (err) {
  // 如果 /cat 接口响应状态码不等于 200,将会捕获到异常
  console.log(err)
}

通过 request.useRouter().host(hostName, middleware),Middleware 将仅在请求域名为 "example.com"时生效。 接下来,我们可以将 Middleware 代码封装成独立的函数,以便于将其多次应用在不同路由上:

import { KeqMiddleware, request } from 'keq'

function responseValidator(): KeqMiddleware {
  return async (context, next) => {
    await next()

    if (context.response) {
      if (context.response.status !== 200) {
        alert(`${ctx.request.url.href} is not 200`)
        throw new Error("The Response is not 200.")
      }
    }
  }
}

request
  .useRouter()
  .host('example.com', responseValidator())
  .pathname('/api/**', responseValidator())

.useRouter() 会匹配所有符合规则的请求,如果我们希望个别的请求不运行alert。可以给 Middleware 添加 option:

import { KeqMiddleware, request } from 'keq'

declare module 'keq' {                
  interface KeqOptions<T> {           
    silent(value: boolean): Keq<T>    
  }                                   
}                                     

function responseValidator(): KeqMiddleware {
  return async (context, next) => {
    await next()

    if (context.response) {
      if (context.response.status !== 200) {
        if (!context.options.silent) {                 
          alert(`${ctx.request.url.href} is not 200`)  
        }                                              

        throw new Error("The Response is not 200.")
      }
    }
  }
}

request.use(responseValidator)

// 这个请求即便出错,也不会触发`alert`
await request
  .get('/cat')
  .option('silent', true)   

除了处理响应,我们还可以在请求发送前修改请求参数,让我们再实现一个向请求头中添加 x-site: us 的中间件:

import { KeqMiddleware } from 'keq'

function appendSiteHeader(site: string = 'us'): KeqMiddleware {
  return async (context, next) => {
    context.request.headers.append('x-site', site)

    await next()
  }
}

request
  .useRouter()
  .host('example.com', appendSiteHeader('cn'))

上下文(context)​

通过与 context 交互,Middleware 可以完全的控制请求行为,下面表格罗列了 context 所有内置的属性及其功能:

属性描述
context.requestHttp 请求的参数
context.abort()ReadOnly 终止当前请求
context.globalcontext.global 中存储的数据在 Middleware 执行完成后不会随 context 一起销毁。使用时请务必注意内存溢出问题。
context.res原始 Response 对象. 请求成功发送后由 fetchMiddleware 添加至 context。请尽可能使用 context.response 除非你确实需要原始 Response。
context.responsecontext.res 的代理,解决了 Response 的 .json()、.test() 等方法无法被多次调用,导致多个 Middleware 重复获取响应体报错的问题。
context.outputWriteOnly 更改 await request.get('xxx') 的返回内容。(当调用 .resolveWith() 时此属性无效)。
context.identifierReadOnly 标记请求代码位置的唯一 key。
context.metadata当前中间件的上下文信息。往往用于检测中间件执行的状态。
context.optionsrequest.get('xxx').option(key, value) 设置的自定义选项,Middleware 可以添加自己的 options。

context.request​

属性描述
context.request.urlHttp 请求地址
context.request.__url__Readonly 合并了路由参数(routeParams) 的 Http 请求地址
context.request.methodHttp 请求的 Method('get', 'post', 'put', 'patch', 'head', 'delete').
context.request.bodyHttp 请求体
context.request.headersHttp 请求头
context.request.routeParamsHttp 请求地址中的路由参数
context.request.catchFetch API 的 catch 参数
context.request.credentialsFetch API 的 credentials 参数
context.request.integrityFetch API 的 integrity 参数
context.request.keepaliveFetch API 的 keepalive 参数
context.request.modeFetch API 的 mode 参数
context.request.redirectFetch API 的 redirect 参数
context.request.referrerFetch API 的 referrer 参数
context.request.referrerPolicyFetch API 的 referrerPolicy 参数

context.options​

context.options 使得请求调用方可以随时随地的调整 Middleware 的运行逻辑。也允许了外层的 Middleware 通过修改 context.options 改变内层 Middleware 的运行逻辑。

属性默认值描述
context.options.fetchAPIglobal.fetchfetchMiddleware 发起 Http 请求使用的 Fetch API。可以更换成 node-fetch等与 NodeFetch 兼容的三方包。
context.options.resolveWith"intelligent"如何解析 Response 的响应体。设置后 content.output 将失效。
context.options.retryTimesundefined重试次数。
context.options.retryDelayundefined重试间隔时间。
content.options.retryOnundefined自定义重试条件。
content.options.moduleundefined请求所属模块。
content.options.flowControlundefined调整请求的并发运行行为模式,修改后将调节 flowControlMiddleware 的运行行为。
content.options.timeoutundefined修改请求的超时时间,修改后将调节 timeoutControlMiddleware 的运行行为。

context.global​

属性默认值描述
context.global.serialFlowControlundefined用于实现 .flowControl('serial') 模式相关功能。
context.global.abortFlowControlundefined用于实现 .flowControl('abort') 模式相关功能

context.metadata​

Middleware 只有在极少场景下需要操作 context.metadata。

属性描述
context.metadata.finished当期中间件是否运行完成。
context.metadata.entryNextTimes当前中间件结束前调用了几次 next。
context.metadata.outNextTimes当前中间件结束前next调用完成了几次。

路由(.useRouter())​

方法描述
.location(...middlewares)在浏览器中会将 window.location.origin 的请求路由至 middlewares。在 NodeJS 中则是 127.0.0.1
.method(method: string[, ...middlewares])将匹配 method 的请求路由至 middlewares 处理。
.pathname(matcher: string | Regexp[, ...middlewares])matcher 可以是一个 glob(minimatch)表达式或者正则表达式,并将匹配到的请求路由至 middlewares处理。
.host(host: string[, ...middlewares])将发送给 host 域 的请求路由至 middlewares 处理。
.module(moduleName: string[, ...middlewares])将moduleName模块的请求路由至 middlewares 处理。
.route(...middlewares)自定义路由策略

createRequest​

调用 createRequest() 创建一个独立的 request 实例。 多个 request 实例间的 Middleware 和 context.global 不会共享。

import { createRequest } from "keq"

const customRequest = createRequest()

// Middleware 只会在 customRequests 中生效
customRequest.use(/** some middleware */)

const body = await customRequest.get("http://test.com")

你从 'keq' 中引用的 request 实例 也是通过 createRequest() 创建的。

optiondescription
initMiddlewares修改 Keq 内置的中间件。
baseOrigin如果发送的请求未设置 origin,在浏览器中默认使用 window.location.origin;在 NodeJS 中则是 http://127.0.0.1