前后端分离是目前主流的团队开发方式,有效的隔离不同技术领域开发人员的依赖,包括开发进度上的依赖和代码文件上的依赖。而做到这一点,全靠一个规则:依赖接口而不依赖实现。

那么,web领域前后端分离,通常都会选择基于http+json的方式,也就是大家说的restful类型的接口。在进入代码编写之前,团队会定义好所有依赖的rest接口,前后端开发人员遵照接口文档各自完成自己的工作,完美!

那么,在前后端开发进度不一致的情况下,可能会出现前端界面完成了但对应的后端服务还没有完成,怎么办?这个时候一般都是会让前端人员自己来提供一个模拟的响应数据,这种方案简单有效,瞬间前端人员就可以模拟出所需的服务接口响应。但是,问题在于,随着需求的变化,接口会发生变更,此时会影响前后端的代码,但往往会由于各种实际原因,之前的模拟接口响应的脚本并没有随着接口的变更而更新,对未来的测试留下了隐患~

怎么做最理想?几年前我在之前的团队推广过阿里的一个开源项目:RAP。虽然最终并没有大规模使用,但通过调研,RAP真心是个不错的解决方案,尤其是对国内的项目,本地化支持度非常高。该项目也一直在持续更新,如果有兴趣,我首先推荐尝试RAP。

不过这里,我们并不会采用RAP,理由可能有些牵强,仅仅是因为团队已经存在大量的接口文档在swagger标准上,并且组员也已经相当熟悉这个规范,他们不会接受突然切换到RAP上而带来的学习成本。

不过没关系,swagger作为rest文档界的事实标准,自然应该存在大量的模拟数据的解决方案,所以我花了几天的时间查阅了相关的资料(其实就是不停地google),找到了几种方案。

不过目前我能找到的,swagger下的对应解决方案都不像RAP那样开箱即用,多数都是需要自己完成组合才能投入使用的。这里我罗列一下相关的库:

最终,我决定基于 swagger-node + mockjs 来完成我们的根据swagger接口文档生成模拟数据的目的。之所以选择swagger-node,是因为它自身就容纳了很多方便的工具,例如:swagger editor:swagger.io在线版的编辑器,你可以直接跑在自己的服务器上,并且它是实时持久化到文件中且同步到mock server的。

安装和配置swagger-node非常的简单,只需要按照官方的步骤完成即可。装好后按照推荐的用法,我们需要先swagger project create一个项目出来,记得选择express作为服务端框架。swagger-node会按照预设为我们创建好一个标准规范的项目文件结构。

直接执行swagger project start就可以运行一个解析基于swagger接口标准规范文档并自动化创建对应服务的后端服务。该服务可以校验每次接口的请求,根据接口文档的要求来返回匹配的结果数据。

基本上完美,就是我们想要的。不过,美中不足的是,目前swagger-node根据接口文档返回的模拟数据是静态的,例如,整型只会返回1,字符串类型只会傻傻的返回Sample text。这显然无法满足我们的要求,唉,真可惜!

不过庆幸的是,我在该项目的issue中找到了一个很有价值的,提供了一个非常不错的解决方案。其实也很好理解,就是将原本swagger-node用来生成模拟数据的逻辑修改成符合我们要求的逻辑即可。同时该方案也让我关注到了mockjs项目,应该是个国人的项目,非常棒!其实国外也有不少同类型的项目,但说到本地化,还是我们自己国家的开发人员最贴心。并且看完文档后,发现它的扩展性也非常不错,简单直观,赞!

好的,一切就绪,开始改造吧!

先找到\你的项目\node_modules\swagger-express-mw\node_modules\swagger-node-runner\node_modules\swagger-tools,目录结构比较深~

swagger-tools提供了我们需要修改的中间件middleware\swagger-router.js。由于要使用到mockjs,所以需要将对应的文件放到lib文件夹下。

接下来就要修改代码了,使用你心爱的编辑器打开swagger-router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//找到getMockValue方法,大概在107行
var Mock = require('../lib/mock');
var getMockValue = function (version, schema) {
var type = _.isPlainObject(schema) ? schema.type : schema;
var xmock = _.isPlainObject(schema) ? schema['x-mock'] : schema;
var Random = Mock.Random;
var value;
var temp;
if(xmock){
var data = xmock.split("|");
type =data[0];
if(data.length>1)
temp = data[1];
}
if (!type) {
type = 'object';
}
switch (type) {
case 'guid':
value = Random.guid();break;
case 'id':
value = Random.id();break;
case 'step':
value = Random.increment(temp);break;
case 'title':
value = temp==='cn'?Random.ctitle():Random.title();break;
case 'text':
value = temp==='cn'?Random.cparagraph():Random.paragraph();break;
case 'color':
value = Random.hex();break;
case 'ip':
value = Random.ip();break;
case 'email':
value = Random.email(temp);break;
case 'firstname':
value = temp==='cn'?Random.cfirst():Random.first();break;
case 'lastname':
value = temp==='cn'?Random.clast():Random.last();break;
case 'cname':
value = Random.cname();break;
case 'name':
value = Random.name(temp === 'middle');break;
case 'url':
value = Random.url(temp);break;
case 'date':
value = Random.date(temp);break;
case 'time':
value = Random.time(temp);break;
case 'datetime':
value = Random.datetime(temp);break;
case 'array':
value = [];
for(var i = 0; i < temp && i < 20; i++){
value.push(getMockValue(version, _.isArray(schema.items) ? schema.items[0] : schema.items));
}
break;
case 'boolean':
if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
value = schema.defaultValue;
} else if (version === '2.0' && !_.isUndefined(schema.default)) {
value = schema.default;
} else if (_.isArray(schema.enum)) {
value = schema.enum[0];
} else {
value = Random.boolean(5, 5, true)||1;;
}
// Convert value if necessary
value = value === 'true' || value === true ? true : false;
break;
case 'file':
case 'File':
value = Random.image();break;
case 'integer':
if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
value = schema.defaultValue;
} else if (version === '2.0' && !_.isUndefined(schema.default)) {
value = schema.default;
} else if (_.isArray(schema.enum)) {
value = schema.enum[0];
} else {
value = Random.integer(1,1000)||1;
}
// Convert value if necessary
if (!_.isNumber(value)) {
value = parseInt(value, 10);
}
//TODO: Handle constraints and formats
break;
case 'object':
value = {};
_.each(schema.allOf, function (parentSchema) {
_.each(parentSchema.properties, function (property, propName) {
value[propName] = getMockValue(version, property);
});
});
_.each(schema.properties, function (property, propName) {
value[propName] = getMockValue(version, property);
});
break;
case 'number':
if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
value = schema.defaultValue;
} else if (version === '2.0' && !_.isUndefined(schema.default)) {
value = schema.default;
} else if (_.isArray(schema.enum)) {
value = schema.enum[0];
} else {
value = Random.float(0, 100, 0, 4);
}
// Convert value if necessary
if (!_.isNumber(value)) {
value = parseFloat(value);
}
// TODO: Handle constraints and formats
break;
case 'string':
if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
value = schema.defaultValue;
} else if (version === '2.0' && !_.isUndefined(schema.default)) {
value = schema.default;
} else if (_.isArray(schema.enum)) {
value = schema.enum[0];
} else {
if (schema.format === 'date') {
value = new Date().toISOString().split('T')[0];
} else if (schema.format === 'date-time') {
value = new Date().toISOString();
} else {
value = Random.string(0, 61);
}
}
break;
}
return value;
};

放心保存吧~~

接下来,我们在当前项目下直接运行swagger project start,哇哈,后台服务应该已经跑起来了。swagger-node会监控我们的接口定义文件(在你的项目\api\swagger下),每次变更,都会被实时映射到mock服务上。

上面的代码并不是非常难理解,我只是将mockjs官方提供的数据类型都简单的绑定到了swagger生成模拟数据的规则上。

使用起来也非常简单,特殊类型只需要增加对应的x-mock声明即可,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
swagger: "2.0"
info:
version: "0.0.3"
title: Hello World App
# during dev, should point to your local machine
host: localhost:10010
# basePath prefixes all resource paths
basePath: /
#
schemes:
# tip: remove http to make production-grade
- http
- https
# format of bodies a client can send (Content-Type)
consumes:
- application/json
# format of the responses to the client (Accepts)
produces:
- application/json
paths:
/hello:
# binds a127 app logic to a route
x-swagger-router-controller: hello_world
get:
description: Returns 'Hello' to the caller
# used as the method name of the controller
operationId: hello
parameters:
- name: name
in: query
description: The name of the person to whom to say hello
required: false
type: string
responses:
"200":
description: Success
schema:
type: object
required:
- total
- users
properties:
total:
description: 用户总数
type: integer
users:
description: 用户列表数据
type: array
x-mock: array|5
items:
$ref: "#/definitions/HelloWorldResponse"
# responses may fall through to errors
default:
description: Error
schema:
$ref: "#/definitions/ErrorResponse"
/swagger:
x-swagger-pipe: swagger_raw
# complex objects have schema definitions
definitions:
HelloWorldResponse:
required:
- id
- name
- message
properties:
id:
type: integer
x-mock: step|1
name:
type: string
x-mock: title|cn
message:
type: string
ErrorResponse:
required:
- message
properties:
message:
type: string

完工!

警告

这种结合mockjs和swagger-node的方法并不是很优雅,毕竟需要直接修改项目的依赖库。这会导致 每次创建新项目可能都会丢失之前的修改。不过我并没有找到swagger-tools这个项目的扩展方式,至少官方没有提供类似的hook供我们来使用。(逼急了哥,直接弄个docker镜像给你看 :-))

补充: swagger-node目前只支持单个swagger.yaml,并不支持多个独立的接口文件。只能要求我们在使用的时候将多个文件合并到一起,或者搭建多个项目,前面放一个nginx做反向代理。