yeoman_swagger.jpg

前言

目前很多后台的接口文档工具都使用了 swagger来完成,开发过程中,为了减少前后端的不必要沟通,接口文档通常会写的比较详细,分类也会比较明确,在 editor.swagger.io (swagger 在线编辑器)中看到接口文档时,就想,为何不把这些文档处理一下,转换成我们前端可以直接调用的工具呢?

本文会简单介绍如何处理转换` swagger`文档,并借助` yeoman` 开箱即用的` yeoman-generator` 脚手架自动化生成前端需要的接口请求函数。

正文

首先去研究一下 swagger 文档的数据结构,看看是不是能够对部分信息进行提取和转换来生成我们前端可以使用的工具,发现 editor.swagger.io 中可以直接导出 swagger.json 文件,每一个接口包含丰富的信息,部分如下:

 "paths": {
    "/pet": {
      "post": {
        "tags": [
          "pet"
        ],
        "summary": "Add a new pet to the store",
        "description": "",
        "operationId": "addPet",
        "consumes": [
          "application/json",
          "application/xml"
        ],
        "produces": [
          "application/xml",
          "application/json"
        ],
        "parameters": [
          {
            "in": "body",
            "name": "body",
            "description": "Pet object that needs to be added to the store",
            "required": true,
            "schema": {
              "$ref": "#/definitions/Pet"
            }
          }
        ],
        "responses": {
          "405": {
            "description": "Invalid input"
          }
        },
        "security": [
          {
            "petstore_auth": [
              "write:pets",
              "read:pets"
            ]
          }
        ]
      }
    }

可以发现,我们可以得到 pathsmethodparameters (包括每个参数的类型且是否必传)、description(描述)、consumes 、produces (header 中需要的一些参数),以及 responses(成功和失败的返回值数据结构)。

发现可行性真的是非常高,所以开始研究怎样实施。

目的

期望结果:


1. 应该是一个函数,函数名可以使用 operationId 字段(这个字段是 swagger 生成的,具有唯一性,且比较语义化);

2. 函数的参数应该是当前 api 需要的参数,能提示哪些参数必传,且每个参数的数据类型;

3. 每个函数仅调用当前api 的 path,自动填充 meathod,当为 GET 且 path 中有参数时自动替换;eg: 'path/list/{id}' ==> 'path/list/123'

4. 每个函数应该有详细的注释,包括 api 分类,params的数据类型和解释;

提取和转换 swagger.json

在 swagger 官网找到了这个 swagger-codegen, 根据官网描述,这个工具可以使用通过 openAPI 规范定义的接口来生成客户端 SDK。大概就是可以通过前期接口定义文档生成具体的服务端代码,看样子是对服务端的同学帮助比较大的一个工具。

image.png

github: swagger-codegen

在这个库中又发现了一个 JavaScript 生成库,swagger-js-codegen
(A Swagger Codegen for typescript, nodejs & angularjs)

他可以生成 JavaScript / TypeScript 的 api 库,由于我们项目中目前使用的是 TypeScript ,碰巧这里也有对TypeScript的实现。

在此推荐使用TypeScript的实现,因为 ts 对 params 的定义更加详细和规范,对于 params 比较多的 api 可以将 params 的类型定义提取出来,且可以复用。

这个包从一个 swagger file 中生成一个nodejs,reactjs或angularjs类。代码使用mustache templates生成,可以自定义类名,并由jshint进行质量检查,并由js-beautify进行美化,听起来不错。

但是该项目不再由其创建者积极维护,大概看了一下项目代码;

项目提供了部分生成模板文件:

angular-class.mustache    
flow-class.mustache    
flow-method.mustache    
flow-type.mustache    
method.mustache    
node-class.mustache    
react-class.mustache    
type.mustache    
typescript-class.mustache    
typescript-method.mustache

我使用react-class.mustache 试了一下:

var fs = require('fs');
var CodeGen = require('swagger-js-codegen').CodeGen;
var swagger = JSON.parse(fs.readFileSync('generators/swagger.json', 'UTF-8'));
var reactjsSourceCode = CodeGen.getNodeCode({ className: 'Test', swagger: swagger });

console.log(reactjsSourceCode);

生成文件:

/*jshint esversion: 6 */
/*global fetch, btoa */
import Q from 'q';
/**
 * This is a sample server Petstore server.  You can find out more about     Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).      For this sample, you can use the api key `special-key` to test the authorization     filters.
 * @class Test
 * @param {(string|object)} [domainOrOptions] - The project domain or options object. If object, see the object's optional properties.
 * @param {string} [domainOrOptions.domain] - The project domain
 * @param {object} [domainOrOptions.token] - auth token - object with value property and optional headerOrQueryName and isQuery properties
 */
let Test = (function() {
    'use strict';

    function Test(options) {
        let domain = (typeof options === 'object') ? options.domain : options;
        this.domain = domain ? domain : 'https://petstore.swagger.io/v2';
        if (this.domain.length === 0) {
            throw new Error('Domain parameter must be specified as a string.');
        }
        this.token = (typeof options === 'object') ? (options.token ? options.token : {}) : {};
        this.apiKey = (typeof options === 'object') ? (options.apiKey ? options.apiKey : {}) : {};
    }

    function serializeQueryParams(parameters) {
        let str = [];
        for (let p in parameters) {
            if (parameters.hasOwnProperty(p)) {
                str.push(encodeURIComponent(p) + '=' + encodeURIComponent(parameters[p]));
            }
        }
        return str.join('&');
    }

    function mergeQueryParams(parameters, queryParameters) {
        if (parameters.$queryParameters) {
            Object.keys(parameters.$queryParameters)
                .forEach(function(parameterName) {
                    let parameter = parameters.$queryParameters[parameterName];
                    queryParameters[parameterName] = parameter;
                });
        }
        return queryParameters;
    }

    /**
     * HTTP Request
     * @method
     * @name Test#request
     * @param {string} method - http method
     * @param {string} url - url to do request
     * @param {object} parameters
     * @param {object} body - body parameters / object
     * @param {object} headers - header parameters
     * @param {object} queryParameters - querystring parameters
     * @param {object} form - form data object
     * @param {object} deferred - promise object
     */
    Test.prototype.request = function(method, url, parameters, body, headers, queryParameters, form, deferred) {
        const queryParams = queryParameters && Object.keys(queryParameters).length ? serializeQueryParams(queryParameters) : null;
        const urlWithParams = url + (queryParams ? '?' + queryParams : '');

        if (body && !Object.keys(body).length) {
            body = undefined;
        }

        fetch(urlWithParams, {
            method,
            headers,
            body: JSON.stringify(body)
        }).then((response) => {
            return response.json();
        }).then((body) => {
            deferred.resolve(body);
        }).catch((error) => {
            deferred.reject(error);
        });
    };

    /**
     * Set Token
     * @method
     * @name Test#setToken
     * @param {string} value - token's value
     * @param {string} headerOrQueryName - the header or query name to send the token at
     * @param {boolean} isQuery - true if send the token as query param, otherwise, send as header param
     */
    Test.prototype.setToken = function(value, headerOrQueryName, isQuery) {
        this.token.value = value;
        this.token.headerOrQueryName = headerOrQueryName;
        this.token.isQuery = isQuery;
    };
   
   
    /**
     * This can only be done by the logged in user.
     * @method
     * @name Test#deleteUser
     * @param {object} parameters - method options and parameters
     * @param {string} parameters.username - The name that needs to be deleted
     */
    Test.prototype.deleteUser = function(parameters) {
        if (parameters === undefined) {
            parameters = {};
        }
        let deferred = Q.defer();
        let domain = this.domain,
            path = '/user/{username}';
        let body = {},
            queryParameters = {},
            headers = {},
            form = {};

        headers['Accept'] = ['application/xml, application/json'];

        path = path.replace('{username}', parameters['username']);

        if (parameters['username'] === undefined) {
            deferred.reject(new Error('Missing required  parameter: username'));
            return deferred.promise;
        }

        queryParameters = mergeQueryParams(parameters, queryParameters);

        this.request('DELETE', domain + path, parameters, body, headers, queryParameters, form, deferred);

        return deferred.promise;
    };

    return Test;
})();

exports.Test = Test;

从生成文件来看,跟一开始预期的目的差不多,生成了一个 class 类,对 swagger.json 文件进行了转换,在这个class 里封装了一些通用的方法,同时也对 fetch 进行了一些简单的封装,可以说是开箱即用了,但是结果看起来单个 api 还是有些臃肿,并且也不是非常通用,这个库的关键代码是转换 swagger.json 的部分,看一下源码, 源代码比较多,关键代码是这一段:


var getViewForSwagger1 = function(opts, type){
    var swagger = opts.swagger;
    var data = {
        isNode: type === 'node' || type === 'react',
        isES6: opts.isES6 || type === 'react',
        description: swagger.description,
        moduleName: opts.moduleName,
        className: opts.className,
        domain: swagger.basePath ? swagger.basePath : '',
        methods: []
    };
    swagger.apis.forEach(function(api){
        api.operations.forEach(function(op){
            if (op.method === 'OPTIONS') {
                return;
            }
            var method = {
                path: api.path,
                className: opts.className,
                methodName: op.nickname,
                method: op.method,
                isGET: op.method === 'GET',
                isPOST: op.method.toUpperCase() === 'POST',
                summary: op.summary,
                parameters: op.parameters,
                headers: []
            };

            if(op.produces) {
                var headers = [];
                headers.value = [];
                headers.name = 'Accept';
                headers.value.push(op.produces.map(function(value) { return '\'' + value + '\''; }).join(', '));
                method.headers.push(headers);
            }

            op.parameters = op.parameters ? op.parameters : [];
            op.parameters.forEach(function(parameter) {
                parameter.camelCaseName = _.camelCase(parameter.name);
                if(parameter.enum && parameter.enum.length === 1) {
                    parameter.isSingleton = true;
                    parameter.singleton = parameter.enum[0];
                }
                if(parameter.paramType === 'body'){
                    parameter.isBodyParameter = true;
                } else if(parameter.paramType === 'path'){
                    parameter.isPathParameter = true;
                } else if(parameter.paramType === 'query'){
                    if(parameter['x-name-pattern']){
                        parameter.isPatternType = true;
                        parameter.pattern = parameter['x-name-pattern'];
                    }
                    parameter.isQueryParameter = true;
                } else if(parameter.paramType === 'header'){
                    parameter.isHeaderParameter = true;
                } else if(parameter.paramType === 'form'){
                    parameter.isFormParameter = true;
                }
            });
            data.methods.push(method);
        });
    });
    return data;
};

对源文件进行简单修改,便可以达到使用目的,在此,对 swagger.json文件的提取和转换大致实现。

接下来就是模板文件了,在研究这个的时候,在 github 上发现了也引用这个库的一个工具库
generator-swagger-2-ts, 看了下源码,作者使用了 Yeoman generator 脚手架生成器工具,之前没使用过Yeoman,便借此去研究了下,发现功能非常强大,所以,本文的主角登场!

使用 Yeoman 自动化生成接口函数

简单介绍一下 Yeoman

Yeoman 是一种脚手架搭建系统,意在精简开发过程。用yeoman 写脚手架非常简单,yeoman 提供了 yeoman-generator 让我们快速生成一个脚手架模板。

接下来介绍 yeoman-generator 和如何编写自己的 generator

安装依赖

首先需要安装yo

npm install -g yo

官方们构建了一个generator-generator脚手架来帮助用户快速构建自己的generator, 安装后开箱即用,接下来主要介绍这个脚手架的使用。

npm install generator-generator -g

使用命令:

$ yo generator
? Your generator name generator-swagger-api-tool
? Description 
? Project homepage url 
? Author's Email *****@***.com
? Author's Homepage 
? Send coverage reports to coveralls Yes
? Enter Node versions (comma separated) 
? GitHub username or organization 
   create package.json
   create README.md
   create .editorconfig
   create .gitattributes
   create .gitignore
   create generators/app/index.js
   create generators/app/templates/dummyfile.txt
   create __tests__/app.js
   create .travis.yml
   create .eslintignore

生成package.json文件到创建文件目录,再到 npm install,最后初始化 git,可谓一气呵成!

分析文件目录:

├── README.md
├── __tests__
│   └── app.js
├── generators  // 生成器主目录
│   ├── app  // package.json 中files 必须为当前路径
│      ├── index.js // 入口文件,脚手架主要逻辑
│      └── templates // 模板文件夹
│          ├── dummyfile.txt
├── package-lock.json
└── package.json
编写和扩展脚手架
这是基本生成器的方式:
var Generator = require("yeoman-generator");

module.exports = class extends Generator {};
添加自己的功能

添加到原型的每种方法都将运行,并且通常是按顺序进行的。

module.exports = class extends Generator {
  method1() {
    this.log('method 1 just ran');
  }

  method2() {
    this.log('method 2 just ran');
  }
};
运行你的 Generator

接下来要测试运行当前 Generator,当前Generator是在本地开发,因此尚不能作为全局npm模块使用。可以使用npm创建一个全局模块并将其符号链接到本地​​模块。

命令行中,在generator根目录(在generator-name/文件夹中,通常是项目根目录)

npm link

这将项目依赖项和链接一个全局模块到本地。npm 下载完后,就可以使用yo name来运行你的
Generator了。

yeoman 的生命周期

1. initializing - 初始化方法 (检查当前项目的状态,配置等)
2. prompting - 用户提示选项 (在这你会使用 this.prompt())
3. configuring - 保存配置并配置项目 (创建 .editorconfig 文件和其他元数据文件)
4. default - 如果方法名称不匹配优先级,将被推到这个组。
5. writing - 这里是你写的 generator 特殊文件(路由,控制器,等)
6. conflicts - 处理冲突的地方 (内部使用)
7. install - 运行(npm, bower)安装相关依赖(没必要每次都执行安装)
8. end - 所谓的最后的清理,Generator结束

常用的生命周期:

- prompting
- writing
- install
prompting(互动)

提示是generator与用户交互的主要方式。

该prompt方法是异步的,并返回一个Promise。您需要从任务中返回Promise,以便在完成下一个任务之前等待其完成。

module.exports = class extends Generator {
  async prompting() {
    const answers = await this.prompt([
      {
        type: "input",
        name: "name",
        message: "Your project name",
        default: this.appname // 默认值
      }
    ]);

    this.log("app name", answers.name);
  }
};

记住用户偏好

对于每次运行时高频的相同输入,可以通过配置 store: true来记住偏好。

this.prompt({
  type: "input",
  name: "username",
  message: "What's your GitHub username",
  store: true
});

日志输出

命令行中的log输出需要使用 this.log()方法,与使用 console.log()类似。

writing(通过生成器生成项目结构)

Generators会暴露所有方法到 this.fs

例如使用copyTpl 方法通过模板文件生成目标文件。

class extends Generator {
  writing() {
    this.fs.copyTpl(
      this.templatePath('index.html'), // 模板所在路径
      this.destinationPath('public/index.html'), // 输出文件路径
      { title: 'Templating with Yeoman' } // 配置参数
    );
  }
}

以上是 对 yeoman generator 使用的简单介绍,更多详细文档请移步官网https://yeoman.io/


示例项目

自己构建的 demo,可以 clone 下来后根据自己项目需求稍加改动即可使用。

github: https://github.com/Wuguanghua...

引用

总结

在研究处理 swagger文档生成前端请求工具的时候,意外发现 yeoman这个强大的工具,本文也是对 yeoman 的第一次尝试,如果要自己编写一个脚手架的话可以按照官网的步骤进行。


guanghua
2.7k 声望295 粉丝

喜欢摄影、健身的一个程序猿。


引用和评论

0 条评论