Open API 是一个标准,它的主要作用是描述 REST API,既可以作为文档给开发者阅读,又可以让机器根据这个文档自动生成客户端代码等。Open API 和我们说的 openapi 是不同的, openapi 是一种 api 的类型,一般是给外部的开发者使用的使用 auth 鉴权的 API。

Open API 发源于 swagger ,swagger 作为最受欢迎的 API 开发工具提供了相当多的工具来帮助我们从产品的各个生命周期来管理 API。
image.png

而这一切的基石就是其中的 API 规范,所以 2016 它独立了出来交给了 Linux 基金会 ,并且换了个更加牛逼的新名字 Open API

规范内容

OpenAPI 规范(OAS)定义了一个标准的、语言无关的 RESTful API 接口规范,它可以同时允许开发人员和操作系统查看并理解某个服务的功能,而无需访问源代码,文档或网络流量检查(既方便人类学习和阅读,也方便机器阅读)。

基本数据结构

image.png

由于 js 的数据类型明显要少于 java 等语言,所以我们这边需要把这些给转化为相应的 js 类型。

  • number 数字类型: integer long float double
  • stirng 字符串:string byte date dateTime password
  • 文件对象 : binary

在新的规范中还增加了富文本的格式 ,只要 description 标定的字段,内部就是使用的 CommonMark markdown 的规范的 markdown,当然也可以扩展它,但是很多工具可能就不支持了。

根对象总览

  1. openapi: 3.0.3 # OpenAPI 规范版本号
  2. info: # API 元数据信息
  3. servers: # 服务器连接信息
  4. tags: # API 的分组标签
  5. paths: # 对所提供的 API 有效的路径和操作
  6. components: # 一个包含多种纲要的元素,可重复使用组件
  7. security: # 声明 API 使用的安全机制
  8. externalDocs: # 附加文档

Info 对象

info 里面的信息会用于请求网络和获取基本的标题和描述。

  1. # API 元数据信息
  2. info:
  3. title: xx开放平台接口文档 # 应用的名称
  4. description: |
  5. 简短的描述信息,支持 markdown 语法。 | 表示换行,< 表示忽略换行。
  6. version: "1.0.0" # API 文档的版本信息
  7. termsOfService: 'http://swagger.io/terms/' # 指向服务条款的 URL 地址
  8. contact: # 所开放的 API 的联系人信息
  9. name: API Support # 人或组织的名称
  10. url: http://www.example.com/support # 指向联系人信息的 URL 地址
  11. email: apiteam@swagger.io # 人或组织的 email 地址
  12. license: # 所开放的 API 的证书信息。
  13. name: Apache 2.0
  14. url: 'http://www.apache.org/licenses/LICENSE-2.0.html'

Server 对象

所有的 API 端点都是相对于基本 URL 的。例如,假设 https://api.example.com/v1 的基本 URL 中,/users 端点指的是 https://api.example.com/v1/users

  1. https://api.example.com/v1/users?role=admin&status=active
  2. \ __________________ / \__/ \ ______________________ /
  3. 服务器URL 端点路径 查询参数

这个配置其实非常重要,但是在我们生成 typescript 描述文档中几乎没什么用。

Components 对象

components 对象包含开放 API 规范规定的各种可重用组件。当没有被其他对象引用时,在这里定义的组件不会产生任何效果,只是可以被别的配置来 $ref 过来。

  1. # 一个包含多种纲要的元素,可重复使用组件
  2. components:
  3. schemas, // 定义可重用的 Schema 对象 的对象。
  4. responses, // 定义可重用的 responses 对象 的对象。
  5. parameters, // 定义可重用的 parameters 对象 的对象。
  6. examples, // 定义可重用的 examples 对象 的对象。
  7. requestBodies, // 定义可重用的 requestBodies 对象 的对象。
  8. headers, // 定义可重用的 headers 对象 的对象。
  9. securitySchemes, // 定义可重用的 securitySchemes 对象 的对象。
  10. links, // 定义可重用的 links 对象 的对象。
  11. callbacks, // 定义可重用的 callbacks 对象 的对象。

我们需要这个配置来生成 typing.d.ts 中的内容,这其中我们主要关注的是 components.schemas 的配置,下面有一个简单的配置:

  1. "schemas": {
  2. "Order": {
  3. "type": "object",
  4. "properties": {
  5. "id": {
  6. "type": "integer",
  7. "format": "int64"
  8. },
  9. "petId": {
  10. "type": "integer",
  11. "format": "int64"
  12. },
  13. "quantity": {
  14. "type": "integer",
  15. "format": "int32"
  16. },
  17. "shipDate": {
  18. "type": "string",
  19. "format": "date-time"
  20. },
  21. "status": {
  22. "type": "string",
  23. "description": "Order Status",
  24. "enum": ["placed", "approved", "delivered"]
  25. },
  26. "complete": {
  27. "type": "boolean",
  28. "default": false
  29. }
  30. },
  31. "xml": {
  32. "name": "Order"
  33. }
  34. }
  35. }

这个解析是非常简单的伪代码如下:

  1. Object.keys(schemas).map((apiName) => {
  2. const properties = schemas[apiName].properties;
  3. const propertieName = Object.keys(properties)
  4. .map((name) => {
  5. const propertie = properties[name];
  6. return `${name}:${propertie.type} //${properties[format]}`;
  7. })
  8. .join(",");
  9. return `
  10. type ${apiName} {
  11. ${propertieName}
  12. }
  13. `;
  14. });

有些时候 会有 "$ref:"#/components/schemas/Address" 的类型,这里需要根据生成的 Address 的来替换。

  1. `${name}:${API.$ref} //${properties[format]}`;

注意事项

  • 不要纠结完美解析,随时准备上 Record<string, any> ,不然 就是整个依赖链的雪崩
  • 注意一下 apiName 的格式,很多项目有自己的脾气,比如喜欢给 xxx.xx ,或者直接上中文名
  • 建议使用模板,因为依赖之间是强依赖,但是对顺序没有要求,所以需要解析两遍,把所有的 $ref 都维护一个模板
  • 建议所有的类型全部包裹在 namespace 中防止冲突,并且尽量不要生成枚举,只生成注释

Path 对象

定义各个的端点和操作的相对路径。这里指定的路径会和 Server 对象 内指定的URL地址组成完整的URL地址,路径可以为空,这依赖于 ACL constraints 的设置。

  1. { "/{path}" : {
  2. get,
  3. put,
  4. post,
  5. delete,
  6. options,
  7. head,
  8. patch,
  9. trace,
  10. servers,
  11. parameter,
  12. }
  13. }

Path 对象对我们生成网络请求是最重要的,我们需要解析这个所有的路径,然后生成 request 的代码, 每个 path 下面都有一系列的配置:

  1. {
  2. "/${path}": {
  3. get, // get 请求的配置
  4. put, // put 请求的配置
  5. post, // post 请求的配置// get 请求的配置
  6. delete, // delete 请求的配置
  7. options, // options 请求的配置
  8. head, // head 请求的配置
  9. patch, // patch 请求的配置
  10. servers, // 可以覆盖的全局的 servers 配置
  11. parameter // 全部的参数配置,里面不允许重复
  12. }
  13. }

伪代码如下:

  1. Object.keys(pathConfig).forEeach((path) => {
  2. const obj = pathConfig[path];
  3. object.keys(obj).map((method) => {
  4. const requestConfig = obj[method];
  5. const requestStr = `
  6. /**
  7. * 注释
  8. * ${requestConfig.description}
  9. */
  10. export async function ${requestConfig.operationId}(params: LoginParamsType) {
  11. return request('${path}', {
  12. method: '${method}',
  13. data: params,
  14. });
  15. }`;
  16. });
  17. })

需要注意的点如下:

  • 如果 params 的类型是 $ref ,要去匹配引用的类型
  • 如果是form 上传需要拼接一下 formdata
  • 如果是path 是个变量,需要和 params 配合好,get 和 post 传参不同也要特别注意
  • 不要乱生成多余的类型,比如 header 之类的 ,很多团队都不规范,最好直接不要生成。

总结

写一个生成工具其实是非常简单(3小时左右)的,比较困难的其实是一个好看的图形界面和对于各种异常情况的处理,swagger 虽然很火但是这个界面其实说不上美观。
image.png
而内部的 oneapi 做了个好看的界面,但是对于异常情况处理的不是很完美,我的项目中的 python 代码就经常跑不起来。这里的成本其实非常大。

如果我们自己要做这方面的工作可以从以下作为卖点:

  • 超漂亮好用的界面
  • 健壮性,支持各种合法不合法的 openapi json 并且总是能生成文件
  • 足够简单的使用方式,前后端都需要没有成本(足够开发且有影响力)
  • 支持扩展,比如根据 get 来生成 table,根据post 生成 form。能大大的减少工作量