目前很多后台的接口文档工具都使用了 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" ] } ] } }
可以发现,我们可以得到 paths
、 method
、 parameters
(包括每个参数的类型且是否必传)、 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。大概就是可以通过前期接口定义文档生成具体的服务端代码,看样子是对服务端的同学帮助比较大的一个工具。
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-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
了。
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' } // 配置参数 ); } }
自己构建的 demo,可以 clone 下来后根据自己项目需求稍加改动即可使用。
github: https://github.com/Wuguanghua...
在研究处理 swagger
文档生成前端请求工具的时候,意外发现 yeoman
这个强大的工具,本文也是对 yeoman
的第一次尝试,如果要自己编写一个脚手架的话可以按照官网的步骤进行。