跳到主要内容

@keq-request/cli

Keq 提供的命令行工具可以将 swagger 文档编译为 typescript 代码。从而能像调用函数一样发送 HTTP 请求。

Node.js 版本需 >= 20.19.0

安装

警告

建议在 package.json 锁定 @keq-request/cli 的版本。 @keq-request/cli 的小版本升级为了修复 Bug 可能会修改代码模板。这有极小的概率导致代码不向前兼容。

使用方法

首先,我们需要一份 swagger 文档,例如下面的 cat-service-swagger.json 文件,描述了一个获取猫咪信息的 HTTP 接口。

cat-service-swagger.json
cat-service-swagger.json
{
  "openapi": "3.0.0",
  "info": {
    "title": "Cat Service",
    "version": "0.0.1"
  },
  "paths": {
    "/cats": {
      "get": {
        "operationId": "getCats",
        "parameters": [
          {
            "name": "breed",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Cat"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Cat": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "age": {
            "type": "number"
          }
        }
      }
    }
  }
}

然后,我们创建 @keq-request/cli 的配置文件,指定 swagger 文档的位置和编译参数:

.keqrc.ts
import { defineKeqConfig, FileNamingStyle } from "@keq-request/cli"

export default defineKeqConfig({
  outdir: "./src/apis", // 编译结果的输出目录
  fileNamingStyle: FileNamingStyle.snakeCase,
  modules: {
    catService: "./cat-service-swagger.json",
    // 也可以从网络上获取 swagger 文档,例如:
    // dogService: "http://dog.example.com/swagger.json"
  },
})
建议

优先使用 typescriptjavascript 格式的配置文件。一些高级功能(例如 operationIdFactory)只能通过代码实现。

接下来,在终端执行以下命令:

执行完成后,@keq-request/cli 会在 ./src/apis/cat_service/ 目录下生成如下文件:

./src/apis/cat_service/get_cats.request.ts
import { Keq } from "keq"
import { request } from '../../request'
import { Operation, GetCatsResponseBodies, GetCatsRequestParameters } from '../types/get_cats.type'
export { GetCatsRequestQuery, GetCatsRequestHeaders, GetCatsRequestBody } from '../types/get_cats.type'

const moduleName = "catService"
const method = "get"
const pathname = "/cats"

export function getCats<STATUS extends keyof GetCatsResponseBodies>(args?: GetCatsRequestParameters): Keq<Operation<STATUS>> {
  const req = request.post<GetCatsResponseBodies[STATUS]>("/cats")
    .option('module', { name: moduleName, pathname, method })

  if (args && "breed" in args) req.query("breed", args["breed"], {"arrayFormat":"repeat"})

  return req
}

getCats.pathname = pathname
getCats.method = method

这样,我们就可以调用 getCats 函数来发送 HTTP 请求:

import { getCats } from "./src/apis/cat_service/operations"

const cats = await getCats({ breed: "siamese", unknownKey: 'value' }) // unknownKey 未在 swagger 定义,将被丢弃
  .retry(3, 1000)
  .timeout(1000)
  // 追加额外的请求参数
  .query('extraQuery', 'extra')
  .set('Authorization', 'TOKEN')
  .send({ extraBody: 'extra' })

// 实际请求地址: /cats?breed=siamese&extraQuery=extra

console.log(`我的猫咪有:${cats.map(cat => cat.name).join('、')}`)
console.log(`请求路径:${getCats.pathname}`)
BodyFallbackPlugin 可以将 unknownKey 这样的未定义请求参数,全部添加到请求体中。

最后,我们可以为 catService 模块的所有接口添加统一的错误处理逻辑:

import { request } from './api/request'
import { throwException, RequestException } from 'keq-exception'


request
  .useRouter()
  .module('catService', throwException(context => {
    if (context.response) {
      if (context.response.status >= 400 && context.response.status < 500) {
        throw new RequestException(context.response.status, context.response.statusText, false) // 客户端错误,不需要重试
      } else if (context.response.status >= 500) {
        throw new RequestException(context.response.status, context.response.statusText)
      }
    }
  }))
为什么 Keq 采用 链式调用 + 中间件 的设计模式?

Keq 最初就是为了从 swagger 文档生成请求函数而设计的。然而,只依赖cli 生成代码,许多问题是无法解决的:

  1. swagger 文档中定义的 HTTP 接口信息可能是不完整的,甚至是错误的,例如缺少认证信息、缓存策略等参数。 可偏偏一些外部因素阻止我们通过修改 swagger 文档来补全这些信息。
  2. 客户端在不同场景下调用同一个 HTTP 接口时,可能会有不同的需求,例如在某些场景下需要重试,而在另一些场景下则不需要。 静态生成 的代码无法满足这些动态变化的需求。
  3. 同一个 swagger 文档的接口往往有着类似的鉴权、错误处理、日志记录等复杂的需求。这些高度定制化的功能如果使用 cliPlugin 生成会非常难以维护。

Keq 便是为给予静态生成代码提供更强大的运行时 API 而设计的 HTTP 客户端:

  • 链式调用 让我们在调用请求函数时,可以动态的修改请求参数,甚至设置违背 swagger 定义的参数。并且它完美地避免了臃肿的配置选项。
  • 中间件 则提供了一种优雅的方式来为同一模块的所有接口添加统一的功能。并可以通过 链式调用 在不同场景下灵活的启用或禁用 中间件

配置文件

.keqrc.(yml|json|js|ts)

@keq-request/cli 会自动查找名为 .keqrc.yml.keqrc.json.keqrc.js.keqrc.ts的配置文件。 你可以通过 -c --config <config_file_path> 设置配置文件地址。

配置参数是否必填默认值描述
outdirtrue-编译结果的输出目录
fileNamingStylefalse-文件名风格
modulestrue-Swagger 文件地址和模块名称
operationIdFactoryfalse({ operation }) => operation.operationId自定义函数名的生成规则,默认使用 swagger 文件中的 operationId
strictfalsefalse是否清空输出目录
esmfalsepackage.json 是否设置了 "type": "module" 则为 true是否生成 ESM 风格的代码
pluginsfalse[]插件向第三方开发者提供了完整的自定义 cli 行为的能力

FileNamingStyle

枚举示例
FileNamingStyle.camelCase"twoWords"
FileNamingStyle.capitalCase"Two Words"
FileNamingStyle.constantCase"TWO_WORDS"
FileNamingStyle.dotCase"two.words"
FileNamingStyle.headerCase"Tow-Words"
FileNamingStyle.noCase"two words"
FileNamingStyle.paramCase"two-words"
FileNamingStyle.pascalCase"TwoWords"
FileNamingStyle.pathCase"two/words"
FileNamingStyle.sentenceCase"Two words"
FileNamingStyle.snakeCase"two_words"

.keqignore

还可以通过 .keqignore 文件忽略不需要生成代码的 HTTP 接口:

# 格式为
# [moduleName] [method] [pathname]

* * *                   # 忽略所有模块的所有接口

* GET /cats             # 忽略所有模块的 GET /cats 接口

* GET *                 # 忽略所有模块的 GET 接口

catService * /cats      # 忽略 catService 模块的 /cats 接口

catService GET /cats    # 忽略 catService 模块的 GET /cats 接口

! dogService POST /dogs # 不忽略 dogService 模块的 POST /dogs 接口

命令

keq build [options]

按照配置文件的内容,生成代码。

选项描述
--modules <moduleName...>仅生成匹配模块名称的模块
-c --config <config_file_path>配置文件的地址
-i --interactive通过命令行交互,指定需要生成的 HTTP 接口
--tolerant忽略 Swagger 格式错误,尽可能生成代码
--method <method...>仅生成匹配 method('get' | 'post' | 'put' | 'patch' | 'head' | 'options' | 'delete') 的 HTTP 接口
--pathname <pathname...>仅生成匹配 pathname 的 HTTP 接口

keq ignore <mode> [options]

添加忽略规则到 .keqignore 文件。使用 --interactive 可以通过命令行交互指定需要忽略的 HTTP 接口。

选项描述
<mode>add:添加忽略规则,except:添加不忽略规则
-c --config <config_file_path>配置文件的地址
--modules设置添加的忽略规则的模块名称
--method <method> 设置添加的忽略规则的 HTTP 方法('get' | 'post' | 'put' | 'patch' | 'head' | 'options' | 'delete')
--pathname <pathname...>设置添加的忽略规则的 HTTP 接口路径
-i --interactive通过命令行交互,指定需要忽略的 HTTP 接口
--build忽略规则添加完成后,自动执行 keq build 命令生成代码

Plugin

Plugin 为第三方开发者提供了完整的自定义 CLI 行为的能力。通过 Plugin,你可以在编译过程的各个阶段介入,修改生成的代码或添加自定义逻辑。

一个最简单的 Plugin 只需要实现 apply 方法:

import { Plugin } from '@keq-request/cli'

export class MyPlugin implements Plugin {
  apply(compiler) {
    console.log('插件已加载!')
  }
}

然后在配置文件中启用 Plugin

.keqrc.ts
import { defineKeqConfig } from "@keq-request/cli"
import { MyPlugin } from "./my-plugin"

export default defineKeqConfig({
  outdir: "./src/apis",
  modules: {
    catService: "./cat-service-swagger.json",
  },
  plugins: [new MyPlugin()],
})

编译生命周期

Plugin 可以通过 Hooks 在编译的各个阶段介入。 因此,在深入了解 Hooks 之前,需要先理解 @keq-request/cli 的编译流程。 整个编译过程分为 7 个阶段,按顺序执行:

1. Setup - 加载配置

加载 .keqrc 配置文件和 .keqignore 忽略规则。

数据输出context.setup.rc(配置信息)、context.setup.matcher(匹配规则)

2. Download - 下载文档

从本地或远程加载 Swagger 文档。

数据输出context.downloaded.documents(原始 Swagger 文档)

3. Validate - 验证转换

验证 Swagger 格式并统一转换为 OpenAPI 3.1 标准。此阶段会进行:格式验证、版本转换(Swagger 2.0 → OpenAPI 3.1)、特殊字符修正、去除冗余 $ref、重新计算 operationId 等操作。

数据输出context.validated.documents(规范化后的文档)

4. Interactive - 交互选择(可选)

通过命令行交互选择需要生成的接口,并将选择结果添加到 context.setup.matcher。仅在使用 -i--interactive 参数时执行。

5. Shaking - 过滤接口

根据 context.setup.matcher 中的匹配规则,移除不需要生成代码的接口。

数据输出context.shaken.documents(过滤后的文档)

6. Compile - 生成代码

context.shaken.documents 编译为 TypeScript 代码文件。

数据输出context.compiled.artifacts(生成的代码文件)

7. Persist - 写入文件

context.compiled.artifacts 写入磁盘,并将 context.setup.matcher 写入 .keqignore 文件。

Hooks

CLI 基于 tapable 实现了钩子系统。根据使用场景,钩子分为两类:

生命周期 Hook

在编译的各个阶段触发,用于执行通用逻辑:

钩子名称触发时机类型
afterSetupSetup 阶段完成后AsyncSeriesHook<[]>
afterDownloadDownload 阶段完成后AsyncSeriesHook<[]>
afterValidateValidate 阶段完成后AsyncSeriesHook<[]>
afterShakingShaking 阶段完成后AsyncSeriesHook<[]>
afterPersistPersist 阶段完成后AsyncSeriesHook<[]>

示例:在配置加载后打印日志

export class LoggerPlugin implements Plugin {
  apply(compiler) {
    compiler.hooks.afterSetup.tap('LoggerPlugin', () => {
      console.log('配置加载完成:', compiler.context.setup.rc)
    })
  }
}

Compile Hook

在 Compile 阶段触发,可用于修改生成的代码:

钩子名称触发时机类型
afterCompileKeqRequest生成 request.ts 文件后AsyncSeriesWaterfallHook<[Artifact]>
afterCompileComponentSchema生成数据模型后AsyncSeriesWaterfallHook<[Artifact, SchemaDefinition]>
afterCompileOperationRequest生成请求函数后AsyncSeriesWaterfallHook<[Artifact, OperationDefinition]>
afterCompileOperationType生成类型定义后AsyncSeriesWaterfallHook<[Artifact, OperationDefinition]>

所有 Compile Hook 都是必须返回修改后的 Artifact

内置 Plugin

EslintPlugin

EslintPlugin 会在代码生成后,自动运行 eslint --fix 来格式化代码。

.keqrc.ts
import { EslintPlugin } from '@keq-request/cli/plugins'

export default defineKeqConfig({
  outdir: "./src/apis",
  modules: {
    catService: "./cat-service-swagger.json",
  },
  plugins: [new EslintPlugin()],
})

PrettierPlugin

PrettierPlugin 会在代码生成后,自动运行 prettier --write 来格式化代码。

.keqrc.ts
import { PrettierPlugin } from '@keq-request/cli/plugins'

export default defineKeqConfig({
  outdir: "./src/apis",
  modules: {
    catService: "./cat-service-swagger.json",
  },
  plugins: [new PrettierPlugin()],
})

BodyFallbackPlugin

默认情况下,未定义在 swagger 文档中的请求参数会被丢弃。 BodyFallbackPlugin 会将这些未定义的请求参数,全部添加到请求体中:

.keqrc.ts
import { BodyFallbackPlugin } from '@keq-request/cli/plugins'

export default defineKeqConfig({
  outdir: "./src/apis",
  modules: {
    catService: "./cat-service-swagger.json",
  },
  plugins: [new BodyFallbackPlugin()],
})