目前不少后台的接口文档工具都使用了 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" ] } ] } }
能够发现,咱们能够获得 paths
、 method
、parameters
(包括每一个参数的类型且是否必传)、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
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-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
是在本地开发,所以尚不能做为全局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
提示是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()
相似。
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
的第一次尝试,若是要本身编写一个脚手架的话能够按照官网的步骤进行。