proto2api如何把Protocol Buffer转换成TS文件

一、前言

如果厌倦了手写各种后端调用的api文件,可以试试proto2api的ts 生成功能,一个命令自动把Protocol Buffer文件定义的各种message、enum、services翻译成需要的TypeScript中的interface、enum、function

举个例子:

有个hello.proto,通过proto2api命令就可以自动翻译成了hello.ts,如下图所示

上面的效果只需要下面一行命令就可以实现。

# 如果没有安装 proto2api 
# sudo npm install -g proto2api
proto2api -d /xxx/xxx.proto -o xxx/api  
复制代码

上面这个例子算是比较简单的,在实际项目中,一个PB几百行并不少见,甚至上千行都有可能,更何况还有一堆的依赖文件,就算有再大的耐心,如果多几个PB尼?

会面对一系列问题,比如:

  • 效率性问题,一个个翻译,耗时巨大
  • 准确性问题,工作量一旦多了起来,单纯靠人会面对准确性问题。
  • 及时性问题,后端修改了PB,前端不一定能及时修改
  • 类型问题,PB类型非常广,但js的类型有限,另外还有类型不一致的问题,比如int64,到了前端就变成了string类型
  • 其他问题,前端喜欢用驼峰,而PB推荐用下划线;PB定义了默认值,前端如何写入

通过proto2api命令行生成TS文件就可以一次性解决上述问题

二、命令行说明

Usage: proto2api [options]

Convert proto file to api file

Options:
  -V, --version         output the version number
  --debug               Load code with ts-node for debug
  -d, --dir <type>      Directory address of Protocol Buffers. eq: /path/pbdir or /path/hello.proto
  -o, --output <type>   Output api path
  --apiName <type>      apiName (default: "webapi")
  --apiPath <type>      apiPath (default: "~/utils/api")
  --ignore [ignore...]  Ignore unnecessary generated pb files (default: "google|swagger")
  -h, --help            display help for command
复制代码

这里可能需要额外说明的是三个参数

2.1 --apiName

这个值用来代表导出的webapi的名字,这个导出对象需要实现http的get、post、put、delete、patch5个常用的http请求操作,用来支撑PB中关于service的定义。

备注:如果不知道如何实现,下面有实际代码

2.2 --apiPath

这个代表webapi的路径

2.3 --ignore

对PB文件进行生成时,PB有可能还引入了一些第三方的插件,比如google官方的http插件、swagger插件,这些插件通常是用来增强gRPC某些功能,但是在纯前端领域我们不需要关注这些,那么可以默认把它忽略掉,这样就不会生成对应的TS代码。

注意:当前很多使用gRPC基本上都用了http、swagger的插件,默认情况下会自动把这两个插件忽略生成。

具体用法如下所示

## 以下都是忽略google、swagger的插件
proto2api -d xx/svc.proto -o apps/bot/api --ignore google|swagger
proto2api -d xx/svc.proto -o apps/bot/api --ignore google swagger
# 选择全量输出
proto2api -d xx/svc.proto -o apps/bot/api --ignore
复制代码

三、webapi的说明

前端每次接口的调用,都是一次http的请求,而http请求总共有9种,我们和后端常用的有5种,每次请求api基本上主要的内容有:url、method、header、request、response,如下所示。

webapi就是用来处理所有api请求的抽象,这里提供了一个基于axios的模板,方便大家使用

import Axios, { AxiosRequestConfig, Method, AxiosInstance } from 'axios';

/**
* 解析路由参数responseType
*
*/
const reg = /:[a-z|A-Z]+/g;
export function parseParams(url: string): Array<string> {
  const ps = url.match(reg);
  if (!ps) {
    return [];
  }
  return ps.map((k) => k.replace(/:/, ''));
}

/**
* 按照url和params生成相应的url
* @param url
* @param params
*/
export function genUrl(url: string, params: WebReq['params']) {
  if (!params) {
    return url;
  }
  
  const ps = parseParams(url);
  ps.forEach((k) => {
    const reg = new RegExp(`:${k}`);
    url = url.replace(reg, params[k]);
  });
  
  const path: Array<string> = [];
  for (const key of Object.keys(params)) {
    if (!ps.find((k) => k === key)) {
      path.push(`${key}=${params[key]}`);
    }
  }
  
  return url + (path.length > 0 ? `?${path.join('&')}` : '');
}

export interface WebReq {
  params?: { [index: string]: any };
  forms?: any;
}

// export interface Re{
//   code: number;
//   result: T;
//   msg: string;
// }

export interface CustomAxiosRequestConfig extends AxiosRequestConfig {
  contentType?: 'application/json' | 'multipart/form-data' | 'text/plain';
}

export interface Headers {
  'Content-Type': string;
}

export interface Options {
  headers?: Headers;
  config?: CustomAxiosRequestConfig;
}

export class Webapi {
  public axios: AxiosInstance;
  
  public reqInterceptors: number;
  
  public resInterceptors: number;
  
  constructor(options: Options) {
    this.axios = Axios.create(options.config);
  }
  
  get<T>(
  url: string,
   params?: WebReq['params'],
   forms?: WebReq['forms'],
   config?: AxiosRequestConfig
  ): Promise<T> {
    return this.api<T>(url, { params, forms }, 'get', config);
  }
  
  delete<T>(
  url: string,
   params?: WebReq['params'],
   forms?: WebReq['forms'],
   config?: AxiosRequestConfig
  ): Promise<T> {
    return this.api<T>(url, { params, forms }, 'delete', config);
  }
  
  post<T>(
  url: string,
   params?: WebReq['params'],
   forms?: WebReq['forms'],
   config?: AxiosRequestConfig
  ): Promise<T> {
    return this.api<T>(url, { params, forms }, 'post', config);
  }
  
  put<T>(
  url: string,
   params?: WebReq['params'],
   forms?: WebReq['forms'],
   config?: AxiosRequestConfig
  ): Promise<T> {
    return this.api<T>(url, { params, forms }, 'put', config);
  }
  
  patch<T>(
  url: string,
   params?: WebReq['params'],
   forms?: WebReq['forms'],
   config?: AxiosRequestConfig
  ): Promise<T> {
    return this.api<T>(url, { params, forms }, 'patch', config);
  }
  
  api<T>(
  url: string,
   req: WebReq,
   method: Method = 'get',
   config?: AxiosRequestConfig
  ): Promise<T> {
    if (url.match(/:/) || method.match(/get|delete/i)) {
      // 如果路由是带挂参的,先看params再看forms
      url = genUrl(url, req.params || req.forms);
    }
    method = method.toLocaleLowerCase() as Method;
    switch (method) {
      case 'get':
        return this.axios.get(url, config);
      case 'delete':
        return this.axios.delete(url, config);
      case 'post':
        return this.axios.post(url, req.forms || req.params, config);
      case 'put':
        return this.axios.put(url, req.forms || req.params, config);
      case 'patch':
        return this.axios.patch(url, req.forms || req.params, config);
      default:
        return this.axios.get(url, config);
    }
  }
}
复制代码

备注:如果不习惯用axios,可以基于上面这个模板写一个类似的,只要提供好get、post这些常用http请求就可以了。

在实际业务中有很大差异,所以还需要做一定程度的本地化才行

// ~/utils/api.ts
import { Webapi, ApiBaseUrl } from '../webapi';

const timeout = 30000;

export const webapi = new Webapi({
  config: { baseURL: `${ApiBaseUrl}/api/xxx`, timeout },
});
复制代码

这也是为啥最前面演示的里面,默认地址是~/utils/api的原因。

四、引用资料

1、 HTTP request methods - HTTP | MDN

如果觉得这个库对你有帮助,欢迎star呀

github.com/yuexing0921…

猜你喜欢

转载自juejin.im/post/7078411411709231134
今日推荐