使用 also-json-server 自动生成后台 Restful API

与本文相关的【视频链接】

一、also-json-server 的初衷

在我的早期开发生涯或教学中,如果需要进行消费 Restful API 的功能测试,往往需要自行编写一个能够提供相应 API 的后台程序。后来,大约在 2019 年,我发现了一个 github 项目,叫做 json-server。它可以让测试人员编写一个 json 作为后端数据文件,然后根据数据生成典型的 Restful API。

在使用这个项目的过程中,我感觉到该项目缺乏:

  1. 一些典型的需要被测试的功能
  2. 服务器的定制性
  3. 易用性

为了达到自己的使用要求,我 Fork 了该项目进行修改。为了表达对原作者的尊重,我将项目取名为 also-json-server

针对以上三项缺失,我做了以下功能的添加:

  1. 添加了多对多关系数据的处理(功能性)
  2. 添加了身份验证的功能(功能性)
  3. 服务器可定制:端口、URL、返回结果形式、响应延迟时间等(定制性)
  4. 添加了自动生成样例数据文件,便于用户进行测试和模拟(易用性)

二、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 文件中添加一定形式的元数据标记,这可能增加了用户的使用难度,所以暂时不会增加这个功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陶艺夫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值