我们都知道写代码要写注释是常识。而写Api程序注释是不够的,因为使用你Api的人看不到源代码。为了让使用你Api的人可以调用,我们还必须为每一个Api写调用说明文档,甚至还必须给出使用示例。这为写Api的程序员多了不少的工作量,并且许多程序员写代码会比较认真,写文档会因为被认为是不那么重要的工作比较马虎,这也导致文档质量参差不齐。另外Api是会变的,每一次变动还需要重新修改使用说明文档,这也会照成错漏,导致文档和实际情况不一致。文档一大堆,时间一长也不知道文档都放哪里了。一个项目半年之后出现找不到说明文档的情况非常正常。如果项目后面有人事调动,新人还需要和旧人进行文档方面的交接。。。文档是许多程序员的痛,在行业内一直都没有很好的解决,直到出现了Swagger。
Swagger是一套基于OpenAPI规范构建的开源工具,它是一种规范,你只需要按照它的规范去定义接口及接口相关的信息。再通过Swagger衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,以及在线接口调试页面等等。在开发新版本或者迭代版本的时候,只需要更新Swagger描述文件,就可以再次自动生成接口文档,做到调用端代码、服务端代码以及接口文档的一致性。
为了能更感性直观的理解Swagger,请大家前往https://editor.swagger.io/
归纳一下,大约有以下几点
如果你是前端程序员,看到这样的文档是不会也会感觉心里更有底了。( ̄▽ ̄)~*
使用Swagger看起来很美好,但是如果Json还是需要自己来写,那就是另外一种痛苦了。有许多聪明的懒人也已经注意到这个,各种各样的可以自动生成Swagger Json的工具应运而生,并且其中还有专门为Sails设计的开源库。Let’s go!
npm install sails-hook-swagger-generator --save
安装完这个库之后,还有一个动作一定不能忽略,就是要在根目录下创建一个swagger文件夹,因为该库默认生成的Json是放在这个文件夹的,如果没有这个文件夹,生成的时候就会产生错误。如果已经做好了准备,启动我们的系统吧,运行:
node app.js
Boom! 在swagger里面多出了一个名为swagger.json的文件,如果把swagger.json的内容copy到https://editor.swagger.io/ 网站上左边的编辑器里面,替代原来的内容。我们将会在右边看到我们自己的Api文档。(大家可以动手试试)
虽然我们可以看到生成的api文档了,但是怎么生成的我们还是需要进行配置的。在config文件夹里面添加swaggergenerator.js,然后在config/swaggergenerator.js写类似如下代码:
module.exports['swagger-generator'] = {
disabled: false,//如果设置为true,就表示不生成,这个选项是我们开发完成投入生成的时候需要的
swaggerJsonPath: './swagger/swagger.json',//这是生成json文件的位置,后面我可以进行修改
swagger: {
openapi: '3.0.0',
info: {
title: 'Api文档',
description: 'AdminSystem 后端Api说明及测试用例',
termsOfService: 'https://github.com/orgs/PassionOrganization/teams/codeteam',
contact: {name: 'JimmyTsai', url: 'https://github.com/JimmyTsai75', email: '[email protected]'},
license: false,//有不需要显示的选项,可以设置为false
version: '1.0.0'
},
servers: [
{ url: 'http://localhost:1898/' }
],
externalDocs: {description:'点击查看更详细说明',url: "https://passionorganization.gitbook.io/sailsjs-cong-ling-kai-shi/"},
},
defaults: {
responses: {
'200': { description: 'The requested resource' },
'404': { description: 'Resource not found' },
'500': { description: 'Internal server error' }
}
},
includeRoute: function(routeInfo) { return true; },//这个是过滤函数,可以根据需要设置
updateBlueprintActionTemplates: false,//可以通过这个函数设置sails蓝图模版
postProcess: function(specifications) { }//可以在函数中拦截输出,并修改输出内容
};
上面的这段代码是对Swagger generator进行配置,具体用请看注释。
根据配置生成的内容是swagger.json,这是个约定格式文档,大家可以打开生成的内容看看,这个地方还需要解释一下的有三个地方:
这些看起来有点复杂,大致了解一下就可以,后面的具体操作可以让我们逐渐的理解更多细节,多做几个就完全明白了。
到目前为止,我们已经可以自动产生api的描述文档swagger.json,并且如果我们把内容粘贴到https://editor.swagger.io/ 这个网站上面,我们还可以看到展示效果,但是这依然不够,我们需要在我们自己的后端上面就能够展示。那我们就需要把整个https://editor.swagger.io/ 网站搬到我们的sails后端系统上,幸运的是SwaggerUI是开源的,我们可以直接下载下来
下载SwaggerUI :这个开源库的网址如下:https://github.com/swagger-api/swagger-ui 我们不打算修改他,只需要download它的zip文档就够了
Copy:下载并解压之后,源代码根目录下面有一个dist文件夹,我们只需要把这个文件夹复制到我们sails项目根目录下assets里面就可以了。现在我们已经拥有了SwaggerUI,在浏览器中输入:http://localhost:1898/dist/#/,我们可以看到如下:
快好了,我们还差一步,因为现在SwaggerUI不知道我们的swagger.json文档在哪里。打开assets/dist/swagger-initializer.js
修改SwaggerUIBundle.url指向网站根目录…/swagger.json,代码如下:
//assets/dist/swagger-initializer.js
window.onload = function() {
window.ui = SwaggerUIBundle({
url: "../swagger.json",//**修改这个地方为上一级目录的swagger.json**
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
//
};
|-- assets
| |-- dist
| |-- favicon.ico
| |-- dependencies
| | |-- .gitkeep
| | |-- sails.io.js
| |-- images
| |-- swagger.json //这个就是我们复制过来的
.......
记得保存一下assets/dist/swagger-initializer.js,刷新浏览器应该就看到了
module.exports['swagger-generator'] = {
disabled: false,
swaggerJsonPath: 'assets/swagger.json',//这个地方直接改成我们希望的目的地
....
有时候我们修改swagger的生成规则(config/swaggergenerator.js)之后,保存,重启服务并且刷新浏览器,swaggerUI里面的内容没有及时变成我们要的,这个时候可以到.tmp/public 里面把swagger.json删除掉再重试
再重复一下,现代软件工程有一个非常好的理念叫“约定高于配置”(convention over configuration),在软件工程里面这是一种非常简约的设计思想,sails许多地方都体现了这点。
这种复制是通过grunt这个库来实现的,如果需要修改,可以通过tasks/config/copy.js来实现。
另外dist文件夹名称和我们的功能不是很搭,建议修改文件夹名称为apiDoc
如果按照上面的操作成功了,我们应该是可以看到许多我们的Api说明的,但是很奇怪的是,多出来许多我们没有写的路由,如下图:
我的config/routes.js里面是这样的:里面没有’GET /user’,‘PATCH /user/{id}’ 等等
POST /api/login/account': { action: 'users/check' },
'GET /api/currentUser': { action: 'users/curUser' },
'GET /api/notices': { action: 'users/notices' },
'POST /api/login/outLogin': { action: 'users/logout' },
'GET /api/crudDemo': { action: 'Amis/crudDemo' },
'post /api/crudnew': { action: 'Amis/crudNew' },
'delete /api/crudDelete': { action: 'Amis/crudDelete' },
....
但是swagger generator 帮我们找出来了,这是怎么回事?
在sails里面只要创建了model,blueprint 实际上已经帮你实现了表的增删改查了,根本不需要写任何增删改查的代码,也不需要在config/routes.js里面添加任意的控制器,这些代码已经做好。我们以我们做好的user数据模型为例,我们在config/routes.js里面添加的路由都是以api开头的,但是如果我们打开postman软件,输入http://localhost:1898/user,我们会发现,Wow,竟然是可以查询到所有用户数据的:
Sails 旨在减少代码量以及启动和运行功能性应用程序所需的时间。 蓝图是 Sails根据您的应用程序设计快速生成 API路由和操作的方式。默认情况下,它会自动完成Find,Create,Update,Destroy,Add几个操作。
Blueprint API是一个很sweet的设计,但是我不是很建议使用,有几个原因
所以,如果我们要设计一个安全性要求不高的,我们可以直接采用blueprint或是混合开发,而我们对系统安全有一定要求的,那我们还是要控制blueprint的使用,甚至是关闭它。
如果需要对blueprint进行配置,我们可以进入config/blueprints.js里面进行设置。我们试着把它的所有功能关闭掉,可以这样设置:
//config/blueprints.js
module.exports.blueprints = {
actions: false,
rest: false,
shortcuts: false,
autoWatch:false,
};
以上配置更加具体的说明见:https://sailsjs.com/documentation/concepts/blueprints
配置后大家可以在postman里面再次测试(需要保存后重新启动服务),如果成功,应该可以看到postman里面显示Not Found,并且我们的swaggerUI里面多出来的路由也没有了。
swaggerUI 里面的自动变化前提是你有修改swagger.json的生成路径(swaggerJsonPath)
关闭blueprint的操作会受到sails的Environment(环境)变量设置的影响。Env是Nodejs程序开发经常使用的做法。目的是为了实现开发周期内(development)和开发完成的运营周期(production)可以有不同的配置,比如是否跨域,stockets配置等。在sails里面,开发环境可以通过启动参数或是config/local.js来配置,这个文件同时也可以配置端口号等其它比较敏感的内容,所以一般是会在gitignore里面设置为不上传到git服务器的。(我们如果是私有库并且可信任,这个文件也是可以上传的)。
如果我们在local.js里面把环境变量设置为production如下:
//config/local.js
module.exports = {
port:1898,
environment: 'production'
};
这个时候,sails会加载config/env/production.js并且覆盖config里面的设置,这种情况,我们要关闭blueprint api的话,也需要同时修改config/env/production.js对应设置。
就算屏蔽掉了blueprint的“影子路由”我们也依然有需要对其它路由进行过来,避免我们的Api文档多维护没有使用到的路由,那我们就需要使用路由过滤函数。这个函数在config/swaggergenerator.js
//config/swaggergenerator.js
module.exports['swagger-generator'] = {
disabled: false,
swaggerJsonPath: 'assets/swagger.json',
...
includeRoute: (routeInfo) => {
if (routeInfo.isShortcutBlueprintRoute == true) return false;
if (routeInfo.verb == 'put' || routeInfo.verb == 'patch') return false;
if (routeInfo.action == 'security/grant-csrf-token') return false;
if (!routeInfo.path.startsWith('/api/')) return false;
return true;
},
比如我们通过上面的过滤函数,过滤掉所有不是api开头的路由
另外,我们还可以对一些默认的response进行配置:
defaults: {
responses: {
'400': { description: '验证错误' },
'403': { description: '没有权限访问' },
'401': { description: '认证失败' },
'200': { description: '请求成功' },
'404': { description: '找不到请求,请检查url和method是否正确' },
'500': { description: '服务器内部错误' }
},
},
目前,我们看到的一些路由是类似这样的:
尽管里面许多信息还是空白的,但是try it out功能按钮是没有问题的,并且也真的可以做一些类似postman的测试了(有了Swagger,并不表示就不用postman了,它有它的不可替代),当然我们还需要进一步把Api里面具体Action的详细使用描述得更加清楚一些的。这些我们可以直接在swagger.json里面手动填写,但这不是个好主意,因为我们的Api在开发过程是不断变化的,某个action一开始时实现的功能和最终实现的功能可能相差十万八千里。
我们需要像写代码注释一样的方式,把这些描述就写在代码里面,如果有变动也应该在源代码里面变动,这样我们日后就可以和管理源代码一起管理这些Api文档了。甚至它们还应该一起被push到我们的git仓库里面去的。
有两个选项,一个是写在每个controller.ts源代码里面,一个是写在config/routes.js 我更倾向于后者因为它更集中,管理起来更加方便一些。
'POST /api/userCreate': {
swagger: {
summary: '创建用户',
description: `包含email,password等信息;post body里面包含email,其中email格式由前端验证,后端只负责检查是否非空。email
字段具备唯一性,添加的email如果有重复系统将返回500错误。`,
tags: ['用户管理'],//放到用户管理这个tag标签里面
//post的时候body里面的内容
requestBody: {
content: {
'application/json': {
schema: {
properties: {
email: { type: 'string' },
password: { type: 'string' },
nickname: { type: 'string' }
},
required: ['email', 'password'],
}
}
}
},
//参数
parameters:[
{in:'header', name:'justTest',required:false,schema:{type:'string',example:'what '}}
],
//对响应的400代码做解析
responses: {
'400': { description: '验证错误' },
},
//关闭扩展文档显示
externalDocs: false,
},
// action不变
action: 'User/create'
},
还是有一点小小的不舒服:
做法也不难:
一、在根目录下创建swagger,增加UserSwagger.ts文件,import sails-hook-swagger-generator里面对应的interfaces,把原本写在routes.js里面的swagger相关代码移过来:
//swagger/UserSwagger.ts
//导入类型支持
import { SwaggerActionAttribute } from "sails-hook-swagger-generator/lib/interfaces"
//用户控制器所在Tag
const Tag1 = "用户管理";
//移植原来的代码
const userCreate:SwaggerActionAttribute={
summary: '创建用户',
description: `包含email,password等信息;post body里面包含email,其中email格式由前端验证,后端只负责检查是否非空。email
字段具备唯一性,添加的email如果有重复系统将返回500错误。`,
tags: [Tag1],
requestBody: {
content: {
'application/json': {
schema: {
properties: {
email: { type: 'string' },
password: { type: 'string' },
nickname: { type: 'string' }
},
required: ['email', 'password'],
}
}
}
},
parameters:[
{in:'header', name:'justTest',required:false,schema:{type:'string',example:'what '}}
],
responses: {
'400': { description: '验证错误' },
},
externalDocs: false,
};
export { userCreate};
二、修改config/routes.js
import { userCreaten } from 'swagger/UserSwagger';
'POST /api/userCreate': {swagger: userCreate,action: 'User/create'},
三、增加别名
在package.json里面增加路径别名如下:
"_moduleAliases": {
"utils": "./utils",
"swagger":"./swagger"
},
cool,现在的代码看起来清爽多了。
因为routes.js里面我们采用’swagger/UserSwagger’,如果没有别名,这个地方要改成import { userCreaten } from ‘…/swagger/UserSwagger’;但是这种相对路径的写法容易错,采用路径别名会舒服一些。路径别名请参考本系列文章之《二、在Sails中使用Typescript》
还剩最后一个问题了,我们上一篇讲的Jwt认证之后,我们有一些控制器的动作是需要携带token的,遇到这种情况,Swagger上面的try it out功能就会受挫,因为我们没有可以让测试人员输入token的地方。
swagger.json上面是可以有安全选项的,在components这个节点里面,除了我们前面说的schemas和parameters之外,还可以添加一个securitySchemes项。具体的做法是在swagger.json创建之前做一个拦截函数,幸运的是config/swaggergenerator.js里面是有提供这样的hook的,我们通过修改postProcess函数就可以实现,做法和路由拦截类似:
//config/swaggergenerator.js
module.exports['swagger-generator'] = {
....
postProcess: function (specifications) {
let sch = specifications.components;
//在Top-Level Swagger Defintions中添加身份认证项Authorize
sch.securitySchemes = {
APIKeyHeader: {
type: "apiKey",
in: "header",
name: "Authorization",
description: "Bearer xxxx"
}
}
//在Top-Level Swagger 中添加安全选项,如果在顶级属性中添加安全项,则每一个path上都可以执行Authorize
//如果此处没有添加,可以在每个action中的swagger里面添加security,有添加的path才能进行身份认证
//大小写敏感
specifications.security = [
{
APIKeyHeader: []
}
];
},
.....
}
在将最终生成的Swagger写入swagger.json之前,可以使用postProcess函数进行拦截
注意APIKeyHeader 这个名称,在swagger.json里面是大小写敏感的,这是一个小坑。
成功之后,刷新swaggerUI,可见如下:可以在try it out 的之前,先做身份认证:在弹窗的value里面输入Bearer xxxx… (登录后获取的token),并且我们可看到每个path后面都带一个锁的图标,如果需要修改token,可以点击任意一个path上lock图标实现再次输入token
可以尝试在api/userRetrieve这个path上面使用try it out 功能,没有Authorization的时候会显示“您没有权限访问本页面",Authorizations之后就有可以正常查询了