请求与响应的统一处理
1 获取请求数据
通常客户端的请求会根据业务需求同时发送一些额外数据,数据的传输携带方式也有如下几种常见场景:
-
params
也就是我们所说的动态路由可变的部分。 -
queryString
url 问号后的部分。 -
body
通常就是我们说的请求正文部分。 -
headers
除了一些内置头,还可以自定义一些头信息,我们应用中的用户授权token
就是通过头信息来传递的。
koa-ts-controllers
不仅仅提供请求方法装饰器来处理请求,同时也提供了一些 参数装饰器
来处理请求数据。
-
@Params()
-
@Query()
-
@Body()
-
@Header()
1-1 Params
Params 装饰器
@Params() 或者 @Params(name)
我们可以通过装饰器 Params
拿取对应的数据。
// file: backend/src/controllers/Test.ts
import {
Controller,
Get,
Post,
Params
} from "koa-ts-controllers";
@Controller("/test")
class TestController {
@Get("/hello")
async hello() {
return "Hello Test!"
}
@Get("/user/:id")
async getUser(@Params() p:{id: number}) {
return "当前params中用户id是:" + p.id;
}
}
不传参是获取所有内容,也可以直接将id传参给@params
@Get("/user/:id")
async getUser(@Params("id") id: number) {
return "当前params中用户id是:" + id;
}
1-2 Query
Query
装饰器的用法与 Params
的用法一致。
@Get("/user")
async getUser2(@Query() q: {id: number}) {
return "当前params中用户id是:" + q.id;
}
1-3 Body
同上。
@Post("/postUser")
async postUser(@Body() body: {
name: string,
pwd: string,
}) {
return `提交的数据为:${JSON.stringify(body)}`
}
注意controllers
只起到一个管理职责,并没有实现路由功能
所以需要安装解析body的三方库
npm i koa-bodyparser
npm i -D @types/koa-bodyparser
使用
// app.ts
import koaBodyParser from "koa-bodyparser";
...
app.use(koaBodyParser());
利用postman模拟请求
1-4 Header
还是同上。
@Post("/postUser")
async postUser(
@Body() body: {
name: string,
pwd: string,
},
@Header() h: any
) {
console.log(h);
return `提交的数据为:${JSON.stringify(body)}`
}
2 数据的响应
2-1 响应类型
- 成功响应
- 错误响应
2-2 成功响应处理
成功响应处理比较简单,根据不同情况返回对应状态码(200、201)和内容。
2-3 错误响应处理
- 服务器错误(500)
- 其它一些业务逻辑错误(422、401、403……)
- 请求路由不存在(404)
错误捕获处理
我们可以通过 koa-ts-controllers
的统一错误处理函数来捕获错误,并同时输出。
// file: backend/src/app.ts
// ...
await bootstrapControllers(app, {
router: router,
basePath: '/api',
versions: [1],
controllers: [
path.resolve(__dirname, "controllers/**/*"),
],
errorHandler: async (err: any, ctx: Context) => {
let status = 500;
let body: any = {
statusCode: 500,
error: "Internal Server error",
message: "An internal server error occurred"
};
ctx.status = status;
ctx.body = body;
}
});
// ...
2-4 验证请求数据
许多时候,我们会对请求携带的数据进行一些必要的验证。
params
对于 params
数据,我们可以直接通过 path
规则进行验证。
比如id只允许数字,非数字报错404
file: backend/src/controllers/Test.ts
//...
@Get("/user/:id(\\d+)")
async getUser(@Params("id") id: number) {
return "当前params中用户id是:" + id;
}
// ...
query 和 body
对于 query
和 body
数据,我们可以通过 class-validator
库来进行统一处理。
class-validator 地址
定义验证类
通过类来定义要验证的数据。
其中,属性表示要验证的包含数据名称,配合着 class-validator
提交的装饰器来定义数据验证规则。
file: backend/src/controllers/Test.ts
import {IsNumberString} from 'class-validator';
class GetUsersQuery {
@IsNumberString()
page: number;
}
使用验证
把验证类作为要验证的参数(如:query)的类型。当请求该路由以后就会使用验证类对对应的数据进行验证。
file: backend/src/controllers/Test.ts
import {Controller, Get, Params, Query} from "koa-ts-controllers";
import {IsNumberString} from 'class-validator';
class GetUsersQuery {
@IsNumberString()
page: number;
}
//...
@Get("/users")
async getUsers(@Query() q: getUsersQuery) {
console.log(q);
return `传过来的query:${JSON.stringify(q)}`;
}
// ...
为了方便统一管理,我们可以把验证类存放到一个外部文件中,然后进行引入
存放目录:backend/src/validators/[模块,如:User].ts
验证返回格式
如果验证失败,返回如下格式:
为了保证和我们前面定义的响应格式保持一致,我们对这个数据进行分类处理。
// file: backend/src/app.ts
// ...
await bootstrapControllers(app, {
router: router,
basePath: '/api',
versions: [1],
controllers: [
__dirname + '/controllers/**/*'
],
errorHandler: async (err: any, ctx: Context) => {
let status = 500;
let body: any = {
"statusCode": 500,
"error": "Internal Server error",
"message": "An internal server error occurred"
};
if (err.output) {
status = err.output.statusCode;
body = {...err.output.payload};
if (err.data) {
body.errorDetails = err.data;
}
}
ctx.status = status;
ctx.body = body;
}
});
// ...
自定义验证信息
class GetUsersQuery {
// 验证是否为数字字符串
@IsNumberString({},{
message: "page必须为数字"
})
page: number;
}
2-5 其它业务逻辑错误
有些错误是一些业务逻辑等方面的错误,比如:用户已经被注册,没有该用户等等。
我们这里会用到一个库 @hapi/Boom
,上面的 class-validator
与 koa-ts-controllers
配合验证中,也是使用了它。听过它提供的一些 API,返回对应 HTTP 状态的响应格式。
file: backend/src/controllers/Test.ts
import {Controller, Get, Params, Query} from "koa-ts-controllers";
import {IsNumberString} from 'class-validator';
import Boom from '@hapi/Boom';
class GetUsersQuery {
@IsNumberString()
page: number;
}
//...
@Get("/users")
async getUsers(@Query() q: GetUsersQuery) {
// 比如业务逻辑错误,用户名已经被注册
if (true) {
throw Boom.notFound("注册失败", "用户名已经被注册了");
}
return `传过来的query:${JSON.stringify(q)}`;
}
// ...
返回的错误格式如下:
通过前面的统一处理,返回统一的错误格式。
2-6 未命中的路由
最后,我们还要对未命中的路由(不存在的api)进行一个单独的处理。
// file: backend/src/app.ts
// ...
import Boom from '@hapi/Boom';
await bootstrapControllers(app, {
//...
});
router.all('*', async ctx => {
throw Boom.notFound();
});
以上需要注意:
await
router.all
放在bootstrapControllers
后面
否则每次请求都会先处理 '*'
。