zoukankan      html  css  js  c++  java
  • 前端小工具:脚本拉取swagger文档

    前端小工具:脚本拉取swagger文档

    前后端分离,后端把接口API使用swagger文档展示给前端,前端又需要手动把swagger文档拷贝修改成前端可以调用的接口,几个接口都还好,一下子来个几十个接口,复制粘贴都成了问题。

    总结一下问题:

      1. 前端需要手动定义接口函数,配置文档,增加开发时间。

      2. 拷贝文档接口,参数容易错乱异常,增加联调时间。

      3. 前端文档不统一,不同项目,不同开发者,手动配置文档不一致,增加项目使用的复杂性。

    现在项目的开发,很多时候后端都是会把一个需求的接口开发完成后,全部丢给前端,这样联调对于前端来说,时间非常紧凑。

    swagger文档支持json结构的接口,一般都再文档的头部,没有得找后台去配置了。

    基本思路就有了,通过swagger的json生成接口文件,通过node读取json文件生成API请求函数,页面直接调用就可以了。

    需要支持的功能

      1. 自动拉取swagger文档,生成配置文件

      2. 支持多个swagger文档拉取。

      3. 支持老版本,没有swagger文档的手动输入。

      4. 构建生成前端请求接口函数

    完成脚本拉取swagger文档,接口联调可以在后端释放接口文档的同时,直接进行接口联调了,为摸鱼又争取了一波时间。

    上手代码构建:

    新建swagger.lib目录,添加拉取文件generate.js,配置文件config.js

    config.js :

    简单描述一下:

    swaggerUrl: 就是swagger的json文档的URL
    baseURL:接口请求地址的baseURL,生产环境需求配置其跨域处理。
    fileName: 接口本地目录
    手动配置的文档,注意fileName是否一致,多个swagger文档的话,多添加一个结构就可以了。

    构建generate.js:

    生产文档使用nunjucks去动态设置文件内容

    function getFileHeaderTmpl() {
      let fileHeaderTmpl = `// 该文档由脚本自动生成,请勿修改
    // {{ description }}
    `;
      return fileHeaderTmpl;
    }
    
    function getTmpl() {
      const tmpl = `
    /**
     * {{ TagName }}
     * {{ description }}
     * {% for param in params %}@param (Request {{param.in}}) {% if param.required %}(Optional) {% endif %}{ {{param.type|default('object')}} } {{param.name}} {{param.description}}
     * {% if param.raw %}raw { {% for raw in param.raw %} 
     *   {{raw.key}}: {{raw.type}} {% if raw.description %}// {{raw.description}} {% endif %} {% endfor %}
     * } {% endif %}{% endfor %}
     * {@link {{docPath}}/{{ functionName }}}.
     */
    exports.{{ functionName }} = {
      server: '{{server}}',
      method: '{{ method }}',
      url: '{{ url }}',
      msg: '请求[{{description}}]出错',
      consumes: '{{consumes}}'
    };
    `;
      return tmpl;
    }

    先构建一下文件的结构,其中{%%}等的,就是nunjucks的模板语法了,不懂得自行去了解一下。

    构建generate函数,拉取和处理文件生成输入

    function generate(option) {
      if (!option.swaggerUrl) {
        return;
      }
      const { swaggerUrl, fileName, baseURL: server } = option;
      axios
        .get(swaggerUrl, {
          proxy: {
            host: '127.0.0.1',
            port: 8899,
          },
        })
        .then((result) => {
          if (result.data.swagger !== '2.0') {
            throw new Error('unknow support swagger version');
          }
          const Specification = result.data;
          const BasePath = Specification.basePath;
          const DocPath = `http://${Specification.host}${BasePath}swagger-ui.html`;
          const ApiByTag = {};
          for (let i = 0; i < Specification.tags.length; i++) {
            const CurrentTag = Specification.tags[i];
            const TagName = CurrentTag.description
              .split(' Controller')[0]
              .replace(/s+/g, '');
            ApiByTag[TagName] = {
              fileName: `${uppperFirstChar(TagName)}.spec.js`,
              content: nunjucks.renderString(getFileHeaderTmpl(), {
                description: CurrentTag.name,
              }),
            };
            for (let [keyOfPaths, valueOfPaths] of Object.entries(
              Specification.paths
            )) {
              let isMatched = false;
              for (let [keyOfMethod, valueOfMethod] of Object.entries(
                valueOfPaths
              )) {
                if (valueOfMethod.tags[0] === CurrentTag.name) {
                  isMatched = true;
                  const functionName = valueOfMethod.operationId;
                  const renderString = nunjucks.renderString(getTmpl(), {
                    server,
                    TagName,
                    description: valueOfMethod.summary,
                    functionName,
                    method: keyOfMethod,
                    url: (BasePath + keyOfPaths).replace(////g, '/'),
                    params: handleParameters(
                      keyOfMethod,
                      valueOfMethod.parameters,
                      Specification.definitions
                    ),
                    docPath: DocPath + '#/' + CurrentTag.name,
                    consumes: valueOfMethod.consumes,
                  });
                  ApiByTag[TagName].content += renderString;
                }
              }
              if (isMatched) {
                delete Specification.paths[keyOfPaths];
              }
            }
            const dirPath = path.resolve(__dirname, fileName);
            const filePath = fs.existsSync(dirPath);
            if (!filePath) {
              fs.mkdirSync(dirPath);
            }
            const pathOfFile = path.resolve(dirPath, ApiByTag[TagName].fileName);
            const fileExist = fs.existsSync(pathOfFile);
            if (fileExist && forceOverwrite === false) {
              console.warn(`file ${pathOfFile} exist, skiping`);
            } else {
              console.warn(`generating file: ${pathOfFile}`);
              fs.writeFileSync(pathOfFile, ApiByTag[TagName].content);
            }
          }
        })
        .catch((e) => {
          console.error(e);
        });
    }

    其中axios中的proxy就是whistle的配置了,不了解的可以去看看,whistle本地代理

    对于raw结构的参数需要单独处理

    function uppperFirstChar(string) {
      return string.charAt(0).toUpperCase() + string.slice(1);
    }
    
    function handleParameters(method, params, option) {
      if (method === 'get' || !params) {
        return params;
      }
      const newParams = [];
      params.forEach((item) => {
        if (item.schema && Object.keys(item.schema).includes('$ref')) {
          const { $ref: refPath } = item.schema;
          const definitions = refPath.split('#/definitions/')[1].trim();
          const { properties } = option[definitions];
          const raw = [];
          for (let [keyParam, valueParam] of Object.entries(properties)) {
            raw.push({
              ...valueParam,
              key: keyParam,
            });
          }
          item['raw'] = raw;
        }
        newParams.push(item);
      });
      return newParams;
    }

    这样,拉取swagger文档就已经完成了。

    拉取的文件大概就是长成这个样:

     文件有了,然后就把文件导出来生成前端请求的API就可以了。

    先构建个request文件,跟普通的请求封装没有太多区别,直接上代码了:

    const requestInterceptor = config => {
      config.params = config.params || {};
      if (config.method.toUpperCase() === 'GET') {
        config.params = {
          ...config.params,
          ...config.data
        };
      }
      config.params.md = Math.random();
    
      log.info(`request [${config.method}] ${config.baseURL}${config.url}`);
      log.debug(`params [${JSON.stringify(config.params)}]`);
      log.debug(`data [${JSON.stringify(config.data)}]`);
    
      return config;
    };
    
    const requestInterceptorError = error => {
      log.error(error.toString());
      return Promise.reject(error);
    };
    
    const responseInterceptor = response => {
      if (!response.data || (response.data && !response.data.success)) {
        log.warn(
          `request ${response.config.url} faild: ${JSON.stringify(response.data)}`
        );
      }
      if (typeof response.data !== 'object') {
        return {
          success: false,
          msg: 'ERROR_EMPTY_BODY'
        };
      }
      if (!response.data.success && typeof response.data.msg !== 'string') {
        response.data.msg = 'ERROR_UNKNOWN_REASON';
        return response;
      }
      return response;
    };
    
    const responseInterceptorError = (error = {}) => {
      if (error.request) {
        log.warn(`request ${error.request.path} faild: ${error.toString()}`);
      } else {
        log.error(error);
      }
      if (
        error.message.includes('timeout') ||
        error.message.includes('Network Error')
      ) {
        error.data = {
          success: false,
          msg: 'ERROR_REQUEST_TIMEOUT'
        };
      }
      error.data = {
        success: false,
        msg: 'ERROR_REQUEST_FAILD'
      };
      return error;
    };
    
    const emptyFunction = () => { };
    
    module.exports = class Request {
      constructor({
        apiStyle = 'cps',
        requestHandle,
        requestErrorHandle,
        responseHandle,
        responseErrorHandle
      }) {
        const instance = axios.create({
          timeout: 90000,
          withCredentials: true,
          headers: { 'content-type': 'application/json;charset=utf-8' },
          // 覆盖掉外面的全局axios配置
          baseURL: ''
        });
        instance.interceptors.request.use(
          requestInterceptor,
          requestInterceptorError
        );
        if (
          typeof requestHandle === 'function' ||
          typeof requestErrorHandle === 'function'
        ) {
          instance.interceptors.request.use(
            requestHandle || emptyFunction,
            requestErrorHandle || emptyFunction
          );
        }
    
        instance.interceptors.response.use(
          responseInterceptor,
          responseInterceptorError
        );
    
        if (
          typeof responseHandle === 'function' ||
          typeof responseErrorHandle === 'function'
        ) {
          instance.interceptors.response.use(
            responseHandle || emptyFunction,
            responseErrorHandle || emptyFunction
          );
        }
        return instance;
      }
    };

    有特别的请求头,响应头处理,这里也可以稍微了解,添加一下特定的处理方式就可以了。

    接下来就是导出的index:

    const ServiceSpecsMap = new Map();
    const isServer = typeof window === 'undefined';
    // 允许客户端透传的header
    const AllowRequestHeaderKeys = ['cookie'];
    // 允许透传回客户端的header
    const AllowResponseHeaderKeys = ['set-cookie'];
    // 单例
    let ServiceSingleton = null;
    function getServiceSpecsMap(option) {
      if (isServer) {
        // 扫描文件夹下的所有定义文件
        require('fs')
          .readdirSync('./libs/swagger.lib/' + option.fileName)
          .forEach((file) => {
            if (file.endsWith('.spec.js')) {
              const specName = file.split('.spec.js')[0];
              try {
                const specObj = require('./' +
                  option.fileName +
                  '/' +
                  specName +
                  '.spec.js');
                const keysOfSpec = Object.keys(specObj);
                if (keysOfSpec.length === 0) {
                  throw new Error('spec file not found any api definition');
                }
                // 缓存到map中
                ServiceSpecsMap.set(specName, specObj);
              } catch (e) {
                log.warn(`load spec ${file} faild.`);
              }
            }
          });
      } else {
        // 使用Webpack require.context 动态引入所有符合后缀的服务描述文件
          const path = './' + option.fileName + '/';
          const ServicesSpecModules = require.context('./', true, /.spec.js$/);
          var reg = new RegExp(path);
          ServicesSpecModules.keys().forEach((key) => {
            // 名字转key
            if (reg.test(key)) {
              const specName = key.replace(path, '').replace('.spec.js', '');
              // 所有目录下,specName和文件中的exports Name不允许完全一致。
              ServiceSpecsMap.set(
                specName,
                Object.assign(
                  {},
                  ServiceSpecsMap.get(specName),
                  ServicesSpecModules(key)
                )
              );
            }
          });
      }
    }
    config.forEach((i) => getServiceSpecsMap(i));
    
    const errorHandle = (result, config) => {
      const { ignoreError = false } = config;
      if (!isServer && typeof result === 'object' && !result.success) {
        // 提示弹窗
        let ErrorMsg = result.msg || `response.data:${JSON.stringify(result.data)}`;
        if (isVerbose) {
          ErrorMsg += `(${result.exception || '无更多错误信息'})`;
        }
        const { Toast } = require('antd-mobile');
        // 绑定微信接口fmp/member/bind/bindWx在入口文件,不需要toast提示,单独处理
        if (config.url.indexOf('fmp/member/bind/bindWx') === -1 && !ignoreError) {
          Toast.fail(`[内部错误]${ErrorMsg}`, 2);
        }
      }
    };
    
    const responseHandleFactory = (spec) => (response) => {
      const { data, headers, config } = response;
      if (isServer) {
        // 如果是服务端渲染,需要回写cookie到浏览器
        const CLSUtil = require('../CLS.lib');
        // Cannot convert undefined or null to object
        if (headers) {
          CLSUtil.setResponseHeaders(headers, AllowResponseHeaderKeys);
          // 因为某些接口在node端调用需要依赖前一个接口的cookie值,所以把接口响应返回的cookie写入req中
          CLSUtil.setRequestHeaders(headers, AllowResponseHeaderKeys);
        }
      }
      if (typeof data === 'object') {
        if (data.success) {
          return data;
        }
        switch (data.msg) {
          case 'ERROR_EMPTY_BODY':
            data.msg = '[返回数据为空]';
            break;
          case 'ERROR_UNKNOWN_REASON':
            data.msg = '[未知错误]';
            break;
          case 'ERROR_REQUEST_TIMEOUT':
            data.msg = '[请求超时]';
            break;
          case 'ERROR_REQUEST_FAILD':
            data.msg = '[请求失败]';
            break;
          default:
        }
        // 调试环境把接口定义文件中的提示也输出
        if (isVerbose) {
          data.msg = `${data.msg}${spec.msg}`;
        }
      }
      // TODO 这里不做await,是因为堵塞了,会导致外面的loading一直在转,但是这里有需要弹出登录框,交互有冲突
      errorHandle(data, config);
      return data;
    };
    
    function requestWithSpec(spec, data, options = {}) {
      if (typeof spec.server === 'string') {
        options.baseURL = spec.server;
      }
      let apiStyle = spec.apiStyle;
    
      // 如果是服务端去CLS中获取调用链上设置的header
      if (isServer) {
        const CLSUtil = require('../CLS.lib');
        options.baseURL = CLSUtil.getBaseURL() || spec.server;
        const serverRequestHeaders = CLSUtil.getRequestHeaders(
          AllowRequestHeaderKeys
        );
        options.headers = Object.assign(
          options.headers || {},
          serverRequestHeaders
        );
      }
      if (spec.consumes) {
        options.headers = Object.assign(options.headers || {}, {
          'content-type': spec.consumes,
        });
      }
    
      const responseHandle = responseHandleFactory(spec);
      const request = new Request({
        responseHandle: responseHandle,
        responseErrorHandle: responseHandle,
        apiStyle,
      });
      let requestUrl = spec.url;
      // 支持URL参数
      if (typeof options.urlParams === 'object') {
        Object.keys(options.urlParams).map((key) => {
          requestUrl = requestUrl.replace(`:${key}`, options.urlParams[key]);
        });
      }
      //如果查询参数直接是在请求地址后面 /picture/12
      let urlReg = /({.+?})/g;
      if (urlReg.test(requestUrl)) {
        let newData = JSON.parse(JSON.stringify(data));
        let newUrl = requestUrl.replace(urlReg, function () {
          return newData[Object.keys(newData)[0]];
        });
        requestUrl = newUrl;
      }
      return request({
        method: spec.method,
        url: requestUrl,
        data: data,
        ...options,
      });
    }
    
    // 并行发起请求
    const parallel = async (requestList) => {
      if (Array.isArray(requestList) === false) {
        throw new Error('service parallel must accept Array as params.');
      }
      return await Promise.all(requestList);
    };
    
    class Service {
      constructor() {
        if (ServiceSingleton) {
          return ServiceSingleton;
        }
    
        ServiceSingleton = {
          parallel,
        };
        ServiceSpecsMap.forEach((value, key) => {
          const ServiceRequest = {};
          const ServiceSpecInstance = value;
          Object.keys(ServiceSpecInstance).forEach((requestName) => {
            ServiceRequest[requestName] = (data, options = {}) => {
              const RequestSpec = ServiceSpecInstance[requestName];
              return requestWithSpec(RequestSpec, data, options);
            };
          });
    
          ServiceSingleton[key] = ServiceRequest;
        });
        return ServiceSingleton;
      }
    }
    /**
     * @param data raw 参数
     * @param option header等配置
     */
    const ServiceInstance = new Service();
    module.exports = ServiceInstance;

    部分对于服务端调用接口,都cookie的处理需要添加一下工具

    const cls = require('cls-hooked');
    const CLS_NAMESPACE = 'CLS_NAMESPACE';
    module.exports.getBaseURL = (url) => {
      const ns = cls.getNamespace(CLS_NAMESPACE);
      if (ns) {
        try {
          const baseURL = 'url';//ns.get('tenant').baseURL;
          return baseURL;
        } catch (error) {
          console.log(error)
        }
        
      }
    };
    
    // 获取客户端传过来的header
    module.exports.getRequestHeaders = (allowed = []) => {
      const ns = cls.getNamespace(CLS_NAMESPACE);
      if (ns) {
        const headers = ns.get('headers');
        if (headers) {
          // 注意这里是区分大小写的
          return Object.keys(headers)
            .filter(key => allowed.includes(key))
            .reduce((obj, key) => {
              obj[key] = headers[key];
              return obj;
            }, {});
        }
      }
      return {};
    };
    
    module.exports.setResponseHeaders = (headers, allowed = []) => {
      const ns = cls.getNamespace(CLS_NAMESPACE);
      if (ns) {
        const expressSetResHeader = ns.get('expressSetResHeader');
        if (typeof expressSetResHeader === 'function') {
          // 注意这里是区分大小写的
          return Object.keys(headers).map(key => {
            if (allowed.includes(key)) {
              // 把header回写到浏览器
              expressSetResHeader(key, headers[key]);
            }
          });
        }
      }
      return {};
    };
    module.exports.setRequestHeaders = (headers, allowed = []) => {
      const ns = cls.getNamespace(CLS_NAMESPACE);
      if (ns) {
        const expressSetReqHeader = ns.get('expressSetReqHeader');
        if (typeof expressSetReqHeader === 'function') {
          // 遍历响应的headers
          return Object.keys(headers).map(key => {
            if (allowed.includes(key)) {
              // 把header回写到req
              expressSetReqHeader(
                key === 'set-cookie' ? 'cookie' : key,
                headers[key]
              );
            }
          });
        }
      }
      return {};
    };

    到这来基本所有的代码配置都完成的了。

    页面的使用,直接按文件路径+导出的命称就可以了

    import * as Service from 'libs/swagger.lib'; 
    Service.OldCustom.smallbore(fromData, {
          headers: {
            'Content-Type': 'multipart/form-data',
          },
        }).then(res => {
          console.log(res)
        });

    拉取swagger是时候,可以把命令配置到package.json里面的scripts去

    "swagger": "node libs/swagger.lib/generate.js"

    通过npm run swagger就可以拉取了。

    脚本拉取swagger文档,就完成了。

    smallbore,world
  • 相关阅读:
    游标、动态sql、异常
    定义declare、%TYPE%、ROWTYPE、加循环
    存储过程
    游标
    异常
    常用的sql语句(转)
    MVC的理解
    模拟struts2
    结构化分析方法
    Maven常用命令
  • 原文地址:https://www.cnblogs.com/bore/p/15457680.html
Copyright © 2011-2022 走看看