前端入门后端NestJS(三):路由控制器Controller

30 阅读7分钟

通过nest new project-name 新建一个项目,我们在src/app.controller.ts里可以看到如下代码:

 // app.controller.ts
 import { Controller, Get } from '@nestjs/common';
 import { AppService } from './app.service';
 ​
 @Controller()
 export class AppController {
   constructor(private readonly appService: AppService) {}
 ​
   @Get()
   getHello(): string {
     return this.appService.getHello();
   }
 }

第六行constructor(private readonly appService: AppService) {}我们将在Providers一节讲解,这里先忽略:

第四行通过@Controller()装饰器,我们可以捕获前端传过来的所有以/开头的路由,也就是@Controller装饰器定义了一组路由的处理方式,这里的一组路由,就是以/开头的路由。如果是@Controller('app'),那么定义的分组路由就是以/app开头。

之后的getHello里的@Get()中传参为空,意味着捕获的路由是/@Controller定义了一组路由之后,其内部的类里需要去捕获这组路由下的子路由,包括分组路由本身。

我们通过nest start --watch启动服务后,在浏览器输入localhost:3000,即可看到hello world的输出。这个输出的逻辑同样放在Providers一节讲解。

image-20240622205507697.png

1. Get请求

通过@Get()装饰器,我们可以捕获指定的get请求。修改@Controller()@Controller('app'),即定义的分组路由为/app,然后修改@Get()装饰器为@Get('a')

 // app.controller.ts
 import { Controller, Get } from '@nestjs/common';
 import { AppService } from './app.service';
 ​
 @Controller('app')
 export class AppController {
   constructor(private readonly appService: AppService) {}
 ​
   @Get('a')
   getHello(): string {
     return this.appService.getHello();
   }
 }

首先通过@Controller()接收所有以/app开头的请求,然后在getHello方法中添加装饰器@Get('a')装饰器,这个装饰器传参为a,意味捕获的get请求路由为/app/a

image.png

2. 其他请求方式

刚才我们学会了如何处理Get请求,其他的诸如Post请求、Put请求也是一样的用法。比如Post请求:

// app.controller.ts
 import {Controller, Get, Post} from '@nestjs/common';
 ​
 @Controller('app')
 export class AppController {
   @Post('b')
   create(): string {
     return '处理了一个post请求。';
   }
 ​
   @Get('a')
   getHello():string {
     return '处理了一个get请求';
   }
 }

Postman中输入localhost:3000/app/b,将会被定义了@Post('b')装饰器的create方法所捕获,因此会返回处理了一个post请求。

image-20240622212406608.png

就是这么简单。Nest为所有标准HTTP方法提供装饰器: @Get()@Post()@Put()@Delete()@Patch()@Options()@Head() 。此外, @All() 装饰器将捕获未被处理的请求。请看下面代码:

// app.controller.ts
 import { All, Controller, Get, Post } from '@nestjs/common';
 import { AppService } from './app.service';
 ​
 @Controller('app')
 export class AppController {
   constructor(private readonly appService: AppService) {}
 ​
  @Post('b')
   create(): string {
     return '处理了一个post请求。';
   }
 ​
   @Get('a')
   getHello():string {
     return '处理了一个get请求';
   }
 ​
   @All()
   getAll(): string {
     return 'All';
   }
 }

这段代码里添加了一个@All()装饰器装饰的getAll方法。我们之前的@Post('b')@Get('a')分别捕获了/app/ppost请求,以及/app/aget请求,但是就是没有处理/app本身,而/app这个请求却是被@Controller(‘app’)定义过的,这时候,就是@All()装饰器的作用,它能捕获@Controller定义的未被处理的分组路由。

image.png

3. 获取请求的详细信息

上面我们学习了怎么处理不同的请求方式,接下来我们继续学习,怎么获取请求方式里传递的参数。

在处理GET请求的函数入参中,我们可以通过装饰器@Req注入Request实例,获得请求对象的信息。

 // app.controller.ts
 import { Controller, Get, Req } from '@nestjs/common';
 import { Request } from 'express';
 ​
 @Controller('app')
 export class CatsController {
   @Get('a')
   getHello(@Req() req: Request): {} {
     const { headers, query, params, body, method, url } = req;
     return { headers, query, params, body, method, url };
   }
 }

为了使用 express 类型,(如上面的 req: Request 参数示例),需要安装 @types/express 包。

image.png

上图我用了谷歌的JSON-handle插件格式化了返回的报文样式。

在这个例子中,我们在req对象中获取了headers, query, params, body, method, url等属性,并返回给了前端。

不过对于常用的GET请求和POST请求来说,最常用的属性还是querybody,那每次使用这两个属性的时候都要从req中解构不是太麻烦了吗?

别急,Nest为我们提供了更简便的装饰器@Query@Body,通过这两个装饰器,我们就能快速获取请求中的query属性和body属性。

// app.controller.ts
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { AppService } from './app.service';
​
@Controller('app')
export class AppController {
  constructor(private readonly appService: AppService) {}
​
  @Get('a')
  getHello(@Query() query: {}): {} {
    return query;
  }
​
  @Post('b')
  create(@Body() body: {}): {} {
    return body;
  }
}

image.png

image-20240623163448508.png

上述代码分别验证了/app/aGET请求以及/app/bPOST请求,并分别用@Query()装饰器和@Body()装饰器获得了对应的参数和请求体。

类似的装饰器还有以下这些:

装饰器代表的对象
@Request(), @Req()req
@Response(), @Res()res
@Next()next
@Session()req.session
@Param(key?: string)req.params / req.params[key]
@Body(key?: string)req.body / req.body[key]
@Query(key?: string)req.query / req.query[key]
@Headers(name?: string)req.headers / req.headers[name]
@Ip()req.ip
@HostParam()req.hosts

@Req()@Request()的别名,@Res()@Response()的别名。

其中,需要留意的是@Ip()@Param()装饰器,这两个也是处理GET请求,因为在GET请求中,还有一种传参方式是路径传参

// app.controller.ts
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { AppService } from './app.service';
​
@Controller('app')
export class AppController {
  constructor(private readonly appService: AppService) {}
​
  @Get('a/:idNum')
  getHello(@Param() param: {}, @Param('idNum') id: string): {} {
    return { param: param, idNum: id };
  }
}

我们在原先/app/aGET请求后追加了idNum参数,用@Param(‘idNum’)装饰器取出参数idNum并赋值给变量id,之后,我们返回param参数和idNum参数。

image.png

4. 修改状态码

默认返回成功的状态码是200,如果我们想定制返回的状态码的话,Nest也提供了简便的装饰器@HttpCode()来手动修改状态码。我们修改/app/b的路由请求,为create方法添加@HttpCode(202)的装饰器:

// app.controller.ts
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
import { AppService } from './app.service';
​
@Controller('app')
export class AppController {
  constructor(private readonly appService: AppService) {}
​
  @Post('b')
  @HttpCode(202)
  create(@Body() body: {}): {} {
    return `Accepted: ${JSON.stringify(body)}`;
  }
}

然后我们用Postman调用POST请求/app/b,就会看到返回的状态码变成了202:

image-20240624194310796.png

如果需要更复杂的判断逻辑,可以通过@Res()注入响应对象来修改响应状态。我们在第6小节介绍。

5. 指定响应头

@HttpCode装饰器类似的,Nest也提供了@Header装饰器来指定响应头:

// app.controller.ts
import { Body, Controller, Header, HttpCode, Post } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('app')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post('b')
  @Header('Cache-Control', 'no-cache123')
  create(@Body() body: {}): {} {
    return `Accepted: ${JSON.stringify(body)}`;
  }
}

在上述代码中,我们为/app/b的post请求添加了@Header('Cache-Control', 'no-cache123')装饰器,这里的用法是@Header(key, value),当然这个no-cache123是故意这么写的,为的就是让大家明确的知道这是我们自己添加的响应头。

如果需要定义多个响应头,可以多次使用@Header()装饰器:

// app.controller.ts
import { Body, Controller, Header, Post } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('app')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post('b')
  @Header('Cache-Control', 'no-cache123')
  @Header('Content-Type', 'application/json123')
  create(@Body() body: {}): {} {
    return `Accepted: ${JSON.stringify(body)}`;
  }
}

然后我们用Postman调用/app/bPOST请求,便能看到我们自定义的响应头了:

image-20240624201929991.png

另外,还有一个特殊的响应头装饰器@Redirect(),这个装饰器是专门用来做重定向的:

// app.controller.ts
 import { Body, Controller, Get, Redirect } from '@nestjs/common';
 import { AppService } from './app.service';
 ​
 @Controller('app')
 export class AppController {
   constructor(private readonly appService: AppService) {}
 ​
   @Get('a')
   @Redirect('https://docs.nestjs.com', 302)
   create(@Body() body: {}): {} {
     return `Accepted: ${JSON.stringify(body)}`;
   }
 }

我们去掉了@Header()装饰器,将/app/bPOST请求改成了/app/aGET请求,然后添加@Redirect()装饰器,指定重定向后的路径以及响应码,接着,我们在浏览器里输入localhost:3000/app/a测试,发现/app/aGET请求已经被重定向到指定地址了:

image-20240624200958759.png

如果需要更复杂的判断逻辑,可以通过@Res()注入响应对象,然后通过res.set({xxx: xxx})来修改响应头。

6. 使用@Res()完全控制响应对象

以上就是Nest提供的一些简便的响应头装饰器,细心的同学可能会发现,@Header装饰器只能用于简单的逻辑,如果我需要根据不同的条件返回不同的响应头,这时候@Header()装饰器就有些捉襟见肘了,此时,我们就需要用@Res()装饰器注入Response实例,然后再根据不同的条件用Response实例返回不同的响应,以此提供更细粒度的控制:

// app.controller.ts
 import { Body, Controller, Post, Res } from '@nestjs/common';
 import { AppService } from './app.service';
 import { Response } from 'express';
 ​
 @Controller('app')
 export class AppController {
   constructor(private readonly appService: AppService) {}
 ​
   @Post('b')
   create(@Body() body: { key: string }, @Res() res: Response) {
     const { key } = body;
     if (key === 'value1') {
       res.set({ 'custom-header': key });
       res.status(200).send('This is value1');
     } else {
       res.set({ 'custom-header': key });
       res.status(202).send('This is other');
     }
   }
 }

在这个示例代码中,我们引入了@Res()装饰器以及Response类型,并将Response实例注入到了res属性上,在create方法内部,我们先取出body参数里的key属性,根据不同的key值,用res.set方法设置了不同的响应头,并返回了不同的状态码和内容。

现在,让我们用Postman测试一下:

首先是传参为{key: 'value1'}的情况:

image-20240624205005558.png

根据上图,当传参为{key: 'value1'}时,返回的响应体和响应头都验证了我们走的是if的条件分支。

因此,可以确定的是,当传参为{key: 'other'}的情况,走的便是我们的else分支。

image-20240624205306674.png

如此,我们便通过@Res()装饰器注入的Respose实例,实现了对于响应头更加精细的控制。

不过,使用@Res()时需要注意一下几点:

  1. 使用 @Res() 装饰器的方法不能有返回值,否则 Nest 会基于返回值生成响应,导致冲突。
  2. 使用@Res()注入响应对象时,会导致响应对象被完全控制,必须要手动模拟响应,否则HTTP服务将挂起。也就是说如果没有send()方法,会导致服务被挂起。