Api配置一键生成ts接口

368 阅读6分钟

前端如何封装protobuf进行数据交互 前端protobuf请求响应封装(axios)

前端如何压缩proto.json文件为最简的json在这里前端压缩proto文件.json

必须先看这两篇文章.更容易懂我本篇文章

之前我们调用一个接口的时候,api是这么写的,那么我们是不是需要每个都自己这些去写,还要加上对应的ts接口呢?

export function rechargeBonusList(data) {
  return request({
    reqDesction: 'RechargeBonusListReq',
    resDesction: 'RechargeBonusListRes',
    funcName: 'RechargeBonusList',
    data,
    method: "post",
    url: `/proxymsg`,
    serverName: 'XXXXXX',
  })
}

现在我们要做的事,就是通过json配置出api,并且生成ts接口,TypeScript 通过类型注解提供编译时的参数传入静态类型检查,以及输出时候vscode的提示.

1.我们要实现什么?

只需要写入接口的基本配置:

1666323894986.png

能够生成:

ts接口以及vue组件中可直接调用的api

1666324043308.png

1666323981247.png

2.代码以及思路

2.1先安装包
npm i colors prettier --save
2.2 配置运行指令
 "scripts": {
    "apiJsonToTs": "node src/protobuf/apiJsonToTs/index"
  },
2.3 思路
const colors = require("colors");
const path = require("path")

const { deleteDeadFile } = require("./utils/index.js");

const miniJsonPath = path.resolve(__dirname, '../protoFile/index.min.json');
const interfaceTs = path.resolve(__dirname, '../protoFile/interface.ts');
const apiTsPath = path.resolve(__dirname, './api/index.ts')

const deadFileArr = [miniJsonPath, interfaceTs, apiTsPath]

// 1.在执行之前把之前的文件先清空

deadFileArr.forEach(item => {
  deleteDeadFile(item)
})


// 2.首先应该对pb进行压缩-开启子进程, 一定要同步的,完成后执行下面
const { execSync } = require('child_process')
execSync(`npx pbjs -t json src/protobuf/protoFile/*.proto > src/protobuf/protoFile/proto.json`, {
  encoding: "utf8",
})

// 3.拿到所有的pb转化的json数据
const allProtoJson = require('../protoFile/proto.json')

// 4. 拿配置的接口与全部的json进行匹配,主要是根据名字来匹配,只保留当前的需要的,进行压缩
// 如果存在index.min.json先自动删除

const projectCurrentApiJson = require('./api/conf.json').api

const { minifyPb } = require("./minify");
let methods = minifyPb( // 一定写入的时候要用同步
  projectCurrentApiJson,
  allProtoJson,
  miniJsonPath,
)

console.log(colors.green("pb压缩成功, 生成index.min.json"));

// 5.生成ts接口

execSync(`node src/protobuf/toTs.js --bp`, { encoding: "utf8", stdio: 'inherit' });
console.log(colors.green("pb转换ts接口成功,生成protoFile/interface.ts"));

// 6.生成api页面

const { toTsInterface } = require("./toTsInterface");

toTsInterface(methods, apiTsPath)

之前的文章已经完成了1-4步,今天来完成5.6步

第4步基于之前有一点新增

minify.js

const fs = require("fs")

// 原始类型 不需要递归(不需要作为key)
const primitiveTypes = [
  "int32",
  "int64",
  "sint32",
  "sint64",
  "uint32",
  "uint64",
  "bytes",
  "string",
  "bool",
];
 /**--------------------------------- new start ---------------------- */
/**
 * 定义出指定的methods格式
 */
function setSubMethodKey(subMethods, fileName, requestType, responseType, isReqEmpty) {
  return Object.assign(subMethods, {
    extFile: fileName,
    description: {
      req: requestType,
      res: responseType,
    },
    isReqEmpty,
    processed:true,
  })
}
 /**--------------------------------- new end ---------------------- */

// 保存压缩后的代码
function saveMiniJson(data, filepath) {
  const str = JSON.stringify(data,"","\t") // 格式化成字符串
  fs.writeFileSync(filepath, str, { 'flag': 'a' }, function(err) {
    if (err) {
      throw err;
    }
  });
}

/**
 * 给nested添加方法
 * @param {*} nested
 * @param {*} newNested
 * @param {Array} methods
 * @return [types]  方法用到的类型
 */
 function addFunctionToNested(nested, newNested, methods) {
  // 方法需要的types,也就是对应的json里面package的nest底下的key
  // 首先需要确定的是所需要的方法是从方法名从而拿到请求和相应需要的所有的type
  // 而含属于请求的可以通过固定的XXX进行匹配到
  // 1.拿到与请求响应有关的key [ 'ActivityXXX', 'IndexXXX' ]
  const requestResponseKeys =  Object.keys(nested).filter((key) => {
    return /xxx$/i.test(key)
  })
  // 2.api中含有的请求响应的key, methods是一定在requestResponseKeys里面的,所以挑出methodsInfo存在的数据
  const types = [] // [ 'GetTimestampReq', 'GetTimestampRes' ]
  requestResponseKeys.forEach(ajaxKey => {
    methods.forEach(subMethods => {
      const methodsInfo = nested[ajaxKey]["methods"][subMethods.name]
      // 如果存在数据
      if (methodsInfo) {
        const { requestType, responseType } = methodsInfo
        const ajaxArr = [requestType, responseType]
        // 2.1 types里面加入item
        ajaxArr.forEach(item => {
          if(!types.includes(item)) {
            types.push(item)
          }
        })
        // 2.2存在的就给newNested加上
        // 给新的newNested添加上, 存在的api上的方法
        newNested[ajaxKey] = newNested[ajaxKey] || { methods: {} };
        newNested[ajaxKey]["methods"][subMethods.name] = methodsInfo;
        // 2.3 给methods创建新的值
        /**--------------------------------- new start ---------------------- */
        
        const fileName = ajaxKey.replace(/XXXX$/i, "");// 拿到proto文件的名字--index
        const isReqEmpty = Object.keys(nested[requestType].fields).length === 0;
        subMethods = setSubMethodKey(subMethods, fileName, requestType, responseType, isReqEmpty)
        /**--------------------------------- new end ---------------------- */
      }
    })
  })
  return types;
}

const commonTypes = ["RPCOutput", "RPCInput"];
// 添加消息体
function addTypesToNested(nested, newNested, types) {
  // 1.得到的types应该还要加上服务端统一的请求
  types = types.concat(commonTypes);
  for (let i = 0; i < types.length; i++) {
    // 2.如果nested[types[i]]存在但是newNested[types[i]不存在,就进行赋值
    if (!newNested[types[i]] && nested[types[i]]) {
      newNested[types[i]] = nested[types[i]];
      // 3.判断fileds里面是不是基础类型,如果是,就不用管,不是就作为types中的 Enum类型没有fileds
      let fields = nested[types[i]].fields || {};
      Object.keys(fields).forEach((key) => {
        // 如果不是基本类型,就是可以需要被单独弄成属性的, types要新增
        if (!primitiveTypes.includes(fields[key].type)) {
          types.push(fields[key].type); // 新增后,引用类型可以新增循环
        }
      });
    }
  }
}
  
/**
 * 打包压缩pb
 * @param {*} methods  所需方法配置
 * @param {*} pbJson   原有pb
 * @param {*} savePath 保存路径
 * @return formatMethods
 * {
 *   name:'',
 *   file:'',
 *   extFile:'',
 *   description:{
 *     req:'',
 *     res:''
 *   },
 *   isReqEmpty:false,
 *   processed:false
 * }
 */
function minifyPb(methods, pbJson, savePath) {
  const formatMethods = methods
  // 1.拿到所有的package ,也就是json的第一层key .类似于pb, gm_pb等
  const pbs = Object.keys(pbJson.nested); // [ 'pb' ]  拿到所有的packages
  // 2.创建空json对象来创建
  let newNested = {};
  pbs.forEach((pb) => {
    // 2.1 为每个package创建新的对象
    newNested[pb] = { nested: {} };
    const nested = pbJson.nested[pb].nested; // 每个package底下所有的用到的方法
    // 2.2 为每个package添加必要的方法 [ 'GetTimestampReq', 'GetTimestampRes' ]
    let needTypes = addFunctionToNested(
      nested,
      newNested[pb].nested,
      formatMethods
    );
    // 添加消息体
    addTypesToNested(nested, newNested[pb].nested, needTypes);
  });

  // 保存JSON
  saveMiniJson(Object.assign(pbJson, { nested: newNested }), savePath)
  return formatMethods;
}

module.exports = {
  minifyPb,
};

// readFileSync 是同步,也就是在当前的代码依赖上一个文件的生成就用同步
//没关系,可以用异步

第5步

export interface GetTimestampRes {
  current: number;
  currentTime: string;
}

enum MoneyLogType{
  MLT_ZERO = 0;//其他
  MLT_GAME_PRIZE = 1;//游戏奖金
}

首先我们可以确定的是,如果是req或者res就是上面第一种,value为基本类型的值,直接去primitiveTypeMap里面进行匹配, 如果不是,就写type名字,第二种就是枚举类型的中间用等号.这两种一个是fieldsData类型,一种是valuesData来判断.

有几种是不需要去判断枚举的,第一种就是与接口有关,第二种就是后端定义的, 第三种是req为空的(要么有数据,要么不为req).

toTs.js

const path = require("path");
const fs = require("fs");
const colors = require("colors");
const { saveFile, beautifyJs } = require("./apiJsonToTs/utils/index.js");
const entry = path.resolve(__dirname, './protoFile/index.min.json' )
const output = path.resolve(__dirname, './protoFile/interface' )
if (!fs.existsSync(entry)) {
  console.log(colors.red("未找到protoFile/index.min.json, 请先打包pb文件"));
  process.exit(1);
}

// 原始类型
const primitiveTypeMap = {
  int32: "number",
  int64: "number",
  sint32: "number",
  sint64: "number",
  uint32: "number",
  uint64: "number",
  bytes: "Uint8Array",
  string: "string",
  bool: "boolean",
};

let res = "";
const allNested = require(entry).nested;
const pbs = Object.keys(allNested);

// 给req的参数和res的返回替换为前端的校验类型
pbs.forEach((pb) => {
  const nested = allNested[pb].nested;
  for (const key in nested) {
   // 有几种是不需要去判断枚举的,第一种就是与接口有关,第二种就是后端定义的, 
   // 第三种是req为空的(要么有数据,要么不为req)
    const isXXXX = /XXXX$/i.test(key) // 不是XXX开头的
   const isRpc = ["XXXOutXXX", "XXXInXXXX"].includes(key) // 不包含后端定义的
    const fieldsData = nested[key].fields  
    const valuesData = nested[key].values
    const isReq = /req$/i.test(key)
    if (!isExtObj && !isRpc) {
      // 如果是fieldData 
      if(fieldsData) {
        const hasFieldsKeyData = Object.keys(fieldsData).length // 必须存在值
        if(!isReq || hasFieldsKeyData ) {
          res += `export interface ${key}{`;
          for (const subKey in fieldsData) {
            const { type, rule } = fieldsData[subKey]
            let primitive = primitiveTypeMap[type];
            // subKey对应current和currentTime,如果在基础类型里面找不到,就应该是数组或者对象,直接写type
            res += `${subKey}:${primitive && primitive || type }`;
            if (rule === "repeated") {
              res += "[]";
            }
            res += ";";
          }
          res += "};";
        }
      // 如果是valuesData - 就是枚举
      } else if (valuesData) {
        res += `enum ${key}{`;
        for (const subKey in valuesData) {
          res += `${subKey}=${valuesData[subKey]},`;
        }
        if (res.endsWith(",")) res = res.slice(0, -1);
        res += "};";
      }
    }
  }
});

// 格式化代码
saveFile(beautifyJs(res), `${output}.ts`);

第6步

export function apiRechargeBonusList(
  data: RechargeBonusListReq
): Promise<RechargeBonusListRes> {
  return request({
    reqDesction: "RechargeBonusListReq",
    resDesction: "RechargeBonusListRes",
    funcName: "RechargeBonusList",
    data: data,
    method: "post",
    url: "/proxymsg",
    serverName: "XXXXX"
  } as AxiosRequestConfig<any>) as unknown as Promise<RechargeBonusListRes>;
}

主要是通过一些动态的数据,形成以上的格式

const { beautifyJs, saveFile } = require("./utils/index.js");
const colors = require("colors");
let reqResArr = [];
let apiStr = "";

// 形成str content
function getStr(methods) {
  for (const method of methods) {
    const { remark, isReqEmpty, description: { req, res}, name, extFile, file  } = method
    const currentFile = file || extFile
    // 获取reqResArr参数以及params
    let params = ""
    if (!isReqEmpty) {
      reqResArr.push(req);
      params = `data:${req} `
    }
    reqResArr.push(res);
  
    // 形成api
    apiFunStr({name, currentFile, params, remark, res, isReqEmpty})
  }

  // 引入request
  apiStr = `import { ${Array.from(new Set(reqResArr)).join(",")}, } 
  from '../../protoFile/interface';\n import request 
  from "../../component/request"\n import { AxiosRequestConfig } from "axios"` + apiStr;
  return apiStr
}

// api生成
function apiFunStr({name, currentFile, params, remark, res, isReqEmpty}) {
  const reqDesction = `${name}Req`
  const resDesction = `${name}Res`
  const serverName = `gamecomm.${currentFile.toLowerCase()}.${currentFile}XXXX`
  apiStr += `
    /**
     * ${remark ? remark : 'Remark: to do'}
     */
    export function api${name}(${params}) : Promise<${res}> {
      return request({
        reqDesction: '${reqDesction}',
        resDesction : '${resDesction}',
        funcName: '${name}',
        data: ${isReqEmpty ?  '{}' : 'data' },
        method: "post",
        url: "/proxymsg",
        serverName: '${serverName}',
      } as AxiosRequestConfig<any> ) as unknown as Promise<${resDesction}>
    }\n
  `
}

// 格式化代码
const toTsInterface = (methods, apiTsPath) => {
  const apiStrRes = beautifyJs(getStr(methods));
  saveFile(apiStrRes, apiTsPath );
  console.log(colors.green("api生成成功,生成api/index.ts"));
  methods = methods.filter((method) => method.processed).map((method) => method.name);
  console.log(colors.green(`成功打包了,如下${methods.length}个方法,${JSON.stringify(methods)}`));
}

module.exports = {
  toTsInterface,
};

以上比较难的点就是axios的源码里面封装了AxiosRequestConfig,例如reqDesction, resDesction,serverName等参数都不是源码里面所定义的参数,所以需要手动覆盖

import { AxiosRequestConfig } from "axios";

export function apiRechargeBonusList(
  data: RechargeBonusListReq
): Promise<RechargeBonusListRes> {
  return request({
      ...,
  } as AxiosRequestConfig<any>) as unknown as Promise<RechargeBonusListRes>;
}

utils/index.js

const prettier = require("prettier")
const fs = require('fs')

// 格式化代码
const beautifyJs = (txt) => {
  return prettier.format(txt, {
    parser: "babel-ts",
    printWidth: 80,
    semi: true,
    tabWidth: 2, // 缩进字节数
    useTabs: false, // 缩进不使用tab,使用空格
    singleQuote: false,
    bracketSpacing: true,
    trailingComma: "es5",
  });
}

// 一旦运行,删除老文件
const deleteDeadFile = (path) => {
  if (fs.existsSync(path)) {
    fs.unlinkSync(path)
  }
}

// 保存文件
function saveFile(data, filepath) {
  fs.writeFile(filepath, data, { 'flag': 'a' }, function(err) {
    if (err) {
      throw err;
    }
  });
}

module.exports = {
  beautifyJs,
  deleteDeadFile,
  saveFile
};

3.结果

api/index.ts

import {
  RechargeBonusListReq,
  RechargeBonusListRes,
  GetTimestampRes,
} from "../../protoFile/interface";
import request from "../../component/request";
import { AxiosRequestConfig } from "axios";
/**
 * 活动奖励明细
 */
export function apiRechargeBonusList(
  data: RechargeBonusListReq
): Promise<RechargeBonusListRes> {
  return request({
    reqDesction: "RechargeBonusListReq",
    resDesction: "RechargeBonusListRes",
    funcName: "RechargeBonusList",
    data: data,
    method: "post",
    url: "/proxymsg",
    serverName: "xxxx",
  } as AxiosRequestConfig<any>) as unknown as Promise<RechargeBonusListRes>;
}

/**
 * 时间戳
 */
export function apiGetTimestamp(): Promise<GetTimestampRes> {
  return request({
    reqDesction: "GetTimestampReq",
    resDesction: "GetTimestampRes",
    funcName: "GetTimestamp",
    data: {},
    method: "post",
    url: "/proxymsg",
    serverName: "xxxx",
  } as AxiosRequestConfig<any>) as unknown as Promise<GetTimestampRes>;
}

interface.ts

export interface RechargeBonusListReq {
  actId: number;
  relActId: number;
}
export interface RechargeBonusListRes {
  items: RechargeBonusListItem[];
}
export interface GetTimestampRes {
  current: number;
  currentTime: string;
}
export interface RechargeBonusListItem {
  base: BonusAwardItem;
  createdAt: number;
  bonusType: number;
}
export interface BonusAwardItem {
  awardId: number;
  awardName: string;
  num: number;
  rechargeDiamond: number;
}

直接引入:

import { apiRechargeBonusList, apiGetTimestamp } from "../protobuf/apiJsonToTs/api/index";

const res2 = await apiRechargeBonusList(
{
  actId: xxxx
  relActId: xxx
})
console.log(res2, '拿到数据---奖励记录')


const res1 = await apiGetTimestamp()
console.log(res1, '拿到数据---时间戳')

1666337598484.png

1666337773112.png

interface.ts中RechargeBonusListReq参数必须是number,如果我写string类型的,vscode就会提示,在我获取的时候,也会根据RechargeBonusListRes来提示,你从后端拿出来的东西