藉助yeoman 根據 swagger 接口文檔生成請求函數

yeoman_swagger.jpg

前言

目前不少後臺的接口文檔工具都使用了 swagger來完成,開發過程當中,爲了減小先後端的沒必要要溝通,接口文檔一般會寫的比較詳細,分類也會比較明確,在 editor.swagger.io (swagger 在線編輯器)中看到接口文檔時,就想,爲什麼不把這些文檔處理一下,轉換成咱們前端能夠直接調用的工具呢?javascript

本文會簡單介紹如何處理轉換` swagger`文檔,並藉助` yeoman` 開箱即用的` yeoman-generator` 腳手架自動化生成前端須要的接口請求函數。

正文

首先去研究一下 swagger 文檔的數據結構,看看是否是可以對部分信息進行提取和轉換來生成咱們前端可使用的工具,發現 editor.swagger.io 中能夠直接導出 swagger.json 文件,每個接口包含豐富的信息,部分以下:html

"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(成功和失敗的返回值數據結構)。前端

發現可行性真的是很是高,因此開始研究怎樣實施。java

目的

指望結果:node

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。大概就是能夠經過前期接口定義文檔生成具體的服務端代碼,看樣子是對服務端的同窗幫助比較大的一個工具。react

image.png

github: swagger-codegengit

在這個庫中又發現了一個 JavaScript 生成庫,swagger-js-codegen
(A Swagger Codegen for typescript, nodejs & angularjs)angularjs

他能夠生成 JavaScript / TypeScript 的 api 庫,因爲咱們項目中目前使用的是 TypeScript ,碰巧這裏也有對TypeScript的實現。github

在此推薦使用 TypeScript的實現,由於 ts 對 params 的定義更加詳細和規範,對於 params 比較多的 api 能夠將 params 的類型定義提取出來,且能夠複用。

這個包從一個 swagger file 中生成一個nodejs,reactjs或angularjs類。代碼使用mustache templates生成,能夠自定義類名,並由jshint進行質量檢查,並由js-beautify進行美化,聽起來不錯。typescript

可是該項目再也不由其建立者積極維護,大概看了一下項目代碼;

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

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 的第一次嘗試,若是要本身編寫一個腳手架的話能夠按照官網的步驟進行。

相關文章
相關標籤/搜索