相关文章推荐
酒量小的灯泡  ·  spring ...·  1 年前    · 

如果厌倦了手写各种后端调用的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的原因。