目前不少後臺的接口文檔工具都使用了 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
的第一次嘗試,若是要本身編寫一個腳手架的話能夠按照官網的步驟進行。