目前来看呢,我们也有好几个接口了,如下:
CoolApi
├── apis
│ ├── admin
│ │ ├── login.js
│ │ └── register.js
│ └── article
│ ├── delete.js
│ ├── detail.js
│ ├── insert.js
│ ├── select.js
│ └── update.js
├── public
├── mysql.js
├── package.json
├── server.js
└── routes.js
可以看到,管理员目录
有 2个
接口,文章目录
有 5个
接口。
在我们平时的前后端数据对接中,一般是后端给前端提供接口文档,前端根据接口文档来查看接口的请求路径,请求参数,请求格式等。
那么本文呢,我们来抽丝剥茧,探索和实现 接口文档
。
目前,比较流行和常见的就是 swagger
接口文档。
它一般长这样。
这是 swagger 文档在线编辑器,地址是 https://editor.swagger.io。
左边是接口的协议,数据,描述,右边是具体的接口文档呈现。
我们做前后端数据对接的时候会看到,后端甩给前端一个地址,浏览器打开就能看到右边这样的接口文档。
其实就是接口框架获得所有接口的定义,然后拼接成左边这样的接口描述协议,最后生成右边的接口文档。
其中的重点呢,就是 接口协议
,只要有了这个,那就可以在诸如 ApiFox
,PostMan
等接口管理工具中非常方便地查看接口文档了。
那么在此之前呢,先把这个演示用的接口协议文件下载到本地。
接下来,我用比较流行的 ApiFox
来演示一下。
点击 更多功能
,再点击 导入
。
就能看到,支持的接口协议特别多,有 30 多种。
此时,我们选择 OpenApi/Swagger
,这里 OpenApi
和 Swagger
的关系,还是有点历史故事的。
简单来说,OpenAPI
是规范,Swagger
是实现该规范的一套工具,更详细的故事呢,如果感兴趣,可以去搜索了解。
继续往下拉,选择 文件方式导入
,选择我们刚才下载的接口文件。
点击确认导入。
可以看到,刚才的 swagger
接口文档,就导入到 ApiFox
中了。
所以,接口文档的表现形式,其实是多种多样的,我们用同一份接口协议数据,可以在不同的接口工具中查看。
那么我们这次的任务,你应该清楚了吧?主要是 2 个内容:
- 设计接口参数的定义和验证。
- 根据接口参数生成接口协议。
有了接口协议后呢,我们想用 Swagger
,还是 ApiFox
,还是其他接口工具来进行接口的查看和使用,都是没有问题的。
那么问题来了,怎么设计接口参数的定义和验证呢?。
// /apis/articleInsert.js 文件
import { mysqlPool } from "../mysql.js";
export default async (req) => {
try {
// 参数去掉前后空格
const title = req.body.title.trim();
const content = req.body.content.trim();
const author = req.body.author;
// ------------------------------------------------------
// 验证标题参数
if (title.length < 1 || title.length > 100) {
return {
code: 1,
msg: "文章标题长度不能小于1,不能大于100",
};
}
// ------------------------------------------------------
// 验证内容参数
if (content.length < 1 || content.length > 60000) {
return {
code: 1,
msg: "文章内容长度不能小于1,不能大于6000",
};
}
// ------------------------------------------------------
// 验证作者参数,必须为非0数字开头的整数
if (/[1-9]\d*/.test(author) === false) {
return {
code: 1,
msg: "文章作者必须传作者的数字ID",
};
}
// ------------------------------------------------------
// 从连接池中获取一个数据库连接
const db = await mysqlPool.getConnection();
// 查询数据库是否有对应的用户数据
const [result] = await db.query({
sql: "INSERT INTO `article` (`title`,`content`,`author`,`created_at`) VALUES (:title,:content,:author,:created_at)",
values: {
title: title,
content: content,
author: Number(author),
created_at: Date.now(),
},
});
// 释放数据库连接
db.release();
// 返回成功信息
return {
code: 0,
msg: "添加文章成功",
data: {
insertId: result.insertId,
},
};
} catch (err) {
console.log("🚀 ~ err:", err);
return {
code: 1,
msg: "未知错误",
};
}
};
还记得我们的 博客文章添加接口
吗,里面我们进行接口参数验证的方式,就是用的 if
语句来进行判断。
这种方式非常原始,容易出错,不好扩展,这个时候,一般会使用 JSON验证协议
和 JSON协议验证工具
来做这个事情。
说起来比较晦涩,接口怎么跟 JSON
扯上关系了?
因为,JSON
是一个跟 JavaScript
天然友好的 数据协议
。
比如:
let user = {
name: "陈随易",
age: 31,
};
这在 JavaScript
中,叫做 JS对象
,那么我们来进行传输的时候,就是这样的 {"name":"陈随易","age":31}
,叫做 JSON协议
。
可以看到,JS对象
和 JSON协议
堪称无缝转场,非常丝滑。
那么我们进行参数的定义,就是用这种格式来定义的,名词叫做:JSON验证协议
。
JSON验证协议
,常用的有 JSON Schema
和 JSON Type Definition
,后者简称 JTD
。
而 JSON协议验证工具
就多了,比如 Ajv
,Joi
,Yup
等等。
那么这里呢,我们就用 JSON Schema
+ Ajv
来处理我们的 接口参数验证
问题,因为它们比较流行,性能也不错,是比较常用的方案。
等我们定义好 接口参数验证
后,再把它转换成 OpenApi接口文件
,是不是就能放到任意接口工具查看使用了呢。
{
"openapi": "3.0.0",
"info": {
"title": "CoolApi接口文档",
"version": "1.0.0"
},
"paths": {
"/user/login": {
"post": {
"tags": ["用户"],
"summary": "用户登录",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "用户名"
},
"password": {
"type": "string",
"description": "密码"
}
},
"required": ["username", "password"]
}
}
}
},
"responses": {
"200": {
"description": "登录成功",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"token": {
"type": "string",
"description": "登录令牌"
}
}
}
}
}
},
"401": {
"description": "登录失败,用户名或密码错误"
},
"500": {
"description": "服务器内部错误"
}
}
}
}
}
}
说到这里,我们看看 OpenApi接口文件
是如何定义 用户登录接口
的:
放到 swagger
的在线接口编辑器中,效果如上。
{
"name": "CoolApi",
"version": "1.0.0",
"description": "我的接口框架",
"type": "module",
"main": "server.js",
"scripts": {
"dev": "nodemon server.js"
},
"dependencies": {
"ajv": "^8.17.1",
"ajv-i18n": "^4.2.0",
"mime-types": "^2.1.35",
"mysql2": "^3.10.3"
},
"devDependencies": {
"nodemon": "^3.1.4"
}
}
接下来就开始用代码实现。
首先安装好 ajv
和 ajv-i18n
2 个依赖库。
import Ajv from "ajv";
import localize from "ajv-i18n";
const ajv = new Ajv({ allErrors: true, messages: true });
// 参数验证
export const ajvValid = (schema, data) => {
return new Promise((resolve, reject) => {
const validate = ajv.compile(schema);
const valid = validate(data);
if (!valid) {
localize.zh(validate.errors);
reject({ from: "ajv", data: ajv.errorsText(validate.errors, { separator: "\n" }) });
} else {
resolve();
}
});
};
然后新建一个 /utils.js
文件,写一个专门用于参数验证的函数 ajvValid
并导出。
// /apis/admin/login.js 文件
import { mysqlPool } from "../../mysql.js";
import { ajvValid } from "../../utils.js";
export const apiSchema = {
// 元数据
meta: {
tags: ["用户"],
summary: "用户登录",
},
// 请求协议
request: {
type: "object",
properties: {
username: {
type: "string",
title: "用户名",
minLength: 2,
maxLength: 20,
},
password: {
type: "string",
title: "密码",
minLength: 6,
maxLength: 50,
},
},
required: ["username", "password"],
additionalProperties: false,
},
// 返回协议
response: {
type: "object",
properties: {
token: {
type: "string",
description: "登录令牌",
},
},
},
};
// 接口逻辑
export default async (req) => {
try {
// 请求参数验证
const apiData = {
username: req.body.username.trim(),
password: req.body.password.trim(),
};
await ajvValid(apiSchema.request, apiData);
// ------------------------------------------------------
// 从连接池中获取一个数据库连接
const db = await mysqlPool.getConnection();
// 查询数据库是否有对应的用户数据
const [rows] = await db.query({
sql: "SELECT * FROM `user` WHERE `username`=:username LIMIT 1",
values: {
username: username,
},
});
// ------------------------------------------------------
// 如果查到了用户数据,说明该用户名已注册
if (rows.length <= 0) {
return {
code: 1,
msg: "用户未注册",
};
}
// ------------------------------------------------------
// 判断密码是否匹配
const [user] = rows; // rows是一个数组,我们这里用user变量去取数组的第一个值
if (user.password !== password) {
return {
code: 1,
msg: "密码错误",
};
}
// 释放数据库连接
db.release();
// 返回成功信息
return {
code: 0,
msg: "登录成功",
data: user,
};
} catch (err) {
if (err?.from === "ajv") {
return {
code: 1,
msg: err.data,
};
} else {
return {
code: 1,
msg: "未知错误",
};
}
}
};
在 /apis/admin/login.js
文件增加对请求参数的验证和导出,然后就能把我们的 if
判断干掉了。
此时,我们进行管理员登录,就会提示以上信息,可以明确定位是哪些参数有问题。
比起之前的参数验证来说,现在的验证更加专业,更加可扩展,更加强大。
那么解决了 参数验证
问题后,下一步就是如何生成 接口文档
了。
根据我们前面的内容来说,所谓的接口文档的核心,其实就是生成 接口定义的JSON文件
。
import { readdirSync, writeFileSync } from "node:fs";
const files = readdirSync("./apis", { recursive: true });
const apiTemplate = {
openapi: "3.0.0",
info: {
title: "CoolApi接口文档",
version: "1.0.0",
},
paths: {},
};
// 接口映射对象
const apiPaths = {};
for (let file of files) {
if (file.endsWith(".js")) {
const { apiSchema } = await import(`./apis/${file}`);
if (apiSchema) {
apiPaths["/" + file.replace(".js", "").replace(/\\+/, "/")] = {
post: {
tags: apiSchema?.meta?.tags,
summary: apiSchema?.meta?.summary,
requestBody: {
required: true,
content: {
"application/json": {
schema: apiSchema?.request || {},
},
},
},
responses: {
200: {
description: "操作成功",
content: {
"application/json": {
schema: {},
},
},
},
},
},
};
}
}
}
apiTemplate.paths = apiPaths;
writeFileSync("./apis.json", JSON.stringify(apiTemplate));
接下来,我们创建一个 /genApis.js
文件,代码如上。
大概意思就是,遍历接口文件,然后获取协议数据,把协议数据拼接到一起,最后生成 apis.json
接口数据文件。
{
"openapi": "3.0.0",
"info": {
"title": "CoolApi接口文档",
"version": "1.0.0"
},
"paths": {
"/admin/login": {
"post": {
"tags": ["用户"],
"summary": "用户登录",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"username": { "type": "string", "title": "用户名", "minLength": 2, "maxLength": 20 },
"password": { "type": "string", "title": "密码", "minLength": 6, "maxLength": 50 }
},
"required": ["username", "password"],
"additionalProperties": false
}
}
}
},
"responses": {
"200": {
"description": "操作成功",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
}
}
}
这是生成之后的文件。
这是生成之后的文件,放到在线文档编辑器中的效果。
趁热打火,把所有接口都写上参数验证,并生成协议,效果如上。
把接口文件 apis.json
导入到 ApiFox
接口管理工具中,效果如上。
至此,我们就完成了接口的 参数验证
和 接口文档
生成,前后端对接就更方便了。
不过呢,请注意,本小册旨在分享这些事情背后的简单原理,目前的实现远谈不上完善,请勿要在实际项目中使用。
当你学会的 Node.js 全栈开发的一些思路,原理,方法,你再去用其他更完善的框架来开发项目,将会手到擒来。