Nest的请求与返回

6,206 阅读6分钟

上文中,我们通过命令行建立了一个几乎是最小的Nest项目,通过了解目录和主要文件,对Nest的框架有了一个大概的概念。这离能够真正的开发还有相当一段距离,这里我们解决在Web开发中最常见的一些需求。

控制器

在Nest中,由“控制器”负责处理请求和返回业务。其中包含了:

  • 控制请求的路由
  • 控制请求的方法(Get Post等)
  • 预处理请求的对象,其中包括了请求头、请求体(body)和参数(Parameter)
  • 将请求对应到相应的资源中
  • 处理返回的HTTP状态码、重定向
  • 资源返回结果处理

控制器装饰器

一个普通的类,通过控制器装饰,被重新定义为一个控制器。我们看到app.controller.ts中,有@Controller()

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

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

如果你对装饰器还比较陌生,建议先看一下我之前的文章。我们可以跟踪Controller装饰器到其源代码位置详细了解,它支持四种参数:没有参数、字符串、字符串数组和对象。对象有两个字段,分别是路径和主机。

export function Controller(
  prefixOrOptions?: string | string[] | ControllerOptions,
): ClassDecorator {
  const defaultPath = '/';

  const [path, host, scopeOptions] = isUndefined(prefixOrOptions)
    ? [defaultPath, undefined, undefined]
    : isString(prefixOrOptions) || Array.isArray(prefixOrOptions)
    ? [prefixOrOptions, undefined, undefined]
    : [
        prefixOrOptions.path || defaultPath,
        prefixOrOptions.host,
        { scope: prefixOrOptions.scope },
      ];

  return (target: object) => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
    Reflect.defineMetadata(HOST_METADATA, host, target);
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, scopeOptions, target);
  };
}

export interface ControllerOptions extends ScopeOptions {
  path?: string | string[];
  host?: string;
}

  • 没有参数:默认路径
  • 字符串:路径
  • 字符串数组:多个别名路径
  • 对象:路径和主机,如果定义了主机,则会对请求的主机(字符串)处理

例如,设置一个/article的控制器

@Controller({ path: 'article' })
export class ArticleController {}

换做使用Express框架,相当于如下代码:

app.use('/article', require('./routes/article'))

请求

路径与方法装饰器

上面通过控制器装饰器将一个类定义为控制器,并设置了控制器的路径(父级路径)。再通过方法装饰器,将类的某一个方法定义为一个Web的请求资源节点(子级路径),有:@Get() @Post() @Put() @Delete() @Patch() @Options() @Head() @All()

@Get('what')
what(): string {
  return 'sunny';
}

装饰器(方法)仅接受一种可选(为空)字符串参数,就是子路径。如果没有输入参数,默认路径是控制器路径下的根路径/。这个参数还支持通配符和作为孙子路径的参数,例如@Get('get*') @Get(':id')

多层级路径

简单说明一下:当工程的体量达到一定规模后,一个项目中会包含几十个控制器,而控制器映射的路径并不是根据模块化或者引用关系决定的,而是直接在控制器装饰器中定义的。例如先前@Controller('articles')如果有一个子控制器detail,那么detail控制需要定义为@Controller('articles/detail')

参数装饰器

Nest有4种定义参数的装饰器@Body() @Param() @Query() @HostParam() ,与Express的机制一样,见下表:

装饰器Express 参数应用方法
@Param(key?: string)req.params / req.params[key]请求的路径目录为参数
@Body(key?: string)req.body / req.body[key]Put Post Delete在Body中的数据
@Query(key?: string)req.query / req.query[key]在请求url中?以后的参数
@HostParam(property?: string | (Type | PipeTransform)req.hosts请求主机的信息也作为参数处理

举一个简单的例子,我们修改一下app.controller

  // 参数测试
  @Get()
  getHello(@Query() q): string {
    this.logger.log(q);
    return this.appService.getHello();
  }
curl --request GET 'http://localhost:3000/?a=1&b=2'
#{
#  "a": "1",
#  "b": "2"
#}

可以看到,控制台输出了一个对象。我们再稍微修改一下app.controller代码:

  @Get()
  getHello(
    @Query('a') a: number,
    @Query('b') b: number,
    @Query('c') c: string,
  ): string {
    this.logger.log(`a=${a} & b=${b} & c=${c}`);
    return this.appService.getHello();
  }

此时控制台输出 a=1 & b=2 & c=undefined。也就是说,这个参数装饰器中可选的参数如果不写,那么就代表了所有参数,如果写了,那就是具体的某一个参数

另外这里要说一个坑点(版本:7.6.5),就是Nest似乎对 form-data 格式的提交支持的不太好。需要中间做一个转换才可以被识别,但是使用x-www-form-urlencoded没有任何问题。需要利用中断器转换一下。

  @Post('num')
  @UseInterceptors(FileInterceptor(''))
  test(@Body() _body: QFormDto) {
    this.logger.log(`[num] ${typeof _body.num}  ${_body.where}`);
    return false;
  }

如果把第二行注释掉,再执行以下代码,会发现无法获得_body信息。

curl --location --request POST 'localhost:3000/weather/num' --form 'num="3"' --form 'where="shanghai"'

如果将请求改为urlencoded之后,确是没有问题的。

curl --location --request POST 'localhost:3000/weather/num' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'num=3' \
--data-urlencode 'where=shanghai'

关于@HostParam有点特殊,配合这个装饰器,需要在控制器装饰器中定义Host信息。例如下面的代码

@Controller({ path: 'weather', host: ':ip1.:ip2.:ip3.:ip4' })
export class WeatherController {
  logger = new Logger('weather');

  @Get('/')
  getAccount(
    @HostParam('ip1') ip1: number,
    @HostParam('ip2') ip2: number,
    @HostParam('ip3') ip3: number,
    @HostParam('ip4') ip4: number,
  ) {
    this.logger.log(`ip: ${ip1}:${ip2}:${ip3}:${ip4}`);
    return 'done';
  }
}

然后访问http://127.0.0.1:3000/weather看看输出,再将其改为如下的代码,看看输出

@Controller({ path: 'weather', host: 'local:host' })
export class WeatherController {
  logger = new Logger('weather');

  @Get('/')
  getAccount(@HostParam('host') host: string) {
    this.logger.log(`host: ${host}`);
    return 'done';
  }
}

文件上传

针对文件上传,Nest官方内建了multer模块,使用起来也颇为简单:

import {
  Post,
  UseInterceptors,
  UploadedFiles,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express/multer';

@Controller()
export class AppController {
  @Post('upload')
  @UseInterceptors(FilesInterceptor('files'))
  uploadFile(@UploadedFiles() files): string {
    const result = [];
    files.map((file) => {
      this.logger.log(file.originalname);
      result.push(file.originalname);
    });
    return result.join(',');
  }
}    

以上代码有三个过程装饰器包含起来:

  1. @UserInterceptors() 使用一个中断器;
  2. FilesInterceptor() 将multer封装为一个处理文件的中断器(IoC),指定处理专门的字段;
  3. @UploadedFiles() 用装饰器将被上传的文件定义为方法的参数;

req对象

默认情况下,Nest封装了Express框架(目前官方还封装了Fastify框架),如果需要一些较为底层的操作,可以直接使用req对象,就像在Express中开发一样使用。

import {
  Controller,
  Get,
  Logger,
  Render,
  Req
} from '@nestjs/common';
import { AppService } from './app.service';
import {  Request } from 'express';

@Controller()
export class AppController {
  logger = new Logger('root');
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(@Req() req: Request): string {
    this.logger.log(req.headers['user-agent']);
    return this.appService.getHello();
  }
}

返回

Nest利用方法装饰器会自动将返回的对象处理成HTTP的数据包。

返回对象

主要可以将其分为三个部分:HTTP状态码,Header部分和Body部分;

Body

如果返回本来就是string,那么返回的header中content type就是text/html,如果是对象,则会将对象序列化为Json,Header设置为application/json。

Header

例如需要返回(下载)一个文件,需要对返回内容的头信息进行一定处理,利用装饰器Header。它接受两个字符串为参数,分别是Header中的key和value。下面以文件下载的接口为例,来简单描述下Header的用法:

  @Get('/file')
  @Header('Content-Disposition', 'attachment; filename=controller.js')
  getFile() {
    const file = fs.readFileSync(join(__dirname, 'weather.controller.js'));
    return file;
  }

HTTP状态码

利用@HttpCode()装饰器,可以自定义返回的HTTP状态码,譬如说

  @Get('pug')
  @HttpCode(404)
  @Render('index')
  pug() {
    return { title: `Pug ${Math.random()}`, message: 'Hello world' };
  }

通过这种方式,开发者可以自行设计诸如未验证、无授权、未找到页面等等的异常状态页面。

HTML模板

Nest很好的继承了Express所支持的MVC模板,例如pug、hbs。使用之前先安装模板引擎

npm install --save hbs # hbs模板
npm install --save pug # pug模板

以pug为例,在main.ts中加入引用引擎

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.setBaseViewsDir(join(__dirname, '..', 'views'));
  app.setViewEngine('pug');
  await app.listen(3000);
}
bootstrap();

注意:Nest的核心工厂,在创建时一定要定义一个泛型NestExpressApplication,app设置pug为视图引擎。相当于在Express框架中使用app.set('view engine', 'pug')

在项目的更目录下创建一个views目录,新建一个index.pug:

html
  head
    title Pug模板
  body
    h1 #{title}
    p #{message}

最后,创建一个控制器中的资源,可以被最终访问到:

  @Get('pug')
  @Render('index')
  pug() {
    return { title: 'Pug', message: 'Hello world' };
  }

其中@Render()装饰器方法唯一一个参数是模板位置和名词字符串;

重定向

Nest主要有两种重定向的方法,@Redirect(url?: string, statusCode?: number)装饰器重定向和res.redirect(),在底层上,用了Web服务框架(Express)的res.redirect。Redirect中也可以不带任何参数,那么跳转的url就需要在return中定义。如果在装饰器设置了跳转地址同时又在返回值中定义了url,那么程序会执行return的url对象。

  @Get('redirect')
  @Redirect()
  redirect() {
    this.logger.log('redirect to pug');
    return { url: '/pug', "statusCode":301 };
  }

第二种方式是使用底层的对象req。需要引入express的Response

  import { Response } from 'express';
// ...
  @Get('redirect')
  redirect(@Res() res: Response) {
    this.logger.log('res.direct...');
    res.redirect('/pug');
  }

next

next是Express跳转到下一个中间件的函数,其功能有点类似于一种重定向,只不过他发生在一次请求的过程之中。

  @Get()
  getHello(
    @Req() req: Request,
    @Res() res: Response,
    @Next() next: NextFunction,
  ) {
    this.logger.log(req.headers['user-agent']);
    res.setHeader('Warning', 'This is a demo');
    next();
  }

  @Get('/')
  getNext_1(@Next() next: NextFunction) {
    this.logger.log('Do something different.');
    next();
  }

  @Get('/*')
  getNext_2(@Query('where') where: string): string {
    this.logger.log(`${where}: Do it again.`);
    return this.appService.getHello();
  }

如果是在控制器内部,那么会按照方法的编写顺序,逐个传递执行。每个执行的方法请求(req)和响应(res)都是保持一致的。next函数不需要带有参数,next的第一个参数是传递错误对象。虽然可以,但是并不建议用next的方式做夸控制器之间的传递。

res对象

与req对象对应,返回的信息最后也会经由Request来处理,Nest同样提供了一个装饰器来继续从Web框架中再次接管返回处理。

import {Controller, Get, Logger, Res, Req } from '@nestjs/common';
import { AppService } from './app.service';
import { Response, Request } from 'express';
export class AppController {
  logger = new Logger('root');
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(@Req() req: Request, @Res() res: Response): void {
    this.logger.log(req.headers['user-agent']);
    res.setHeader('Warning', 'This is a demo');
    res.send(this.appService.getHello());
  }
}

需要注意:使用Res对象,就需要从Nest手中接管Response对象,那么这也就意味着后续的操作都将转交给用户操作,所以getHello方法如果还是return,则会无效。要避免这种情况,可以在装饰器中加入一个参数{ passthrough: true }来将控制权在方法内部逻辑执行完成之后再次交还给Nest。特别是当前方法同时还使用路由传递(Next)的功能。

  @Get()
  getHello(
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response,
  ): string {
    this.logger.log(req.headers['user-agent']);
    res.setHeader('Warning', 'This is a demo');
    return this.appService.getHello();
  }