与本文相关的【视频链接】
文章目录
一、also-json-server 的初衷
在我的早期开发生涯或教学中,如果需要进行消费 Restful API 的功能测试,往往需要自行编写一个能够提供相应 API 的后台程序。后来,大约在 2019 年,我发现了一个 github 项目,叫做 json-server。它可以让测试人员编写一个 json 作为后端数据文件,然后根据数据生成典型的 Restful API。
在使用这个项目的过程中,我感觉到该项目缺乏:
- 一些典型的需要被测试的功能
- 服务器的定制性
- 易用性
为了达到自己的使用要求,我 Fork 了该项目进行修改。为了表达对原作者的尊重,我将项目取名为 also-json-server。
针对以上三项缺失,我做了以下功能的添加:
- 添加了多对多关系数据的处理(功能性)
- 添加了身份验证的功能(功能性)
- 服务器可定制:端口、URL、返回结果形式、响应延迟时间等(定制性)
- 添加了自动生成样例数据文件,便于用户进行测试和模拟(易用性)
二、also-json-server 的安装和使用
2.1 安装
使用 npm 进行全局安装。
npm install -g also-json-server
2.2 使用
2.2.1 命令行格式
在命令行中通过以下命令使用:
also-json-server [选项] <json数据文件>
2.2.2 选项说明
- -p, --port <端口号>:指定服务器监听端口号(默认:3000)。
- -h, --host <主机地址>:指定服务器监听地址(默认:localhost)。
- -s, --static <路径>:指定静态文件目录(可指定多项),public 目录(如果存在的话)会自动包含到静态文件目录中。
- -a, --auth:使用用户验证进行访问。
- -P, --path :指定主机地址后的URL,如:/api/v1。
- -o, --return-object:指定将结果数据包装到一个 data 对象,并包含状态码和消息。
- -t, --try-server:在当前文件夹生成一个数据文件(also-json-server-test-db.json5)以试用服务器。
- -d, --delay <auto|ms>:延迟响应一定时间以模拟网络延迟。auto 针对每一次请求产生300~1000毫秒的随机延迟毫秒数,或直接指定一个数值以指定固定的延迟毫秒数。
- –help:显示帮助信息。
- –version:显示版本信息。
三、also-json-server 的数据格式
我们将以 also-json-server 自动携带的样例数据进行讲解。
3.1 获取样例数据
当指定 -t 选项运行 also-json-server 时,会在当前目录下生成一个叫做 also-json-server-test-db.json5 的文件。它是 json5 格式的文件,这种文件格式的说明请查看 github上 的 json5 项目。
3.2 样例数据内容
以下是该文件的内容:
{
users: [
{
id: 1,
name: "Jhon Doe",
email: "john@email.com",
password: "JohnPass"
},
{
id: 2,
name: "Jane Doe",
email: "jane@email.com",
password: "JanePass"
}
],
posts: [
{
id: 1,
title: 'a title',
views: 100,
},
{
id: 2,
title: 'another title',
views: 200,
},
],
comments: [
{
id: 1,
text: 'a comment about post 1',
postId: 1,
},
{
id: 2,
text: 'another comment about post 1',
postId: 1,
},
],
contacts: [
{
id: 1,
name: 'Tracy',
mobile: '(555)1234-1256',
groups: [
1,
2,
],
},
{
id: 2,
name: 'Tina',
mobile: '(555)2367-1287',
groups: [
1,
3,
],
},
{
id: 3,
name: 'Bill',
mobile: '(555)2589-1134',
groups: [
1,
2,
3,
],
},
{
id: 4,
name: 'Michael',
mobile: '(555)3345-2345',
groups: [],
},
{
id: 5,
name: 'Jackie',
mobile: '(555)1123-1123',
groups: [],
},
],
groups: [
{
id: 1,
name: 'Collegue',
},
{
id: 2,
name: 'Friend',
},
{
id: 3,
name: 'Family',
},
{
id: 4,
name: 'Business',
},
],
profile: {
name: 'typicode',
},
}
可见其中可包含对象数据(profile),集合数据(users, posts, comments, contacts, groups)。
3.3 数据中的关系
3.3.1 一对多关系
posts(文章)和 comments(评论)是一对多的关系:comments 数据中的 postId(外键) 会自动关联到 posts 的 id(主键)。
注意这里的命名约定(conventions):xxxId 会自动关联到一个叫做 xxx(改为英语复数)的集合的主键(id)。如:guestId → guests(id),staffId → staff(id) 等。
3.3.2 多对多关系
contacts(联系人)和 groups(群组)是多对多的关系:通过在记录较多的一端(contacts)嵌入一个数组属性(与另一端数据集合同名:groups)来实现。
在多对多关系中,通常一端的记录数在量级上要远大于另一端。如“群组”与“联系人(数据量级较另一端大)”,“俱乐部”与“会员(数据量级较另一端大)”等。为了便于编写数据,建议在“数据量较大的一端嵌入数据量较小的一端”。
四、also-json-server 的数据获取
4.1 无条件获取
API 格式:
GET <url>/<collection>
GET <url>/<collection>/:id
4.1.1 返回原生数据
# 启动服务器
also-json-server -P /api/v1 -t
# 获取集合
curl -X GET http://localhost:3000/api/v1/posts
[
{
"id": 1,
"title": "a title",
"views": 100
},
{
"id": 2,
"title": "another title",
"views": 200
}
]
# 获取集合中的单个对象
curl -X GET http://localhost:3000/api/v1/posts/1
{
"id": 1,
"title": "a title",
"views": 100
}
# 获取对象记录
curl -X GET http://localhost:3000/api/v1/profile
{
"name": "typicode"
}
4.1.2 返回对象数据
# 启动服务器(返回数据对象)
also-json-server -P /api/v1 -ot
# 获取集合
curl -X GET http://localhost:3000/api/v1/posts
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 1,
"title": "a title",
"views": 100
},
{
"id": 2,
"title": "another title",
"views": 200
}
]
}
# 获取集合中的单个对象
curl -X GET http://localhost:3000/api/v1/posts/1
{
"statusCode": 200,
"message": "Found",
"data": {
"id": 1,
"title": "a title",
"views": 100
}
}
# 获取对象记录
curl -X GET http://localhost:3000/api/v1/profile
{
"statusCode": 200,
"message": "Success",
"data": {
"name": "typicode"
}
}
4.2 有条件获取
# 启动服务器(返回数据对象)
also-json-server -P /api/v1 -ot
4.2.1 等于
参数后缀:无。
查找阅读量等于 200 的文章:
curl -X GET "http://localhost:3000/api/v1/posts?views=200"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 2,
"title": "another title",
"views": 200
}
]
}
4.2.2 不等于
参数后缀:_ne(not equal)
查找名称不等于 Family 的群组:
curl -X GET "http://localhost:3000/api/v1/groups?name_ne=Family"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 1,
"name": "Collegue"
},
{
"id": 2,
"name": "Friend"
},
{
"id": 4,
"name": "Business"
}
]
}
4.2.3 小于/小于等于
小于参数后缀:_lt(less than)
小于等于参数后缀:_lte(less than or equal)
查找阅读量小于等于 200 的文章:
curl -X GET "http://localhost:3000/api/v1/posts?views_lte=200"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 1,
"title": "a title",
"views": 100
},
{
"id": 2,
"title": "another title",
"views": 200
}
]
}
4.2.4 大于/大于等于
大于参数后缀:_gt(greater than)
大于等于参数后缀:_gte(greater than or equal)
查找阅读量大于等于 100 的文章:
curl -X GET "http://localhost:3000/api/v1/posts?views_gte=100"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 1,
"title": "a title",
"views": 100
},
{
"id": 2,
"title": "another title",
"views": 200
}
]
}
4.2.5 字符串模糊匹配(like)
参数后缀:_like
取值举例:
- ‘some’ - 包含 some
- ‘some*’ - 以 some 开头
- ‘*some’ - 以 some 结尾
# 文章标题包含 wherter
GET /posts?title_like='wheater'
# 文章标题以 wheater 开头
GET /posts?title_like='wheater*'
# 文章标题以 wheater 结尾
GET /posts?title_like='*wheater'
4.3 获取数据范围
查询参数:
_start:记录的开始位置(此位置的记录不包含)
_end:记录结束的位置(此位置的记录被包含)
_limit:限制返回的记录数
即查询范围为:(_start, _end] 区间的记录。
例1:
curl -X GET "http://localhost:3000/api/v1/contacts?_start=3&_end=5"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 4,
"name": "Michael",
"mobile": "(555)3345-2345",
"groups": []
},
{
"id": 5,
"name": "Jackie",
"mobile": "(555)1123-1123",
"groups": []
}
]
}
例2:
curl -X GET "http://localhost:3000/api/v1/contacts?_start=3&_limit=1"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 4,
"name": "Michael",
"mobile": "(555)3345-2345",
"groups": []
}
]
}
4.4 分页
查询参数:
_per_page: 每页记录数
_page:当前页
举例:分页获取 contacts
# 每页3条记录,获取第2页
curl -X GET "http://localhost:3000/api/v1/contacts?_page=2&_per_page=3"
{
"statusCode": 200,
"message": "Success",
"first": 1,
"first_url": "/api/v1/contacts?_page=1&_per_page=3",
"prev": 1,
"prev_url": "/api/v1/contacts?_page=1&_per_page=3",
"current": 2,
"next": null,
"next_url": null
"last": 2,
"last_url": "/api/v1/contacts?_page=2&_per_page=3",
"pages": 2,
"items": 5,
"data": [
{
"id": 4,
"name": "Michael",
"mobile": "(555)3345-2345",
"groups": []
},
{
"id": 5,
"name": "Jackie",
"mobile": "(555)1123-1123",
"groups": []
}
]
}
4.5 排序
查询参数:
_sort=field1,-field2
支持多个排序字段(逗号分隔),字段名前加 - 号表示降序。
# 按 id 降序排序
curl -X GET "http://localhost:3000/api/v1/contacts?_sort=-id"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 5,
"name": "Jackie",
"mobile": "(555)1123-1123",
"groups": []
},
{
"id": 4,
"name": "Michael",
"mobile": "(555)3345-2345",
"groups": []
},
{
"id": 3,
"name": "Bill",
"mobile": "(555)2589-1134",
"groups": [
1,
2,
3
]
},
{
"id": 2,
"name": "Tina",
"mobile": "(555)2367-1287",
"groups": [
1,
3
]
},
{
"id": 1,
"name": "Tracy",
"mobile": "(555)1234-1256",
"groups": [
1,
2
]
}
]
}
4.6 多级属性和数组属性查询
{
"companies": [
{
"id": 1,
"name": "New Company",
"address": {
"city": "City1",
"state": "Some State"
},
"products": [
"Computer",
"Printer"
]
},
...
]
}
针对以上数据,可进行形如以下的查询:
GET /companies?address.city=City1&products[0]=Computer
4.7 关系查询
4.7.1 一对多关系
a) 嵌入多方
curl -X GET "http://localhost:3000/api/v1/posts?_embed=comments"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 1,
"title": "a title",
"views": 100,
"comments": [
{
"id": 1,
"text": "a comment about post 1",
"postId": 1
},
{
"id": 2,
"text": "another comment about post 1",
"postId": 1
}
]
},
{
"id": 2,
"title": "another title",
"views": 200,
"comments": []
}
]
}
curl -X GET "http://localhost:3000/api/v1/posts/1?_embed=comments"
{
"statusCode": 200,
"message": "Found",
"data": {
"id": 1,
"title": "a title",
"views": 100,
"comments": [
{
"id": 1,
"text": "a comment about post 1",
"postId": 1
},
{
"id": 2,
"text": "another comment about post 1",
"postId": 1
}
]
}
}
b) 嵌入一方
curl -X GET "http://localhost:3000/api/v1/comments?_embed=post"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 1,
"text": "a comment about post 1",
"postId": 1,
"post": {
"id": 1,
"title": "a title",
"views": 100
}
},
{
"id": 2,
"text": "another comment about post 1",
"postId": 1,
"post": {
"id": 1,
"title": "a title",
"views": 100
}
}
]
}
curl -X GET "http://localhost:3000/api/v1/comments/1?_embed=post"
{
"statusCode": 200,
"message": "Found",
"data": {
"id": 1,
"text": "a comment about post 1",
"postId": 1,
"post": {
"id": 1,
"title": "a title",
"views": 100
}
}
}
4.7.2 多对多关系
# 获取所有联系人,每个联系人中嵌入其所在群组
curl -X GET "http://localhost:3000/api/v1/contacts?_embed=groups"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 1,
"name": "Tracy",
"mobile": "(555)1234-1256",
"groups": [
{
"id": 1,
"name": "Collegue"
},
{
"id": 2,
"name": "Friend"
}
]
},
{
"id": 2,
"name": "Tina",
"mobile": "(555)2367-1287",
"groups": [
{
"id": 1,
"name": "Collegue"
},
{
"id": 3,
"name": "Family"
}
]
},
{
"id": 3,
"name": "Bill",
"mobile": "(555)2589-1134",
"groups": [
{
"id": 1,
"name": "Collegue"
},
{
"id": 2,
"name": "Friend"
},
{
"id": 3,
"name": "Family"
}
]
},
{
"id": 4,
"name": "Michael",
"mobile": "(555)3345-2345",
"groups": []
},
{
"id": 5,
"name": "Jackie",
"mobile": "(555)1123-1123",
"groups": []
}
]
}
# 获取1个联系人,在其记录中嵌入其所在群组
curl -X GET "http://localhost:3000/api/v1/contacts/1?_embed=groups"
{
"statusCode": 200,
"message": "Found",
"data": {
"id": 1,
"name": "Tracy",
"mobile": "(555)1234-1256",
"groups": [
{
"id": 1,
"name": "Collegue"
},
{
"id": 2,
"name": "Friend"
}
]
}
}
# 获取所有群组,每个群组中嵌入其包含的联系人
curl -X GET "http://localhost:3000/api/v1/groups?_embed=contacts"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 1,
"name": "Collegue",
"contacts": [
{
"id": 1,
"name": "Tracy",
"mobile": "(555)1234-1256"
},
{
"id": 2,
"name": "Tina",
"mobile": "(555)2367-1287"
},
{
"id": 3,
"name": "Bill",
"mobile": "(555)2589-1134"
}
]
},
{
"id": 2,
"name": "Friend",
"contacts": [
{
"id": 1,
"name": "Tracy",
"mobile": "(555)1234-1256"
},
{
"id": 3,
"name": "Bill",
"mobile": "(555)2589-1134"
}
]
},
{
"id": 3,
"name": "Family",
"contacts": [
{
"id": 2,
"name": "Tina",
"mobile": "(555)2367-1287"
},
{
"id": 3,
"name": "Bill",
"mobile": "(555)2589-1134"
}
]
},
{
"id": 4,
"name": "Business",
"contacts": []
}
]
}
# 获取一个群组,在其记录中嵌入其包含的联系人
curl -X GET "http://localhost:3000/api/v1/groups/1?_embed=contacts"
{
"statusCode": 200,
"message": "Found",
"data": {
"id": 1,
"name": "Collegue",
"contacts": [
{
"id": 1,
"name": "Tracy",
"mobile": "(555)1234-1256"
},
{
"id": 2,
"name": "Tina",
"mobile": "(555)2367-1287"
},
{
"id": 3,
"name": "Bill",
"mobile": "(555)2589-1134"
}
]
}
}
五、also-json-server 的数据维护
5.1 添加记录
POST <url>/<collection>
curl -X POST -H "Content-type: application/json" -d '{"name":"Mickey","mobile":"(555)2981-1820"}' "http://localhost:3000/api/v1/contacts"
{
"statusCode": 201,
"message": "Record created",
"data": {
"id": 6,
"name": "Mickey",
"mobile": "(555)2981-1820"
}
}
5.2 修改记录(PATCH:修改局部)
PATCH <url>/<collection>/:id
curl -X PATCH -H "Content-type: application/json" -d '{"mobile":"(555)2981-1821"}' "http://localhost:3000/api/v1/contacts/6"
{
"statusCode": 200,
"message": "Update success",
"data": {
"id": 6,
"name": "Mickey",
"mobile": "(555)2981-1821"
}
}
5.3 修改记录(PUT:整体替换)
PUT <url>/<collection>/:id
注意:除 id 外,没有出现在提交数据里的字段将全部消失!
# name 将从原记录中消失
curl -X PUT -H "Content-type: application/json" -d '{"mobile":"(555)2981-1821"}' "http://localhost:3000/api/v1/contacts/6"
{
"statusCode": 200,
"message": "Update success",
"data": {
"mobile": "(555)2981-1821",
"id": 6
}
}
5.4 删除记录
DELETE <url>/<collection>/:id
curl -X DELETE "http://localhost:3000/api/v1/contacts/6"
{
"statusCode": 200,
"message": "Delete success",
"data": {
"mobile": "(555)2981-1821",
"id": 6
}
}
六、用户验证
以下例子以以下方式启动服务器
$ also-json-server -P /api/v1 -aot
Also JSON Server started on PORT :3000
Using auth...
Press CTRL-C to stop
Watching ...
( ˶ˆ ᗜ ˆ˵ )
Index:
http://localhost:3000/
Static files:
Serving ./public directory if it exists
Endpoints:
POST http://localhost:3000/api/v1/auth/login
GET http://localhost:3000/api/v1/auth/logout
http://localhost:3000/api/v1/users
http://localhost:3000/api/v1/posts
http://localhost:3000/api/v1/comments
http://localhost:3000/api/v1/contacts
http://localhost:3000/api/v1/groups
http://localhost:3000/api/v1/profile
6.1 用户登录
curl -X POST -H "Content-type: application/json" \
-d '{"email":"jhon@email.com","password":"JhonPass"}' \
http://localhost:3000/api/v1/auth/login
{
"status_code": 200,
"user": {
"id": 1,
"name": "John Doe",
"email": "jhon@email.com",
"token": "86f106c3303f8061fb0533388a2ee892da598486"
}
}
6.2 数据访问
6.2.1 不使用 token 访问
curl -X GET http://localhost:3000/api/v1/posts
{
"statusCode": 401,
"message": "Unauthorized"
}
6.2.2 使用头部 Authorization 携带 token
curl -X GET \
-H "Authorization: Bearer 86f106c3303f8061fb0533388a2ee892da598486" \
http://localhost:3000/api/v1/posts
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 1,
"title": "a title",
"views": 100
},
{
"id": 2,
"title": "another title",
"views": 200
}
]
}
6.2.3 使用查询参数携带 token
curl -X GET "http://localhost:3000/api/v1/posts?_token=86f106c3303f8061fb0533388a2ee892da598486"
{
"statusCode": 200,
"message": "Success",
"data": [
{
"id": 1,
"title": "a title",
"views": 100
},
{
"id": 2,
"title": "another title",
"views": 200
}
]
}
6.3 用户登出
curl -X GET \
> -H "Authorization: Bearer 86f106c3303f8061fb0533388a2ee892da598486" \
> http://localhost:3000/api/v1/auth/logout
{
"statusCode": 200,
"message": "Logout Success"
}
七、项目存在的问题
- 暂时没有元数据支持,无法实现数据检验(也许大多数情况下并不需要?)。
- 考虑到为了实现数据校验,就需要在 json 文件中添加一定形式的元数据标记,这可能增加了用户的使用难度,所以暂时不会增加这个功能。