内置装饰器

@Module

@Module 声明模块

ts
1@Module({
2  imports: [],
3  controllers: [AppController],
4  providers: [AppService],
5})

@Controller

声明 Controller

typescript
1@Controller()
2export class AppController {
3  constructor(private readonly appService: AppService) {}
4}

@Controller() 装饰器内还可以传入一些属性,比如指定 path

typescript
1@Controller({ path: 'admin' })

这样就相当于在原有路由的基础上额外增加 admin 路由。

举个例子,当前的路由访问http://127.0.0.1:3000/hello可以获取到 Hello World

typescript
1@Controller()
2export class AppController {
3  constructor(@Inject('AppService') private readonly appService: AppService) {}
4
5  @Get('/hello')
6  getHello(): string {
7    return this.appService.getHello()
8  }
9}

当加入 path 后:

diff
1- @Controller()
2+ @Controller({ path: 'admin' })
3export class AppController {
4  constructor(@Inject('AppService') private readonly appService: AppService) {}
5
6  @Get('/hello')
7  getHello(): string {
8    return this.appService.getHello();
9  }
10}

就需要访问http://127.0.0.1:3000/admin/hello才能获取到了。

@Injectable

声明 provider

typescript
1@Injectable()
2export class AppService {
3  getHello(): string {
4    return 'Hello World!'
5  }
6}

只要是 class,都可以用 @Injectable 表示该 class 是可注入到其他模块的。

@Inject

注入时可以使用@Inject

typescript
1export class AppController {
2  @Inject(AppService)
3  private readonly appService: AppService
4}

效果相当于使用构造器

typescript
1@Controller()
2export class AppController {
3  constructor(private readonly appService: AppService) {}
4
5  @Get()
6  getHello(): string {
7    return this.appService.getHello()
8  }
9}

如果 token 是字符串的话,也可以直接传入 token 来注入:

token:

typescript
1@Module({
2  imports: [],
3  controllers: [AppController],
4  providers: [{ provide: 'AppService', useClass: AppService }],
5})

注入 token:

typescript
1export class AppController {
2  @Inject('AppService')
3  private readonly appService: AppService
4
5  @Get()
6  getHello(): string {
7    return this.appService.getHello()
8  }
9}

@Optional

如果传入不正确的 token,创建AppController时会报错,但如果它是可选的,就可以使用@Optional 声明一下,这样没有对应的 provider 也能正常创建这个对象。

typescript
1@Controller()
2export class AppController {
3  @Optional()
4  @Inject('service')
5  private readonly service: Record<string, any>
6
7  constructor(@Optional() @Inject('AppService') private readonly appService: AppService) {}
8
9  @Get()
10  getHello(): string {
11    return this.appService.getHello()
12  }
13}

@Global

用@Global 来声明全局模块,这样它 exports 的 provider 就可以直接注入无需 imports 了

typescript
1@Global()
2@Module({
3  imports: [],
4  controllers: [AppController],
5  providers: [AppService],
6  exports: [AppService],
7})

@Catch

filter 是处理抛出的未捕获异常的,通过 @Catch 来指定处理的异常:

typescript
1@Catch(HttpException)
2export class HttpExceptionFilter implements ExceptionFilter {
3  catch(exception: HttpException, host: ArgumentsHost) {
4    const ctx = host.switchToHttp()
5    const response = ctx.getResponse()
6    const request = ctx.getRequest()
7    const status = exception.getStatus()
8    response.status(status).json({
9      statusCode: status,
10      path: request.url,
11      message: exception.message,
12    })
13  }
14}

@UseFilters

filter 通过 @UseFilters 应用到 handler 上:

typescript
1  @Get()
2  @UseFilters(HttpExceptionFilter)
3  getHello(): string {
4    throw new HttpException('Error', HttpStatus.BAD_REQUEST);
5    return this.appService.getHello();
6  }

@UseGuards

路由守卫

typescript
1@Controller()
2export class AppController {
3  @Get()
4  @UseGuards(RolesGuard)
5  getHello(): string {
6    return 'Hello World!'
7  }
8}

@UseInterceptors

拦截器

typescript
1@Controller()
2export class AppController {
3  @Get()
4  @UseInterceptors(new RouteInterceptor())
5  getHello(): string {
6    return 'Hello World!'
7  }
8}

@UsePipes

Pipe

typescript
1  @Post()
2  @UsePipes(AppPipe)
3  async create(@Body() createUser: CreateUserDto) {
4    // ...
5  }

@Param

取出 param,同时支持 Pipe 在单个 param 上的应用

ts
1  @Get(':id')
2  getHello(
3    @Param('id', IdPipe) id: string,
4    @Query('name') name: string,
5  ): string {
6    console.log('——————🚀🚀🚀🚀🚀 —— name:', name);
7    console.log('——————🚀🚀🚀🚀🚀 —— id:', id);
8    return this.appService.getHello();
9  }

@Query

取出 Query,同时支持 Pipe 在单个 Query 上的应用

typescript
1  @Get(':id')
2  getHello(
3    @Param('id') id: string,
4    @Query('name', QueryPipe) name: string,
5  ): string {
6    console.log('——————🚀🚀🚀🚀🚀 —— name:', name);
7    console.log('——————🚀🚀🚀🚀🚀 —— id:', id);
8    return this.appService.getHello();
9  }

@Body

从请求体中取出对应的数据

typescript
1@Controller('user')
2export class UserController {
3  @Post('get')
4  body(@Body() getPersonDto: PersonDto) {
5    return `received: ${JSON.stringify(getPersonDto)}`
6  }
7}

类型则定义在 dto 中:

typescript
1// src/dto/person.dto.ts
2
3export class PersonDto {
4  name: string
5  age: number
6}

@[Method]

除了 @Get、@Post 外,还可以用 @Put、@Delete、@Patch、@Options、@Head 装饰器

@SetMetadata

handler 和 class 可以通过 @SetMetadata 指定 metadata:

typescript
1@Controller()
2@UseGuards(AppGuard)
3@SetMetadata('roles', 'user')
4export class AppController {
5  constructor(@Inject('AppService') private readonly appService: AppService) {}
6
7  @Get('/hello')
8  @SetMetadata('roles', 'admin')
9  getHello(): string {
10    return this.appService.getHello()
11  }
12}

metadata 可以在 guard 或者 Interceptor 中取出来:

typescript
1@Injectable()
2export class AppGuard implements CanActivate {
3  @Inject(Reflector)
4  private readonly reflector: Reflector
5  canActivate(ctx: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
6    const classMetadata = this.reflector.get('roles', ctx.getClass())
7    const methodMetadata = this.reflector.get('roles', ctx.getHandler())
8    console.log('——————🚀🚀🚀🚀🚀 —— classMetadata:', classMetadata)
9    console.log('——————🚀🚀🚀🚀🚀 —— methodMetadata:', methodMetadata)
10    return true
11  }
12}

得到的结果:

bash
1——————🚀🚀🚀🚀🚀 —— classMetadata: user
2——————🚀🚀🚀🚀🚀 —— methodMetadata: admin

@Headers

通过 @Headers 装饰器取某个请求头 或者全部请求头:

typescript
1  @Get('/hello')
2  getHello(
3    @Headers('Accept') accept: string,
4    @Headers() headers: Headers,
5  ): string {
6    console.log('——————🚀🚀🚀🚀🚀 —— accept:', accept);
7    console.log('——————🚀🚀🚀🚀🚀 —— headers:', headers);
8    return this.appService.getHello();
9  }

@Ip

获取请求 IP 地址

typescript
1  @Get('/ip')
2  getIp(@Ip() ip: string): string {
3    console.log('——————🚀🚀🚀🚀🚀 —— ip:', ip);
4    return ip;
5  }

@Session

利用@Session 装饰器可以获取到 session 对象。

在使用 session 之前,需要安装一个插件:

bash
1pnpm install express-session

main.ts 里引入并启用该插件:

typescript
1async function bootstrap() {
2  const app = await NestFactory.create(AppModule)
3  app.use(
4    session({
5      secret: 'secret',
6      cookie: { maxAge: 60000 },
7    }),
8  )
9  await app.listen(3000)
10}

上面的代码指定了密钥跟 cookie 的过期时间。

接着刷新页面:

image-20231006111348925

可以看到 Response 里已经设置了 cookie 信息。

之后的每次请求浏览器都会自动在 request header 中带上 cookie。

2023-10-06.111932

现在我们就可以读取和设置 session 啦

typescript
1  @Get('/session')
2  getSession(@Session() se: Record<string, any>) {
3    console.log('——————🚀🚀🚀🚀🚀 —— se:', se.id);
4    if (!se.count) {
5      se.count = 0;
6    }
7    se.count += 1;
8    return { count: se.count, id: se.id };
9  }

第一次的结果:

typescript
1{
2"count": 1,
3"id": "GSNcXmu4fHQLVtxVKCc3QIbp38m4l1Zh"
4}

只要在同一个没有过期的会话中,每次请求都会让 count+1,而 id 不变:

typescript
1// 第二次请求
2{
3"count": 2,
4"id": "GSNcXmu4fHQLVtxVKCc3QIbp38m4l1Zh"
5}

@HostParam

在@Controller 装饰器内还可以指定生效的 path:

typescript
1@Controller({ host: ':host.0.0.1' })
2export class AppController {
3  constructor(@Inject('AppService') private readonly appService: AppService) {}
4
5  @Get('/hello')
6  getHello(): string {
7    return this.appService.getHello()
8  }
9}

现在仅能通过xxx.0.0.1访问路由才有效,通过localhost访问是无效的。

Oct-06-2023 11-48-41

host 里的参数就可以通过 @HostParam 取出来:

typescript
1@Controller({ host: ':host.0.0.1' })
2export class AppController {
3  constructor(@Inject('AppService') private readonly appService: AppService) {}
4
5  @Get('/hello')
6  getHello(@HostParam('host') host): string {
7    console.log('——————🚀🚀🚀🚀🚀 —— host:', host)
8    return this.appService.getHello()
9  }
10}

访问后的结果为:

bash
1——————🚀🚀🚀🚀🚀 —— host: 127

@Req

@Headers@Body@Ip等装饰器都帮我们快速从 request 对象中获取信息。如果我们想自己获取,也是可以的,@Req 就是将 Request 对象注入进来的装饰器。

typescript
1  @Get('/hello')
2  getHello(@Req() req: Request): string {
3    console.log('——————🚀🚀🚀🚀🚀 —— req:', req);
4    return this.appService.getHello();
5  }

通过 @Req 或者 @Request 装饰器,这俩是同一个东西,nest 源码里有类型声明:

typescript
1export declare const Request: () => ParameterDecorator
2export declare const Req: () => ParameterDecorator

注入 request 对象后,可以手动取任何参数。

@Res

@Res 和 @Response 也是同一个东西:

typescript
1export declare const Response: (options?: ResponseDecoratorOptions) => ParameterDecorator
2export declare const Res: (options?: ResponseDecoratorOptions) => ParameterDecorator

但注入 response 对象后需要手动调用才能响应而不是直接 return

typescript
1  @Get('/hello')
2  getHello(@Req() req: Request, @Res() res: Response) {
3    console.log('——————🚀🚀🚀🚀🚀 —— req:', req);
4    // return this.appService.getHello();
5    res.end(this.appService.getHello());
6  }

Nest 这么设计是为了避免你自己返回的响应和 Nest 返回的响应的冲突。

如果你不会自己返回响应,可以通过 passthrough 参数告诉 Nest:

typescript
1  @Get('/hello')
2  getHello(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
3    console.log('——————🚀🚀🚀🚀🚀 —— res:', res);
4    console.log('——————🚀🚀🚀🚀🚀 —— req:', req);
5    return this.appService.getHello();
6  }

现在能够正常响应了。

@Next

当你有两个 handler 来处理同一个路由的时候,可以在第一个 handler 里注入 next,调用它来把请求转发到第二个 handler。

typescript
1  @Get('/hello')
2  async getHello1(@Next() next: NextFunction) {
3    console.log('getHello1');
4    const a = await next();
5    // the code below will not be executed
6    console.log('——————🚀🚀🚀🚀🚀 —— a:', a); // undefined
7    return '123';
8  }
9  @Get('/hello')
10  async getHello2() {
11    console.log('getHello2');
12    return this.appService.getHello();
13  }

这里的 log 打印结果为:

bash
1getHello1
2getHello2
3——————🚀🚀🚀🚀🚀 —— a: undefined

注入 @Next 的 handler 不会处理返回值,所以上面代码中的 123不会被返回。

@HttpCode

handler 默认返回的是 200 的状态码,你可以通过 @HttpCode 修改它:

typescript
1  @Get('/hello')
2  @HttpCode(202)
3  getHello() {
4    return this.appService.getHello();
5  }

image-20231006122759324

@Header

修改 response Header

tsx
1  @Get('/hello')
2  @Header('reference', 'nest')
3  getHello() {
4    return this.appService.getHello();
5  }

image-20231006123121732

@Redirect

将路由重定向到其他 url 上。

typescript
1  @Get('/hello')
2  @Redirect('/hello2')
3  getHello() {}
4
5  @Get('/hello2')
6  getHello2() {
7    return this.appService.getHello();
8  }

Oct-06-2023 12-36-15

@Render

使用@Render 可以给响应内容指定渲染模版,不过需要先安装模版引擎的包 hbs

bash
1pnpm install hbs

然后准备图片和模版文件

image-20231006131528209

home.hbs 内写入渲染模版

html
1<img src="/nature.jpg" />
2<p>{{name}}</p>
3<div>{{age}}</div>

再分别指定静态资源的路径和模版的路径,并指定模版引擎为 handlerbars。

typescript
1import { NestFactory } from '@nestjs/core'
2import { NestExpressApplication } from '@nestjs/platform-express'
3import { AppModule } from './app.module'
4import { join } from 'path'
5
6async function bootstrap() {
7  const app = await NestFactory.create<NestExpressApplication>(AppModule)
8  app.useStaticAssets(join(process.cwd(), 'public'))
9  app.setBaseViewsDir(join(process.cwd(), 'views'))
10  app.setViewEngine('hbs')
11  await app.listen(3000)
12}
13bootstrap()

最后在 handler 中使用@Render 装饰器指定使用哪个模版

jsx
1  @Get('/hello')
2  @Render('home')
3  getHello() {
4    return { name: 'qyx', age: 18 };
5  }

效果如下:

image-20231006131901856

总结

  • @Module: 声明 Nest 模块
  • @Controller:声明模块里的 controller
  • @Injectable:声明模块里可以注入的 provider
  • @Inject:通过 token 手动指定注入的 provider,token 可以是 class 或者 string
  • @Optional:声明注入的 provider 是可选的,可以为空
  • @Global:声明全局模块
  • @Catch:声明 exception filter 处理的 exception 类型
  • @UseFilters:路由级别使用 exception filter
  • @UsePipes:路由级别使用 pipe
  • @UseInterceptors:路由级别使用 interceptor
  • @SetMetadata:在 class 或者 handler 上添加 metadata
  • @Get、@Post、@Put、@Delete、@Patch、@Options、@Head:声明 get、post、put、delete、patch、options、head 的请求方式
  • @Param:取出 url 中的参数,比如 /aaa/:id 中的 id
  • @Query: 取出 query 部分的参数,比如 /aaa?name=xx 中的 name
  • @Body:取出请求 body,通过 dto class 来接收
  • @Headers:取出某个或全部请求头
  • @Session:取出 session 对象,需要启用 express-session 中间件
  • @HostParm: 取出 host 里的参数
  • @Req、@Request:注入 request 对象
  • @Res、@Response:注入 response 对象,一旦注入了这个 Nest 就不会把返回值作为响应了,除非指定 passthrough 为 true
  • @Next:注入调用下一个 handler 的 next 方法
  • @HttpCode: 修改响应的状态码
  • @Header:修改响应头
  • @Redirect:指定重定向的 url
  • @Render:指定渲染用的模版引擎