三、请求相关的参数装饰器

在上一节,我们已经成功启动了服务,现在我们要来封装一些常用的参数装饰器。

们主要实现几个参数装饰器,获取常用的一些信息,如Request, Query, Headers, Param

关于我们要封装的参数装饰器的用法,这里不会赘述,更多的关注其实现,如果不清楚用法请先自行查看官方文档。后续的封装也保持这个思路。

前置内容

参数装饰器封装的主要思路如下:

  • 定义参数装饰器,将参数索引,使用的装饰器,传递的数据,存放到方法的元数据中
  • NestApplication中,解析参数,根据不同的装饰器,映射为不同的内容
  • 注册路由,在处理程序中将参数传入

其实核心还是express,不管是@Request获取请求对象,@Query, @Param获取参数,这些功能都是由express实现的,只是做了一层封装罢了。

我们新建了一个controller文件user.controller.ts,注册几个新的路由来演示这些功能:

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
// user.controller.ts
import { Controller, Get, Req, Query, Headers, Session, IP } from "./@nestjs/common";
import { Request } from 'express'
@Controller("users")
export class UserController {
@Get("req")
handleRequest(@Req() req: Request) {
console.log(req.url, req.path, req.method)
return 'handleRequest'
}
@Get("query")
// 通过@Query()拿到整个query参数对象,通过@Query(key)拿到query中指定key对应的值
handleQuery(@Query() query:any, @Query("id") id:string) {
console.log(query)
console.log(id)
return `query id:${id}`
}

@Get("headers")
// 通过@Headers()拿到整个headers参数对象,通过@Headers(key)拿到headers中指定key对应的值
handleHeaders(@Headers() headers:any, @Headers("accept") accept:string) {
console.log(headers)
console.log(accept)
return `headers accept:${accept}`
}

@Get("session")
handleSession(@Session() session: any, @Session('pageView') pageView: string) {
console.log(session)
console.log(pageView)
if (session.pageView) {
session.pageView ++
} else {
session.pageView = 1
}
return `pageView:${pageView}`
}

@Get("ip")
getUserIP(@IP() ip: string) {
console.log(ip)
return `ip:${ip}`
}

@Get(':username/:age/info')
getUserInfoByName(@Param() params, @Param('username') username: string, @Param('age') age:string) {
console.log(params)
console.log(username)
console.log(age)
return `username:${username}, age:${age}`
}
}

可以看到我们实现了@Session装饰器,这里用到了express-session中间件,所以我们先把中间件引入。

main中使用这个中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// main.ts
import * as session from 'express-session'
import { NestFactory } from './@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 使用session中间件
app.use(session({
secret: 'your-secret-key', // 用于加密会话
resave: false, // 在每次请求结束后是否强制保存绘画
saveUninitialized: false, // 是否保存被初始化的绘画
cookie: {
maxAge: 1000 * 60 * 60 * 24 // cookie有效期24小时
}
}))
await app.listen(3000);
}
bootstrap();

下边开始我们的源码实现部分:

此时我们还没有实现use方法,所以在NestApplication中实现use方法,其实就是expressuse方法:

1
2
3
4
5
6
7
8
9
10
11
// @nestjs/core/nest-application.ts
class NestApplication {
/**
* 其他内容不变
*/

// use方法,使用中间件,直接调用express的use方法
use(middleware) {
this.app.use(middleware)
}
}

参数装饰器实现

接下来我们实现参数装饰器,我们创建了一个生成 装饰器工厂函数工厂函数,在内部给参数所在方法添加元数据,包括每个参数所在的索引,用到的装饰器,传递给装饰器的参数

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
// @nestjs/common/param.decorator.ts
import 'reflect-metadata'

export const createParamDecorator = (decorator: string) => {
// 返回一个参数装饰器的工厂函数,而不是直接返回一个装饰器
return (data?: any): ParameterDecorator => {
// 返回一个参数装饰器
return (target: Object, propertyKey: string, parameterIndex: number) => {
// 给参数所在的方法添加元数据,key是params,值是一个数组,数组中存储方法各个索引位置的参数分别使用哪个装饰器
// 先取出当前已经存储的参数及对应装饰器
const existedParameters = Reflect.getMetadata('params', target, propertyKey) || []
// 把当前参数的索引和装饰器存起来
existedParameters[parameterIndex] = {
parameterIndex, // 参数的索引
decorator, // 使用的装饰器
data // 传递给装饰器的参数,可能会有
}
Reflect.defineMetadata(`params`, existedParameters, target, propertyKey)
}
}
}

// @Request和@Req是一样的
export const Request = createParamDecorator('Request')
export const Req = createParamDecorator('Req')

// @Query
export const Query = createParamDecorator('Query')

// @Headers
export const Headers = createParamDecorator('Headers')

// @Session
export const Session = createParamDecorator('Session')

// @IP
export const IP = createParamDecorator('IP')

// @Param
export const Param = createParamDecorator('Param')

param.decorator中,我们只是存储了元数据,我们注册路由是在NestApplication中做的,所以我们要实现解析参数的方法,并在路由处理程序中将参数传入。

NestApplication代码中,其实改动内容只有两点:

  • init方法中注册路由程序的时候,先获取到了参数,并将参数传入处理程序中
  • 实现了resolveParams方法,在内部完成了参数的解析过程,主要是根据不同的装饰器做一个映射,传入不同的内容
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
// @nestjs/common/nest-application.ts
// 源码是使用express-adaptor适配器的,此处做简化版
import * as express from 'express'
import type { Express, Request, Response, NextFunction } from 'express'
import 'reflect-metadata'
import { Logger } from './logger'
import * as path from 'path'
export class NestApplication {
// 在它的内部私有化一个Express实例
private readonly app = express()

constructor(protected readonly module: any) {}
// 初始化
async init() {
// 取出模块里的所有控制器,做好路由配置
// 传入的模块及其对应的控制器等内容已经执行了装饰器,所以元数据已经定义好了
const controllers = Reflect.getMetadata('controllers', this.module) || []
Logger.log(`AppModule dependencies initialized`, 'InstanceLoader')
// 遍历控制器
for (const Controller of controllers) {
// 创建每个控制器的实例
const controller = new Controller()
// 获取控制器的路径前缀, 无前缀则默认'/'
const prefix = Reflect.getMetadata('prefix', Controller) || '/'
Logger.log(`${Controller.name} {${prefix}}`, 'RoutesResolver')
// 遍历类里的方法
const controllerPrototype = Reflect.getPrototypeOf(controller)
for (const methodName of Object.getOwnPropertyNames(controllerPrototype)){
// 取方法
const method = controllerPrototype[methodName]
// 取请求方法
const httpMethod = Reflect.getMetadata('method', method)
// 取路径
const httpPath = Reflect.getMetadata('path', method)
if (!httpMethod) continue;
// 拼接路由
const routePath = path.posix.join('/', prefix, httpPath)
// 注册路由 - express上通过app.get('/path', () => {})来注册路由
this.app[httpMethod.toLowerCase()](routePath, (req: Request, res: Response, next: NextFunction) => {
// 解析参数
const args = this.resolveParams(controller, methodName, req, res, next)
// 交由对应方法来处理请求
const result = method.call(controller, ...args)
res.send(result)
})
Logger.log(`Mapped {${routePath}, ${httpMethod}} route`, 'RoutesResolver')


}
}
Logger.log('Nest application successfully started', 'NestApplication')
}
// 解析参数
private resolveParams(instance: any, methodName: string, req: Request, res: Response, next: NextFunction) {
// 取出方法存储的参数元数据 => [{parameterIndex, decorator, data?}, ...]
const existedParameters = Reflect.getMetadata('params', instance, methodName) || []
// 根据参数使用的装饰器,做不同的映射
return existedParameters.map((param) => {
const {decorator, data} = param
switch (decorator) {
// 这两个一样
case 'Request':
case 'Req':
return req
case 'Query':
// 判断data是否有值,确定是取指定key还是所有query数据
return data ? req.query[data] : req.query
case 'Headers':
return data ? req.headers[data] : req.headers
case 'Session':
// @ts-ignore
return data ? req.session[data] : req.session
case 'IP':
return req.ip
case 'Param':
return data ? req.params[data] : req.params
default:
return null
}
})
}
// 其他内容...
}

Post请求和请求体

看到Post请求可能觉得会复杂很多,其实是想歪了,要记住一点,我们是基于express来做这些事,所以一切复杂的东西,其实express都已经给做好了,我们只是做一层封装就可以了,PostGet没什么区别

controller中加一个路由用来测试:

1
2
3
4
5
6
7
// usrs.controller.ts
@Post('create')
createUser(@Body() createUserDto, @Body('username') username: string) {
console.log(createUserDto)
console.log(username)
return 'create success'
}

第一步,我们先在http-methods.decorator中实现@Post装饰器,还是坚持上边的观点,都是基于express的,所以不用想着Post请求可以接body就会比Get请求麻烦很多,其实在Nestjs这一层是一样的,只是请求方法不同罢了

1
2
3
4
5
6
7
8
9
// @nestjs/common/http-methods.decorator.ts
// 新增Post装饰器,除了请求方法,其他的与Get一致
export function Post(path:string = ''): MethodDecorator {
return (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
// 给修饰的方法添加元数据
Reflect.defineMetadata('path', path, descriptor.value)
Reflect.defineMetadata('method', 'POST', descriptor.value)
}
}

按照之前的思路,下一步我们要在NestApplication的解析参数那里,处理一下Body装饰器即可,但是要注意express默认并不能直接在req.body中读取请求体,我们要先设置中间件才行,所以在NestApplication的构造函数中加入以下内容:

1
2
3
4
5
6
7
//@nestjs/core/nest-application.ts
class NestApplication {
constructor(protected readonly module: any) {
this.app.use(express.json()) // 把json格式的请求体放到req.body上
this.app.use(express.urlencoded({extended: true})) // 把form格式的表单放到req.body上
}
}

接下来就很常规了,只需要在param.decorator中定义@Body装饰器并在resolveParams方法中加入一个新的case就可以了:

1
2
3
4
5
6
// @nestjs/core/nest-application.ts
/**
* 加入到解析参数部分的一个新case
*/
case 'Body':
return data ? req.body[data] : req.body
作者

胡兆磊

发布于

2024-10-18

更新于

2025-04-21

许可协议