¿Sigues usando Postman? Protobuf + Apifox + GitLab le brindan la mejor experiencia en ingeniería de API

Tarjeta de visita de número público Tarjeta de visita del autor

¿Qué es la Ingeniería API?

La ingeniería de API consiste en automatizar y estandarizar el proceso de escritura, creación, publicación, prueba, actualización y administración de API a través de una combinación de una serie de herramientas. Reduzca el costo de comunicación de cada extremo a nivel de API, reduzca el costo de administrar y actualizar la API y mejore la eficiencia de desarrollo de cada extremo.

El efecto de la ingeniería API de la tecnología Hundred Bottles

Los desarrolladores de back-end escriben archivos Protobuf y los envían a GitLab e inician MergeRequest en GitLab. GitLab enviará un correo electrónico a la fusión MergeRequest. Después de que la fusión reciba el recordatorio por correo electrónico, MergeRequest se fusionará después de CodeReview en GitLab. Los grupos de trabajo reciben mensajes de compilación de API. El desarrollador hace clic en el botón Importar ahora en Apifox y se actualiza la documentación de la interfaz en Apifox. El personal del cliente configura nuevas direcciones de interfaz en sus propios proyectos y se construye un nuevo modelo de solicitud.

El proceso de ingeniería API de Baibo Technology

Escriba y administre archivos de interfaz de Protobuf

La construcción del entorno básico y el uso de Protobuf no se repetirán aquí.

Como resumió el Sr. Jianyu, es realmente dolor de cabeza, ¿dónde está el código Proto? Tal vez cada empresa tenga diferentes métodos de gestión para los archivos Proto. Este artículo adopta el método de gestión del almacén centralizado. Como se muestra abajo:

Gestión centralizada de almacenes

El Sr. Mao Jian de Kratos API 工程化también lo compartió una vez e hizo algunas interpretaciones de este artículo del Sr. Fried Fish . Me beneficié mucho después de escucharlo.

La estructura del proyecto de este artículo es la siguiente:

Estructura del proyecto

La base de este proyecto es un proyecto Go. El paquete API se divide en la interfaz de cliente de la aplicación y la interfaz de fondo de gestión entre bastidores. Como se puede ver en el directorio de usuarios de la aplicación, hay un paquete v1 en el dominio del usuario para distinguir la versión de la interfaz, y hay un archivo user_enums.proto para la enumeración compartida por el dominio del usuario. Los archivos de enumeración son los siguientes:

syntax = "proto3";

package app.user;
option go_package = "gitlab.bb.local/bb/proto-api-client/api/app/user;user";

// Type 用户类型
enum Type {
  // 0 值
  INVALID = 0;
  // 普通用户
  NORMAL = 1;
  // VIP 用户
  VIP = 2;
}
复制代码

有一个 user_errors.proto 文件存放 user 域共用的错误。这里的错误处理使用的是 kratos 的 错误 处理方式。

错误文件如下:

syntax = "proto3";

package app.user;
import "errors/errors.proto";

option go_package = "gitlab.bb.local/bb/proto-api-client/api/app/user;user";
option java_multiple_files = true;

enum UserErrorReason {
  option (pkg.errors.default_code) = 500;

  // 未知错误
  UNKNOWN_ERROR = 0;
  // 资源不存在
  NOT_EXIST = 1[(pkg.errors.code) = 404];

}
复制代码

pkg 中 errors 包放的是编译错误文件用公用模型,model 包放的是业务无关的数据模型,如 page、address 等。transport 包存放的是 Grpc code 转 http code 的代码,在错误处理中用到。validate 包存放的是接口参数校验用的文件,如下:

type validator interface {
     Validate() error
}

// Interceptor 参数拦截器
var Interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    if r, ok := req.(validator); ok {
       if err := r.Validate(); err != nil {
           return nil, status.Error(codes.InvalidArgument, err.Error())
       }
    }
    return handler(ctx, req)
}
复制代码

third_party 存放的是编写编译 proto 文件时需要用的第三方的 proto 文件,其他的文件在后续的流程使用中再进行讲解。

核心的接口文件编写如下:

syntax = "proto3";

package app.user.v1;
option go_package = "api/app/user/v1;v1";

import "google/api/annotations.proto";
import "validate/validate.proto";
import "app/user/user_enums.proto";

// 用户
service User {
  // 添加用户
  rpc AddUser(AddUserRequest) returns (AddUserReply) {
    option (google.api.http) = {
      post: "/userGlue/v1/user/addUser"
      body:"*"
    };
  }

  // 获取用户
  rpc GetUser(GetUserRequest) returns (GetUserReply) {
    option (google.api.http) = {
      get: "/userGlue/1/user/getUser"
    };
  }
}

message AddUserRequest {
  // User 用户
  message User {
    // 用户名
    string name = 1[(validate.rules).string = {min_len:1,max_len:10}];
    // 用户头像
    string avatar = 2;
  }
  // 用户基本信息
  User user = 1;
  // 用户类型
  Type type = 2;
}

message AddUserReply {
  // 用户 id
  string user_id = 1;
  // 创建时间
  int64 create_time = 2;
}

message GetUserRequest {
  // 用户 id
  string user_id = 1[(validate.rules).string = {min_len:1,max_len:8}];
}

message GetUserReply {
  // 用户名
  string name = 1;
  // 用户头像
  string avatar = 2;
  // 用户类型
  Type type = 3;
}

复制代码

从上面的代码可以看到一个业务域中的定义的接口和定义接口用到的 message 都定义在一个文件中。接口用到的请求 message 都是以方法名 + Request 结尾,接口用到的返回 message 都以方法名 + Reply 结尾。这样做的好处是:规范统一、避免有相同的 message 在生成 swagger 文档导入到 Apifox 时模型被覆盖。为了快速编写接口可以使用 GoLand 和 IDEA 自带代码模板,快速编写。

crear_proto_gif

那么 proto 接口文件编写到这里已经结束了,整个思想借鉴了 kratos 的官方示例项目 beer-shop

编译发布 Protobuf 文件

因为编写的 proto 文件需要 CodeReview,而且每个开发人员本地编译环境可能不一致,所以编译这个流程统一放在 GitRunner 上,由 MergerRequest 合并后触发 GitRunner 在 Linux 上编译所有的 proto 文件。关于在 Linux 上 安装 Go 环境和相关的编译插件,就不在这里赘述了。GitRunner 配置文件:

before_script:
  - echo "Before script section"
  - whoami
  - sudo chmod +x ./*
  - sudo chmod +x ./shell/*
  - sudo chmod +x ./pkg/*
  - sudo chmod +x ./third_party/*
  - sudo chmod +x ./api/app/*
  - sudo chmod +x ./api/backstage/*
  - git config --global user.name "${GITLAB_USER_NAME}"
  - git config --global user.email "${GITLAB_USER_EMAIL}"

after_script:
  - echo "end"

build1:
  stage: build
  only:
    refs:
      - master
  script:
    - ./index.sh
    - ./gen_app.sh
    - ./gen_backstage.sh
    - ./format_json.sh
    - ./git.sh
    - curl 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx' -H 'Content-Type:application/json' -d "{\"msgtype\":\"markdown\",\"markdown\":{\"content\":\"构建结果:<font color=\\"info\\">成功</font>\n>项目名称:$CI_PROJECT_NAME\n>提交日志:$CI_COMMIT_MESSAGE\n>流水线地址:[$CI_PIPELINE_URL]($CI_PIPELINE_URL)\"}}"
    - ./index.sh
复制代码

before_script 的内容就是配置文件权限和 git 的账号密码,after_script 输出编译结束的语句 build1only.refs 就是指定只在 master 分支触发。script 就是核心的执行流程。

index.sh 用于将 GitLab 的代码 copy 到 GitRunner 所在的服务器。

cd ..
echo "当前目录 `pwd`"
rm -rf ./proto-api-client
git clone http://xx:[email protected]/xx/proto-api-client.git
复制代码

gen_app.sh 用于编译客户端接口。

#!/bin/bash

# errors
API_PROTO_ERRORS_FILES=$( find api/app -name *errors.proto)
protoc --proto_path=. \
       --proto_path=pkg \
       --proto_path=third_party \
       --go_out=paths=source_relative:. \
       --client-errors_out=paths=source_relative:. \
       $API_PROTO_ERRORS_FILES


# enums
API_PROTO_ENUMS_FILES=$( find api/app -name *enums.proto)
protoc --proto_path=. \
       --proto_path=third_party \
       --go_out=paths=source_relative:. \
       $API_PROTO_ENUMS_FILES


# api
API_PROTO_API_FILES=$( find api/app/*/v* -name *.proto)
protoc --proto_path=. \
       --proto_path=api \
       --proto_path=pkg \
       --proto_path=third_party \
       --go_out=paths=source_relative:. \
       --new-http_out=paths=source_relative,plugins=http:. \
       --new-grpc_out=paths=source_relative,plugins=grpc:. \
       --new-validate_out=paths=source_relative,lang=go:. \
       --openapiv2_out . \
       --openapiv2_opt allow_merge=true,merge_file_name=app \
       --openapiv2_opt logtostderr=true \
       $API_PROTO_API_FILES

复制代码

错误处理

$(find api/app -name *errors.proto) 穷举所有以 errors.proto 结尾的文件,client-errors_out 是下载了 kratos errors 的源码重新编译的命令,同 kratos errors 的用法。

枚举处理

$(find api/app -name *enums.proto) 穷举所有以 enums.proto 结尾的文件。

接口处理

$(find api/app/*/v* -name *.proto) 穷举所有接口文件,new-http_outnew-grpc_out 是为支持公司自研框架编译的命令。

参数校验

new-validate_out 是因为 validate 这个参数校验插件在 linux 环境编译的时候和枚举有冲突(笔者还没解决),所以下载源码重新编译了命令。编译结果如下:

编译结果

openapiv2_out 使用的是 openapiv2 插件,allow_merge=true,merge_file_name=app 参数合并所有的接口文件为一个名字 app.swagger.json 的接口文档。logtostderr=true 参数为开启日志,该命令会到一个 app.swagger.json 的文件,这个文件可以导入到 Apifox 中使用。Apifox 真的是一个神器,大大简化接口相关的工作,对于 Apifox 的使用这里不在赘述,请看 官网。编译文档如下:

编译结果

format_json.sh 因为 openapiv2 插件会把 int64 类型的数据在接口文档上显示为 string 类型,为了方便 前端同学区分接口文档中的 string 类型是不是由 int64 类型转的,所以编写了一个 js 文件用来对生成的 swagger.json 文档进行修改,修改后的文档会在由 int64 转成的 string 类型的字段描述中添加 int64 标识。如图:

convert_int64

脚本如下:

#!/bin/bash

node ./format.js
复制代码

用 node 来执行修改编译出的 swagger.json 文档的 js 代码。

const fs = require('fs');
const path = require('path');

const jsonFileUrl = path.join(__dirname, 'app.swagger.json');

function deepFormat(obj) {
  if (typeof obj == 'object') {
    const keys = Object.keys(obj);
    const hasFormat = keys.includes('format');
    const hasTitle = keys.includes('title');
    const hasDescription = keys.includes('description');
    const hasName = keys.includes('name');
    const hasType = keys.includes('type');

    if (hasFormat && hasTitle) {
      obj.title = `${obj.title} (${obj.format})`;
      return;
    }

    if (hasFormat && hasDescription) {
      obj.description = `${obj.description} (${obj.format})`;
      return;
    }

    if (hasFormat && hasName && !hasDescription) {
      obj.description = `原类型为 (${obj.format})`;
      return;
    }

    if (hasFormat && hasType && !hasName && !hasDescription) {
      obj.description = `原类型为 (${obj.format})`;
      return;
    }

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      const value = obj[key];
      if (typeof value == 'object') {
        deepFormat(value);
      }
    }
    return;
  }
  if (Array.isArray(obj)) {
    for (let i = 0; i < obj.length; i++) {
      const value = obj[i];
      if (typeof value == 'object') {
        deepFormat(value);
      }
    }
  }
}

async function main() {
  const jsonOriginString = fs.readFileSync(jsonFileUrl, 'utf8');
  const jsonOrigin = JSON.parse(jsonOriginString);
  deepFormat(jsonOrigin);
  fs.writeFileSync(jsonFileUrl, JSON.stringify(jsonOrigin, null, 2));
}

main();
复制代码

git.sh 用于提交编译后的代码,-o ci.skip 参数用于在此次提交中不再触发 GitRunner 避免循环触发。

#!/bin/bash

# 获取最后一次 提交记录
result=$(git log -1 --online)

# git
git status
git add .
git commit -m "$result  编译 pb 和生成 openapiv2 文档"
git push -o ci.skip http://xx:[email protected]/xx/proto-api-client.git  HEAD:master
复制代码

curl https://qyapi.weixin.qq.com/cgi-bin/webhook/send... 用于构建成功后给工作群发送构建结果。这里使用的是企业微信。具体怎么使用这里不再赘述。效果如下:

通知结果

index.sh 再次 clone 编译后的代码到 GitRunner 服务器。

Apifox 更新接口

Apifox 导入数据支持使用在线的数据源,因为在使用 GitLab 的数据源 url 的时候需要鉴权,而 Apifox 目前不支持鉴权,所以想了一个折中的方案,在提交编译后的代码后,将代码再 clone 到 GitRunner,通过 nginx 映射出一个不需要鉴权的数据源 url。将 不需要鉴权的 url 填入 Apifox。

构建结果

客户端更新请求模型

众所周知,除 JavaScript 外的大多数语言在使用 JSON 时需要对应的数据模型,虽然 Apifox 提供了生成数据模型的功能,但是不够简便,接口有改动需要手动生成并且替换到项目内,开发体验并不是很好。

针对以上的痛点,基于 Node.js 开发了一个使用简单,功能强大的工具。

数据模型生成

首先要解决的问题是数据模型怎么生成,经过调研,发现已经有很多优秀的轮子走在前面,可以开箱即用,此处感慨开源的力量是无限的。
最后选择了 quicktype, 开发者提供了在线工具,而将使用它的核心依赖包 quicktype-core 来开发自己的工具。

quicktype 可以接收一个 JSON Schema 格式的 Model 描述字符串,根据目标语言的设置,转换为模型字符串数组,拼装后输出到指定文件内。

调用方法如下:

/**
 * @description: 单个 Model 转换
 * @param {string} language 目标语言
 * @param {string} messageName Model 名称
 * @param {string} jsonSchemaString Model JSON Schema 字符串
 * @param {LanguageOptions} option 目标语言的附加设置
 * @return {string} 转换后的 Model 内容
 */
async function convertSingleModel(
  language: string,
  messageName: string,
  jsonSchemaString: string,
  option: LanguageOptions
): Promise<string> {
  const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());

  await schemaInput.addSource({
    name: messageName,
    schema: jsonSchemaString,
  });

  const inputData = new InputData();
  inputData.addInput(schemaInput);

  const { lines } = await quicktype({
    inputData,
    lang: language,
    rendererOptions: option,
  });

  return lines.join('\n');
}

...

/**
 * @description: 单个转换后的 Model 写入文件
 * @param {ModelInfo} modelInfo 转换后的 Model 信息
 * @param {string} outputDir 输出目录
 * @return {*}
 */
function outputSingleModel(modelInfo: ModelInfo, outputDir: string): void {
  const {
    name, type, region, suffix, snake,
  } = modelInfo;
  let filePath = join(region, type, `${name}.${suffix}`);
  if (snake) {
    filePath = snakeNamedConvert(filePath); // 对有蛇形命名要求的语言转换输出路径
  }

  filePath = join(outputDir, filePath);

  const outputDirPath = dirname(filePath);

  try {
    fs.mkdirSync(outputDirPath, { recursive: true });
  } catch (error) {
    errorLog(`创建目录失败:${outputDirPath}`);
  }

  let { content } = modelInfo;

  // 后置钩子,在转换后,输出前调用,用于统一修改输出内容的格式
  if (hooks[modelInfo.language]?.after) {
    content = hooks[modelInfo.language].after(content);
  }

  try {
    writeFileSync(filePath, content);
  } catch (error) {
    errorLog(`写入文件失败:${filePath}`);
  }
  successLog(`${filePath} 转换成功`);
}
复制代码

要注意的是,当输入的对象中有嵌套对象的时候,转换器会在传入的 JSON Schema 中的 definitions 字段寻找对应的引用,所以需要传入完整的 definitions,或者提前对对象递归查找会引用到的对象提取出来重新拼装 JSON Schema。

提效

上面完成了对一个 Model 的转换和输出,这样还做不到提效,如果可以做到批量转换想要的接口的 Model,岂不美哉?

为了满足上面的目标,工具以 npm 包形式提供,全局安装可以使用 bb-model 命令触发转换,只需要在项目中放置一个配置文件即可,配置文件内容如下:

url_config

具体字段含义:

language:目标语言
indexUrl:swagger 文档 Url
output:输出路径,相对于当前配置文件
apis:需要转换的接口

使用 bb-model 命令输出如下

model

这个方案的配置文件可以随着项目一起由版本控制工具管理,利于多成员协作,后续集成到 CI/CD 中也很简单。

La conversión de modelos es la última pieza del rompecabezas en la primera fase de la ingeniería de la API de Baibo, que mejora en gran medida la eficiencia de desarrollo de los estudiantes del cliente.

resumen

Hasta el momento, se ha completado todo el proceso de la primera fase de ingeniería API. En el futuro, se agregará la compatibilidad con la verificación de lint de los archivos proto y los archivos de compilación de la interfaz se publicarán en forma de etiquetas, lo que agregará compatibilidad con el lenguaje Java.

Referencias

[1] protocolo: github.com/protocolbuf…

[2] tienda de cerveza: github.com/go-kratos/b…

[3] errores de kratos: go-kratos.dev/docs/compon…

[4] openapiv2: github.com/grpc-ecosys...

[5] validar: github.com/envoyproxy/…

[6] apifox: www.apifox.cn/

[7] núcleo de tipo rápido: www.npmjs.com/package/qui…

[8] gitrunner: docs.gitlab.com/runner/

Para cosas más emocionantes, preste atención a nuestra cuenta pública "Tecnología de Cien Botellas", ¡hay beneficios irregulares!

Supongo que te gusta

Origin juejin.im/post/7085231962767491086
Recomendado
Clasificación